From bb27c637e6f9075b5e147b4c533756cb926516c1 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 11:54:40 +0100 Subject: [PATCH 01/15] Add support for float type with the help of Codex and Claude --- .../ezlang/lexer/Lexer.java | 2 +- .../ezlang/compiler/CompiledFunction.java | 68 ++- .../ConstantComparisonPropagation.java | 6 +- .../ezlang/compiler/ExitSSABriggs.java | 11 +- .../ezlang/compiler/Instruction.java | 2 +- .../ezlang/compiler/Operand.java | 31 +- .../SparseConditionalConstantPropagation.java | 553 ++++++++++++------ .../ezlang/interpreter/Interpreter.java | 167 +++--- .../ezlang/interpreter/Value.java | 6 + .../compiler/TestInterferenceGraph.java | 14 +- .../ezlang/compiler/TestLiveness.java | 20 +- .../ezlang/compiler/TestSCCP.java | 90 ++- .../ezlang/compiler/TestSSADestructB.java | 8 +- .../ezlang/compiler/TestSSATransform.java | 8 +- .../ezlang/interpreter/TestInterpreter.java | 171 ++++++ .../ezlang/parser/Parser.java | 2 +- .../ezlang/parser/TestParser.java | 7 + .../ezlang/compiler/CompiledFunction.java | 68 ++- .../ezlang/compiler/Operand.java | 15 +- .../ezlang/interpreter/Interpreter.java | 178 +++--- .../ezlang/interpreter/Value.java | 7 +- .../ezlang/interpreter/TestInterpreter.java | 60 ++ .../ezlang/compiler/Compiler.java | 45 +- .../ezlang/semantic/NullableAnalysis.java | 154 ++++- .../ezlang/semantic/SemaAssignTypes.java | 32 +- .../ezlang/semantic/TestSemaAssignTypes.java | 49 ++ .../ezlang/compiler/CompiledFunction.java | 30 +- .../ezlang/compiler/Instruction.java | 29 +- .../ezlang/types/EZType.java | 21 +- .../ezlang/types/TypeDictionary.java | 6 + .../ezlang/types/TestTypes.java | 9 + 31 files changed, 1380 insertions(+), 489 deletions(-) diff --git a/lexer/src/main/java/com/compilerprogramming/ezlang/lexer/Lexer.java b/lexer/src/main/java/com/compilerprogramming/ezlang/lexer/Lexer.java index 87f44c9..295c7e1 100644 --- a/lexer/src/main/java/com/compilerprogramming/ezlang/lexer/Lexer.java +++ b/lexer/src/main/java/com/compilerprogramming/ezlang/lexer/Lexer.java @@ -31,7 +31,7 @@ private Token parseNumber() { int startPosition = position++; while (position < input.length && Character.isDigit(input[position])) position++; - if (input[position] == '.') { + if (position < input.length && input[position] == '.') { position++; while (position < input.length && Character.isDigit(input[position])) position++; diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java index 6ce9df8..235d3ec 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java @@ -217,6 +217,7 @@ private void compileWhile(AST.WhileStmt whileStmt) { currentBreakTarget = exitBlock; currentContinueTarget = loopHead; startBlock(loopHead); // ISSA cannot seal until all back edges done + checkConditionType(whileStmt.condition.type, whileStmt.lineNumber); boolean indexed = compileExpr(whileStmt.condition); if (indexed) codeIndexedLoad(); @@ -232,6 +233,11 @@ private void compileWhile(AST.WhileStmt whileStmt) { currentBreakTarget = savedBreakTarget; } + private void checkConditionType(EZType conditionType, int lineNumber) { + if (!(conditionType instanceof EZType.EZTypeInteger)) + throw new CompilerException("Condition expression must be Int type", lineNumber); + } + private boolean isBlockTerminated(BasicBlock block) { return (block.instructions.size() > 0 && block.instructions.getLast().isTerminal()); @@ -260,6 +266,7 @@ private void compileIf(AST.IfElseStmt ifElseStmt) { boolean needElse = ifElseStmt.elseStmt != null; BasicBlock elseBlock = needElse ? createBlock() : null; BasicBlock exitBlock = createBlock(); + checkConditionType(ifElseStmt.condition.type, ifElseStmt.lineNumber); boolean indexed = compileExpr(ifElseStmt.condition); if (indexed) codeIndexedLoad(); @@ -455,6 +462,8 @@ private boolean compileSymbolExpr(AST.NameExpr symbolExpr) { } private boolean codeBoolean(AST.BinaryExpr binaryExpr) { + checkConditionType(binaryExpr.expr1.type, binaryExpr.lineNumber); + checkConditionType(binaryExpr.expr2.type, binaryExpr.lineNumber); boolean isAnd = binaryExpr.op.str.equals("&&"); BasicBlock l1 = createBlock(); BasicBlock l2 = createBlock(); @@ -473,7 +482,7 @@ private boolean codeBoolean(AST.BinaryExpr binaryExpr) { jumpTo(l3); startSealedBlock(l2); // ISSA seal immediately // Below we must write to the same temp - codeMove(new Operand.ConstantOperand(isAnd ? 0 : 1, typeDictionary.INT), temp); + codeMove(new Operand.IntConstantOperand(isAnd ? 0 : 1, typeDictionary.INT), temp); jumpTo(l3); startSealedBlock(l3); // ISSA seal immediately // leave temp on virtual stack @@ -502,10 +511,10 @@ private boolean compileBinaryExpr(AST.BinaryExpr binaryExpr) { case "!=": value = 0; break; default: throw new CompilerException("Invalid binary op", binaryExpr.lineNumber); } - pushConstant(value, typeDictionary.INT); + pushIntConstant(value, typeDictionary.INT); } - else if (left instanceof Operand.ConstantOperand leftconstant && - right instanceof Operand.ConstantOperand rightconstant) { + else if (left instanceof Operand.IntConstantOperand leftconstant && + right instanceof Operand.IntConstantOperand rightconstant) { long value = 0; switch (opCode) { case "+": value = leftconstant.value + rightconstant.value; break; @@ -518,10 +527,26 @@ else if (left instanceof Operand.ConstantOperand leftconstant && case "<": value = leftconstant.value < rightconstant.value ? 1: 0; break; case ">": value = leftconstant.value > rightconstant.value ? 1 : 0; break; case "<=": value = leftconstant.value <= rightconstant.value ? 1 : 0; break; - case ">=": value = leftconstant.value <= rightconstant.value ? 1 : 0; break; + case ">=": value = leftconstant.value >= rightconstant.value ? 1 : 0; break; default: throw new CompilerException("Invalid binary op", binaryExpr.lineNumber); } - pushConstant(value, leftconstant.type); + pushIntConstant(value, binaryExpr.type); + } + else if (left instanceof Operand.FloatConstantOperand leftconstant && + right instanceof Operand.FloatConstantOperand rightconstant) { + switch (opCode) { + case "+" -> pushFloatConstant(leftconstant.value + rightconstant.value, binaryExpr.type); + case "-" -> pushFloatConstant(leftconstant.value - rightconstant.value, binaryExpr.type); + case "*" -> pushFloatConstant(leftconstant.value * rightconstant.value, binaryExpr.type); + case "/" -> pushFloatConstant(leftconstant.value / rightconstant.value, binaryExpr.type); + case "==" -> pushIntConstant(leftconstant.value == rightconstant.value ? 1 : 0, binaryExpr.type); + case "!=" -> pushIntConstant(leftconstant.value != rightconstant.value ? 1 : 0, binaryExpr.type); + case "<" -> pushIntConstant(leftconstant.value < rightconstant.value ? 1 : 0, binaryExpr.type); + case ">" -> pushIntConstant(leftconstant.value > rightconstant.value ? 1 : 0, binaryExpr.type); + case "<=" -> pushIntConstant(leftconstant.value <= rightconstant.value ? 1 : 0, binaryExpr.type); + case ">=" -> pushIntConstant(leftconstant.value >= rightconstant.value ? 1 : 0, binaryExpr.type); + default -> throw new CompilerException("Invalid binary op", binaryExpr.lineNumber); + } } else { var temp = createTemp(binaryExpr.type); @@ -537,11 +562,17 @@ private boolean compileUnaryExpr(AST.UnaryExpr unaryExpr) { codeIndexedLoad(); opCode = unaryExpr.op.str; Operand top = pop(); - if (top instanceof Operand.ConstantOperand constant) { + if (top instanceof Operand.IntConstantOperand constant) { switch (opCode) { - case "-": pushConstant(-constant.value, constant.type); break; + case "-": pushIntConstant(-constant.value, constant.type); break; // Maybe below we should explicitly set Int - case "!": pushConstant(constant.value == 0?1:0, constant.type); break; + case "!": pushIntConstant(constant.value == 0?1:0, typeDictionary.INT); break; + default: throw new CompilerException("Invalid unary op", unaryExpr.lineNumber); + } + } + else if (top instanceof Operand.FloatConstantOperand constant) { + switch (opCode) { + case "-": pushFloatConstant(-constant.value, constant.type); break; default: throw new CompilerException("Invalid unary op", unaryExpr.lineNumber); } } @@ -554,15 +585,21 @@ private boolean compileUnaryExpr(AST.UnaryExpr unaryExpr) { private boolean compileConstantExpr(AST.LiteralExpr constantExpr) { if (constantExpr.type instanceof EZType.EZTypeInteger) - pushConstant(constantExpr.value.num.intValue(), constantExpr.type); + pushIntConstant(constantExpr.value.num.intValue(), constantExpr.type); + else if (constantExpr.type instanceof EZType.EZTypeFloat) + pushFloatConstant(constantExpr.value.num.doubleValue(), constantExpr.type); else if (constantExpr.type instanceof EZType.EZTypeNull) pushNullConstant(constantExpr.type); else throw new CompilerException("Invalid constant type", constantExpr.lineNumber); return false; } - private void pushConstant(long value, EZType type) { - pushOperand(new Operand.ConstantOperand(value, type)); + private void pushIntConstant(long value, EZType type) { + pushOperand(new Operand.IntConstantOperand(value, type)); + } + + private void pushFloatConstant(double value, EZType type) { + pushOperand(new Operand.FloatConstantOperand(value, type)); } private void pushNullConstant(EZType type) { @@ -576,7 +613,9 @@ private Operand.TempRegisterOperand createTemp(EZType type) { } EZType typeOfOperand(Operand operand) { - if (operand instanceof Operand.ConstantOperand constant) + if (operand instanceof Operand.IntConstantOperand constant) + return constant.type; + else if (operand instanceof Operand.FloatConstantOperand constant) return constant.type; else if (operand instanceof Operand.NullConstantOperand nullConstantOperand) return nullConstantOperand.type; @@ -594,7 +633,8 @@ private Operand.TempRegisterOperand createTempAndMove(Operand src) { private Operand.RegisterOperand ensureTemp() { Operand top = top(); - if (top instanceof Operand.ConstantOperand + if (top instanceof Operand.IntConstantOperand + || top instanceof Operand.FloatConstantOperand || top instanceof Operand.NullConstantOperand || top instanceof Operand.LocalRegisterOperand) { return createTempAndMove(pop()); diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ConstantComparisonPropagation.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ConstantComparisonPropagation.java index f3ef77c..aba4454 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ConstantComparisonPropagation.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ConstantComparisonPropagation.java @@ -72,13 +72,13 @@ private void propagateConstantsInComparisons(BasicBlock block) { && binary.binOp.equals("==")) { // Get the constant and the register operands // from the binary - Operand.ConstantOperand constantOp = null; + Operand.IntConstantOperand constantOp = null; Operand.RegisterOperand registerOp = null; - if (binary.left() instanceof Operand.ConstantOperand leftConstant + if (binary.left() instanceof Operand.IntConstantOperand leftConstant && binary.right() instanceof Operand.RegisterOperand rightReg) { constantOp = leftConstant; registerOp = rightReg; - } else if (binary.right() instanceof Operand.ConstantOperand rightConstant + } else if (binary.right() instanceof Operand.IntConstantOperand rightConstant && binary.left() instanceof Operand.RegisterOperand leftReg) { constantOp = rightConstant; registerOp = leftReg; diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABriggs.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABriggs.java index c7a4c9b..e65ab2d 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABriggs.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABriggs.java @@ -167,9 +167,12 @@ private void scheduleCopies(BasicBlock block, List pushed) { workList.add(item); } } - else if (src instanceof Operand.ConstantOperand srcConstantOperand) { - addMoveAtBBEnd(block, srcConstantOperand, dest); + else if (src instanceof Operand.IntConstantOperand || + src instanceof Operand.FloatConstantOperand || + src instanceof Operand.NullConstantOperand) { + addMoveAtBBEnd(block, src, dest); } + else throw new IllegalStateException("Unexpected phi source operand: " + src); } /* Engineering a Compiler: To solve the swap problem we can detect cases where phi functions reference the @@ -253,8 +256,8 @@ private void addMoveAtBBEnd(BasicBlock block, Register src, Register dest) { cbr.replaceUse(src,dest); } } - /* Insert a copy from constant src to dst at end of BB */ - private void addMoveAtBBEnd(BasicBlock block, Operand.ConstantOperand src, Register dest) { + /* Insert a copy from constant src (int, float or null) to dst at end of BB */ + private void addMoveAtBBEnd(BasicBlock block, Operand src, Register dest) { var inst = new Instruction.Move(src, new Operand.RegisterOperand(dest)); insertAtEnd(block, inst); } diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java index b4b506a..95152ef 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java @@ -118,7 +118,7 @@ public boolean replaceUse(Register source, Register target) { /** * Replaces all uses of given register with the constant operand */ - public void replaceUseWithConstant(Register register, Operand.ConstantOperand constantOperand) { + public void replaceUseWithConstant(Register register, Operand constantOperand) { for (int i = 0; i < uses.length; i++) { Operand operand = uses[i]; if (operand != null && operand instanceof Operand.RegisterOperand registerOperand && registerOperand.reg.id == register.id) { diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java index 7759cbc..a24666f 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java @@ -9,9 +9,9 @@ public class Operand { EZType type; - public static class ConstantOperand extends Operand { + public static class IntConstantOperand extends Operand { public final long value; - public ConstantOperand(long value, EZType type) { + public IntConstantOperand(long value, EZType type) { this.value = value; this.type = type; } @@ -24,7 +24,7 @@ public String toString() { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ConstantOperand that = (ConstantOperand) o; + IntConstantOperand that = (IntConstantOperand) o; return value == that.value; } @@ -34,6 +34,31 @@ public int hashCode() { } } + public static class FloatConstantOperand extends Operand { + public final double value; + public FloatConstantOperand(double value, EZType type) { + this.value = value; + this.type = type; + } + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FloatConstantOperand that = (FloatConstantOperand) o; + return Double.compare(value, that.value) == 0; + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + } + public static class NullConstantOperand extends Operand { public NullConstantOperand(EZType type) { this.type = type; diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java index ba96971..a2f4049 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java @@ -202,9 +202,11 @@ private void removeEdge(BasicBlock source, BasicBlock target) { private void replaceVarsWithConstants() { for (var register: valueLattice.getRegisters()) { var latticeElement = valueLattice.get(register); - if (latticeElement.kind == V_CONSTANT) { - var constant = new Operand.ConstantOperand(latticeElement.value, register.type); + if (latticeElement.isReplaceableConstant()) { + var constant = latticeElement.asOperand(register.type); var defUseChain = this.ssaEdges.get(register); + if (defUseChain == null) + continue; // replace uses with constant for (var usingInstruction: defUseChain.useList) { if (executableBlocks.get(usingInstruction.block.bid)) @@ -219,67 +221,149 @@ private void replaceVarsWithConstants() { } } - static final byte V_UNDEFINED = 1; // TOP - static final byte V_CONSTANT = 2; - static final byte V_VARYING = 3; // BOTTOM + static final byte F_TOP = 0; // no usable fact yet + + static final byte F_REF_TOP = 1; // known reference type, value unknown + static final byte F_REF_NOT_NULL = 2; + static final byte F_REF_NULL = 3; + static final byte F_REF_BOTTOM = 4; // maybe null + + static final byte F_INT_TOP = 5; // known int type, value unknown + static final byte F_INT_ZERO = 6; + static final byte F_INT_NONZERO_CONST = 7; + static final byte F_INT_NONZERO_VARYING = 8; + static final byte F_INT_BOTTOM = 9; // may be zero or non-zero + + static final byte F_FLT_TOP = 10; // known float type, value unknown + static final byte F_FLT_CONST = 11; + static final byte F_FLT_BOTTOM = 12; // non-const float + + static final byte F_BOTTOM = 13; // impossible / contradiction // Associated with each register static final class LatticeElement { private byte kind; - private long value; - - public LatticeElement(byte kind, long value) { - this.kind = kind; - this.value = value; - } - - boolean meet(long value) { - byte oldKind = this.kind; - long oldValue = this.value; - if (kind == V_UNDEFINED) { - kind = V_CONSTANT; - this.value = value; - } else if (kind == V_CONSTANT && this.value != value) { - kind = V_VARYING; - } - return kind != oldKind || value != oldValue; - } + private long intValue; + private double floatValue; + + LatticeElement(byte kind) { this.kind = kind; } + LatticeElement(byte kind, long value) { this.kind = kind; this.intValue = value; } + LatticeElement(long value) { kind = F_INT_TOP; setIntValue(value); } + LatticeElement(double value) { kind = F_FLT_TOP; setFloatValue(value); } + LatticeElement copy() { var copy = new LatticeElement(kind, intValue); copy.floatValue = floatValue; return copy; } + void copyFrom(LatticeElement other) { kind = other.kind; intValue = other.intValue; floatValue = other.floatValue; } + + boolean isTrue() { return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; } + boolean isFalse() { return kind == F_INT_ZERO; } + boolean isIntegerConstant() { return kind == F_INT_ZERO || kind == F_INT_NONZERO_CONST; } + boolean isFloatConstant() { return kind == F_FLT_CONST; } + boolean isNullConstant() { return kind == F_REF_NULL; } + boolean isDefiniteReference() { return kind == F_REF_NULL || kind == F_REF_NOT_NULL; } + boolean isReplaceableConstant() { return isIntegerConstant() || isFloatConstant() || isNullConstant(); } + + boolean setIntValue(long value) { + byte old = kind; + if (kind == F_TOP || kind == F_INT_TOP) { intValue = value; kind = value == 0 ? F_INT_ZERO : F_INT_NONZERO_CONST; } + else if (kind == F_INT_ZERO && value != 0) kind = F_INT_BOTTOM; + else if (kind == F_INT_NONZERO_CONST && (value == 0 || value != intValue)) kind = F_INT_BOTTOM; + else if (!isInteger()) kind = F_BOTTOM; + return kind != old; + } + boolean setFloatValue(double value) { + byte old = kind; + if (kind == F_TOP || kind == F_FLT_TOP) { floatValue = value; kind = F_FLT_CONST; } + else if (kind == F_FLT_CONST && Double.compare(value, floatValue) != 0) kind = F_FLT_BOTTOM; + else if (!isFloat()) kind = F_BOTTOM; + return kind != old; + } + boolean meetWithTypeBottom(EZType type) { return meet(factBottomFromType(type)); } boolean meet(LatticeElement other) { - byte oldKind = this.kind; - long oldValue = this.value; - if (kind == V_UNDEFINED) { - kind = other.kind; - value = other.value; - } else if (kind == V_CONSTANT) { - if (other.kind == V_CONSTANT) { - if (other.value != value) { - kind = V_VARYING; + byte old = kind; + long oldInt = intValue; + double oldFloat = floatValue; + if (kind == F_TOP) { copyFrom(other); return changed(old, oldInt, oldFloat); } + if (kind == F_BOTTOM || other.kind == F_TOP) return false; + if (other.kind == F_BOTTOM) { kind = F_BOTTOM; return changed(old, oldInt, oldFloat); } + if (kind == other.kind) { + if (kind == F_INT_NONZERO_CONST && intValue != other.intValue) kind = F_INT_NONZERO_VARYING; + else if (kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0) kind = F_FLT_BOTTOM; + return changed(old, oldInt, oldFloat); + } + if (isReference(kind) && isReference(other.kind)) { kind = meetReference(other); return changed(old, oldInt, oldFloat); } + if (isInteger(kind) && isInteger(other.kind)) { meetInteger(other); return changed(old, oldInt, oldFloat); } + if (isFloat(kind) && isFloat(other.kind)) { meetFloat(other); return changed(old, oldInt, oldFloat); } + kind = F_BOTTOM; + return changed(old, oldInt, oldFloat); + } + private boolean changed(byte oldKind, long oldInt, double oldFloat) { return kind != oldKind || intValue != oldInt || Double.compare(floatValue, oldFloat) != 0; } + private byte meetReference(LatticeElement other) { + if (kind == F_REF_TOP) return other.kind; + if (other.kind == F_REF_TOP) return kind; + return switch (kind) { + case F_REF_NULL -> other.kind == F_REF_NULL ? F_REF_NULL : F_REF_BOTTOM; + case F_REF_NOT_NULL -> other.kind == F_REF_NOT_NULL ? F_REF_NOT_NULL : F_REF_BOTTOM; + case F_REF_BOTTOM -> F_REF_BOTTOM; + default -> F_BOTTOM; + }; + } + private void meetInteger(LatticeElement other) { + if (kind == F_INT_TOP) { copyFrom(other); return; } + if (other.kind == F_INT_TOP) return; + switch (kind) { + case F_INT_ZERO -> { if (other.kind != F_INT_ZERO) kind = F_INT_BOTTOM; } + case F_INT_NONZERO_CONST -> { + switch (other.kind) { + case F_INT_NONZERO_CONST -> { if (intValue != other.intValue) kind = F_INT_NONZERO_VARYING; } + case F_INT_NONZERO_VARYING -> kind = F_INT_NONZERO_VARYING; + case F_INT_ZERO, F_INT_BOTTOM -> kind = F_INT_BOTTOM; } - } else if (other.kind == V_VARYING) { - kind = V_VARYING; } - } - return kind != oldKind || value != oldValue; - } - - public boolean setKind(byte kind) { - byte oldKind = this.kind; - this.kind = kind; - return oldKind != kind; + case F_INT_NONZERO_VARYING -> { if (other.kind == F_INT_ZERO || other.kind == F_INT_BOTTOM) kind = F_INT_BOTTOM; } + case F_INT_BOTTOM -> { } + } + } + private void meetFloat(LatticeElement other) { + if (kind == F_FLT_TOP) { copyFrom(other); return; } + if (other.kind == F_FLT_TOP) return; + if (kind == F_FLT_CONST && (other.kind == F_FLT_BOTTOM || (other.kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0))) + kind = F_FLT_BOTTOM; + } + boolean isReference() { return isReference(kind); } + boolean isInteger() { return isInteger(kind); } + boolean isFloat() { return isFloat(kind); } + private static boolean isReference(byte kind) { return kind >= F_REF_TOP && kind <= F_REF_BOTTOM; } + private static boolean isInteger(byte kind) { return kind >= F_INT_TOP && kind <= F_INT_BOTTOM; } + private static boolean isFloat(byte kind) { return kind >= F_FLT_TOP && kind <= F_FLT_BOTTOM; } + + Operand asOperand(EZType type) { + if (kind == F_INT_ZERO) return new Operand.IntConstantOperand(0, type); + if (kind == F_INT_NONZERO_CONST) return new Operand.IntConstantOperand(intValue, type); + if (kind == F_FLT_CONST) return new Operand.FloatConstantOperand(floatValue, type); + if (kind == F_REF_NULL) return new Operand.NullConstantOperand(type); + throw new IllegalStateException("Lattice value is not a constant: " + this); } @Override public String toString() { - if (kind == V_UNDEFINED) { - return "undefined"; - } - else if (kind == V_CONSTANT) { - return String.valueOf(value); - } - return "varying"; + return switch (kind) { + case F_TOP -> "T"; + case F_REF_TOP -> "ref"; + case F_INT_TOP -> "int"; + case F_FLT_TOP -> "flt"; + case F_REF_NOT_NULL -> "not-null"; + case F_REF_NULL -> "null"; + case F_REF_BOTTOM -> "maybe-null"; + case F_INT_BOTTOM -> "int*"; + case F_FLT_BOTTOM -> "flt*"; + case F_INT_ZERO -> "0"; + case F_INT_NONZERO_CONST -> Long.toString(intValue); + case F_INT_NONZERO_VARYING -> "non-zero"; + case F_FLT_CONST -> Double.toString(floatValue); + case F_BOTTOM -> "⊥"; + default -> throw new CompilerException("Unknown lattice kind: " + kind); + }; } } - // A CFG edge static final class FlowEdge { BasicBlock source; @@ -330,117 +414,84 @@ private boolean evalInstruction(Instruction instruction) { var cell = valueLattice.get(toReg.reg); if (moveInst.from() instanceof Operand.RegisterOperand fromReg) { changed = cell.meet(valueLattice.get(fromReg.reg)); - } else if (moveInst.from() instanceof Operand.ConstantOperand constantOperand) { - changed = cell.meet(constantOperand.value); + } else if (isConstantOperand(moveInst.from())) { + changed = cell.meet(factFromOperand(moveInst.from())); } else throw new IllegalStateException(); } else throw new IllegalStateException(); } - case Instruction.Jump jumpInst -> { - changed = markEdgeExecutable(block, jumpInst.jumpTo); - } + case Instruction.Jump jumpInst -> changed = markEdgeExecutable(block, jumpInst.jumpTo); case Instruction.ConditionalBranch cbrInst -> { - if (cbrInst.condition() instanceof Operand.RegisterOperand registerOperand) { - var cell = valueLattice.get(registerOperand.reg); - if (cell.kind == V_CONSTANT) { - if (cell.value != 0) { - changed = markEdgeExecutable(block, cbrInst.trueBlock); - } else { - changed = markEdgeExecutable(block, cbrInst.falseBlock); - } - } else if (cell.kind == V_VARYING) { - boolean changed0 = markEdgeExecutable(block, cbrInst.trueBlock); - boolean changed1 = markEdgeExecutable(block, cbrInst.falseBlock); - changed = changed0 || changed1; - } - } else if (cbrInst.condition() instanceof Operand.ConstantOperand constantOperand) { - if (constantOperand.value != 0) { - changed = markEdgeExecutable(block, cbrInst.trueBlock); - } else { - changed = markEdgeExecutable(block, cbrInst.falseBlock); - } - } else throw new IllegalStateException(); + LatticeElement condition; + if (cbrInst.condition() instanceof Operand.RegisterOperand registerOperand) + condition = valueLattice.get(registerOperand.reg); + else if (isConstantOperand(cbrInst.condition())) + condition = factFromOperand(cbrInst.condition()); + else + throw new IllegalStateException(); + + if (condition.isFloat() || condition.isReference()) + throw new CompilerException("Condition expression must be Int type"); + if (condition.isFalse()) + changed = markEdgeExecutable(block, cbrInst.falseBlock); + else if (condition.isTrue()) + changed = markEdgeExecutable(block, cbrInst.trueBlock); + else if (condition.kind == F_INT_BOTTOM || condition.kind == F_BOTTOM) { + boolean changed0 = markEdgeExecutable(block, cbrInst.trueBlock); + boolean changed1 = markEdgeExecutable(block, cbrInst.falseBlock); + changed = changed0 || changed1; + } } case Instruction.Call callInst -> { if (!(callInst.callee.returnType instanceof EZType.EZTypeVoid)) { var cell = valueLattice.get(callInst.returnOperand().reg); - changed = cell.setKind(V_VARYING); + changed = cell.meetWithTypeBottom(callInst.returnOperand().reg.type); } } case Instruction.Unary unaryInst -> { - Operand.RegisterOperand unaryOperand = (Operand.RegisterOperand) unaryInst.operand(); var cell = valueLattice.get(unaryInst.result().reg); - var input = valueLattice.get(unaryOperand.reg); - if (input.kind == V_CONSTANT) { - changed = cell.meet(unaryInst.unop.equals("-") ? -input.value : (input.value == 0 ? 1 : 0)); - } else { - changed = cell.meet(input); - } + LatticeElement input = factFromOperandOrRegister(unaryInst.operand()); + changed = input == null ? cell.meetWithTypeBottom(unaryInst.result().reg.type) : evalUnary(cell, input, unaryInst.unop, unaryInst.result().reg.type); } case Instruction.Binary binaryInst -> { var cell = valueLattice.get(binaryInst.result().reg); - LatticeElement left = null; - LatticeElement right = null; - // TODO we cannot yet evaluate null in comparisons - if (binaryInst.left() instanceof Operand.ConstantOperand constant) - left = new LatticeElement(V_CONSTANT, constant.value); - else if (binaryInst.left() instanceof Operand.RegisterOperand registerOperand) - left = valueLattice.get(registerOperand.reg); - if (binaryInst.right() instanceof Operand.ConstantOperand constant) - right = new LatticeElement(V_CONSTANT, constant.value); - else if (binaryInst.right() instanceof Operand.RegisterOperand registerOperand) - right = valueLattice.get(registerOperand.reg); + LatticeElement left = factFromOperandOrRegister(binaryInst.left()); + LatticeElement right = factFromOperandOrRegister(binaryInst.right()); if (left != null && right != null) { switch (binaryInst.binOp) { - case "+": - case "-": - case "*": - case "/": - case "%": - changed = evalArith(cell, left, right, binaryInst.binOp); - break; - case "==": - case "!=": - case "<": - case ">": - case "<=": - case ">=": - changed = evalLogical(cell, left, right, binaryInst.binOp); - break; - default: - throw new IllegalStateException(); + case "+", "-", "*", "/", "%" -> changed = evalArith(cell, left, right, binaryInst.binOp, binaryInst.result().reg.type); + case "==", "!=", "<", ">", "<=", ">=" -> changed = evalLogical(cell, left, right, binaryInst.binOp, binaryInst.result().reg.type); + default -> throw new IllegalStateException(); } } else { - cell.setKind(V_VARYING); + changed = cell.meetWithTypeBottom(binaryInst.result().reg.type); } } case Instruction.NewArray newArrayInst -> { var cell = valueLattice.get(newArrayInst.destOperand().reg); - changed = cell.setKind(V_VARYING); + changed = cell.meet(new LatticeElement(F_REF_NOT_NULL)); } case Instruction.NewStruct newStructInst -> { var cell = valueLattice.get(newStructInst.destOperand().reg); - changed = cell.setKind(V_VARYING); + changed = cell.meet(new LatticeElement(F_REF_NOT_NULL)); } case Instruction.ArrayStore arrayStoreInst -> { } case Instruction.ArrayLoad arrayLoadInst -> { var cell = valueLattice.get(arrayLoadInst.destOperand().reg); - changed = cell.setKind(V_VARYING); + changed = cell.meetWithTypeBottom(arrayLoadType(arrayLoadInst)); } case Instruction.SetField setFieldInst -> { } case Instruction.GetField getFieldInst -> { var cell = valueLattice.get(getFieldInst.destOperand().reg); - changed = cell.setKind(V_VARYING); + changed = cell.meetWithTypeBottom(fieldLoadType(getFieldInst)); } case Instruction.ArgInstruction argInst -> { var cell = valueLattice.get(argInst.def()); - changed = cell.setKind(V_VARYING); - } - case Instruction.Phi phiInst -> { - changed = visitPhi(block, phiInst); + changed = cell.meetWithTypeBottom(argInst.def().type); } + case Instruction.Phi phiInst -> changed = visitPhi(block, phiInst); default -> throw new IllegalStateException("Unexpected value: " + instruction); } return changed; @@ -448,7 +499,7 @@ else if (binaryInst.right() instanceof Operand.RegisterOperand registerOperand) private boolean visitPhi(BasicBlock block, Instruction.Phi phiInst) { LatticeElement oldValue = valueLattice.get(phiInst.value()); - LatticeElement newValue = new LatticeElement(V_UNDEFINED, 0); + LatticeElement newValue = new LatticeElement(F_TOP); for (int j = 0; j < block.predecessors.size(); j++) { BasicBlock pred = block.predecessors.get(j); // We ignore non-executable edges @@ -457,8 +508,8 @@ private boolean visitPhi(BasicBlock block, Instruction.Phi phiInst) { LatticeElement varValue = valueLattice.get(phiInst.inputAsRegister(j)); newValue.meet(varValue); } - else if (phiInst.input(j) instanceof Operand.ConstantOperand constantOperand) { - newValue.meet(constantOperand.value); + else if (isConstantOperand(phiInst.input(j))) { + newValue.meet(factFromOperand(phiInst.input(j))); } } } @@ -480,79 +531,196 @@ private boolean markEdgeExecutable(BasicBlock source, BasicBlock target) { return false; } - private static boolean evalLogical(LatticeElement cell, LatticeElement left, LatticeElement right, String binOp) { - boolean changed = false; - if (left.kind == V_CONSTANT && right.kind == V_CONSTANT) { - long leftValue = left.value; - long rightValue = right.value; - long result; - switch (binOp) { - case "==": - result = leftValue == rightValue ? 1 : 0; - break; - case "!=": - result = leftValue != rightValue ? 1 : 0; - break; - case "<": - result = leftValue < rightValue ? 1 : 0; - break; - case ">": - result = leftValue > rightValue ? 1 : 0; - break; - case "<=": - result = leftValue <= rightValue ? 1 : 0; - break; - case ">=": - result = leftValue >= rightValue ? 1 : 0; - break; - default: - throw new IllegalStateException(); - } - changed = cell.meet(result); - } else if (left.kind == V_VARYING || right.kind == V_VARYING) { - // We could constrain the result here to the set [0-1] - // but we don't track ranges or sets of values - changed = cell.setKind(V_VARYING); - } - return changed; + private LatticeElement factFromOperandOrRegister(Operand operand) { + if (operand instanceof Operand.RegisterOperand registerOperand) + return valueLattice.get(registerOperand.reg); + if (isConstantOperand(operand)) + return factFromOperand(operand); + return null; } - private static boolean evalArith(LatticeElement cell, LatticeElement left, LatticeElement right, String binOp) { - boolean changed = false; - if (left.kind == V_CONSTANT && right.kind == V_CONSTANT) { - long leftValue = left.value; - long rightValue = right.value; - long result; - switch (binOp) { - case "+": - result = leftValue + rightValue; - break; - case "-": - result = leftValue - rightValue; - break; - case "/": - if (rightValue == 0) throw new CompilerException("Division by zero"); - result = leftValue / rightValue; - break; - case "*": - result = leftValue * rightValue; - break; - case "%": - result = leftValue % rightValue; - break; - default: - throw new IllegalStateException(); - } - changed = cell.meet(result); - } else if (binOp.equals("*") && ((left.kind == V_CONSTANT && left.value == 0) || (right.kind == V_CONSTANT && right.value == 0))) { - // multiplication with 0 yields 0 - changed = cell.meet(0); - } else if (left.kind == V_VARYING || right.kind == V_VARYING) { - changed = cell.setKind(V_VARYING); + private static boolean isConstantOperand(Operand operand) { + return operand instanceof Operand.IntConstantOperand || + operand instanceof Operand.FloatConstantOperand || + operand instanceof Operand.NullConstantOperand; + } + + private static LatticeElement factFromOperand(Operand operand) { + if (operand instanceof Operand.IntConstantOperand constantOperand) + return new LatticeElement(constantOperand.value); + if (operand instanceof Operand.FloatConstantOperand constantOperand) + return new LatticeElement(constantOperand.value); + if (operand instanceof Operand.NullConstantOperand) + return new LatticeElement(F_REF_NULL); + throw new IllegalStateException("Unexpected constant operand: " + operand); + } + + private static LatticeElement factTopFromType(EZType type) { + if (type instanceof EZType.EZTypeInteger) + return new LatticeElement(F_INT_TOP); + if (type instanceof EZType.EZTypeFloat) + return new LatticeElement(F_FLT_TOP); + if (isReferenceType(type)) + return new LatticeElement(F_REF_TOP); + return new LatticeElement(F_TOP); + } + + private static LatticeElement factBottomFromType(EZType type) { + if (type instanceof EZType.EZTypeInteger) + return new LatticeElement(F_INT_BOTTOM); + if (type instanceof EZType.EZTypeFloat) + return new LatticeElement(F_FLT_BOTTOM); + if (type instanceof EZType.EZTypeNull) + return new LatticeElement(F_REF_NULL); + if (isReferenceType(type)) + return new LatticeElement(F_REF_BOTTOM); + return new LatticeElement(F_BOTTOM); + } + + private static boolean isReferenceType(EZType type) { + return type instanceof EZType.EZTypeNullable || + type instanceof EZType.EZTypeArray || + type instanceof EZType.EZTypeStruct || + type instanceof EZType.EZTypeNull; + } + + private static EZType arrayLoadType(Instruction.ArrayLoad arrayLoadInst) { + EZType type = aggregateBaseType(operandType(arrayLoadInst.arrayOperand())); + if (type instanceof EZType.EZTypeArray arrayType) + return arrayType.getElementType(); + return arrayLoadInst.destOperand().reg.type; + } + + private static EZType fieldLoadType(Instruction.GetField getFieldInst) { + EZType type = aggregateBaseType(operandType(getFieldInst.structOperand())); + if (type instanceof EZType.EZTypeStruct structType) { + EZType fieldType = structType.getField(getFieldInst.fieldName); + if (fieldType != null) + return fieldType; } - return changed; + return getFieldInst.destOperand().reg.type; + } + + private static EZType aggregateBaseType(EZType type) { + if (type instanceof EZType.EZTypeNullable nullable) + return nullable.baseType; + return type; + } + + private static EZType operandType(Operand operand) { + if (operand instanceof Operand.RegisterOperand registerOperand) + return registerOperand.reg.type; + return operand.type; + } + + private static boolean evalUnary(LatticeElement cell, LatticeElement input, String unOp, EZType resultType) { + if (input.isIntegerConstant()) { + long value = input.kind == F_INT_ZERO ? 0 : input.intValue; + if (unOp.equals("-")) + return cell.meet(new LatticeElement(-value)); + if (unOp.equals("!")) + return cell.meet(new LatticeElement(value == 0 ? 1L : 0L)); + } + if (input.isFloatConstant() && unOp.equals("-")) + return cell.meet(new LatticeElement(-input.floatValue)); + if (input.kind == F_TOP || input.kind == F_INT_TOP || input.kind == F_FLT_TOP) + return false; + return cell.meetWithTypeBottom(resultType); } + private static boolean evalLogical(LatticeElement cell, LatticeElement left, LatticeElement right, String binOp, EZType resultType) { + if (left.isIntegerConstant() && right.isIntegerConstant()) { + long leftValue = left.kind == F_INT_ZERO ? 0 : left.intValue; + long rightValue = right.kind == F_INT_ZERO ? 0 : right.intValue; + long result = switch (binOp) { + case "==" -> leftValue == rightValue ? 1 : 0; + case "!=" -> leftValue != rightValue ? 1 : 0; + case "<" -> leftValue < rightValue ? 1 : 0; + case ">" -> leftValue > rightValue ? 1 : 0; + case "<=" -> leftValue <= rightValue ? 1 : 0; + case ">=" -> leftValue >= rightValue ? 1 : 0; + default -> throw new IllegalStateException(); + }; + return cell.meet(new LatticeElement(result)); + } + if (left.isFloatConstant() && right.isFloatConstant()) { + double leftValue = left.floatValue; + double rightValue = right.floatValue; + long result = switch (binOp) { + case "==" -> leftValue == rightValue ? 1 : 0; + case "!=" -> leftValue != rightValue ? 1 : 0; + case "<" -> leftValue < rightValue ? 1 : 0; + case ">" -> leftValue > rightValue ? 1 : 0; + case "<=" -> leftValue <= rightValue ? 1 : 0; + case ">=" -> leftValue >= rightValue ? 1 : 0; + default -> throw new IllegalStateException(); + }; + return cell.meet(new LatticeElement(result)); + } + // Reference equality/inequality where both operands are definite + // (null or not-null). Two distinct not-null pointers are undecidable, + // so only fold when at least one side is known null. + if (left.isDefiniteReference() && right.isDefiniteReference() && + (left.isNullConstant() || right.isNullConstant())) { + boolean equal = left.isNullConstant() && right.isNullConstant(); + long result = switch (binOp) { + case "==" -> equal ? 1 : 0; + case "!=" -> equal ? 0 : 1; + default -> throw new IllegalStateException(); + }; + return cell.meet(new LatticeElement(result)); + } + if (left.kind == F_TOP || right.kind == F_TOP || + left.kind == F_INT_TOP || right.kind == F_INT_TOP || + left.kind == F_FLT_TOP || right.kind == F_FLT_TOP || + left.kind == F_REF_TOP || right.kind == F_REF_TOP) + return false; + return cell.meetWithTypeBottom(resultType); + } + + private static boolean evalArith(LatticeElement cell, LatticeElement left, LatticeElement right, String binOp, EZType resultType) { + if (left.isIntegerConstant() && right.isIntegerConstant()) { + long leftValue = left.kind == F_INT_ZERO ? 0 : left.intValue; + long rightValue = right.kind == F_INT_ZERO ? 0 : right.intValue; + long result = switch (binOp) { + case "+" -> leftValue + rightValue; + case "-" -> leftValue - rightValue; + case "/" -> { + if (rightValue == 0) throw new CompilerException("Division by zero"); + yield leftValue / rightValue; + } + case "*" -> leftValue * rightValue; + case "%" -> { + if (rightValue == 0) throw new CompilerException("Division by zero"); + yield leftValue % rightValue; + } + default -> throw new IllegalStateException(); + }; + return cell.meet(new LatticeElement(result)); + } + if (left.isFloatConstant() && right.isFloatConstant()) { + double leftValue = left.floatValue; + double rightValue = right.floatValue; + double result = switch (binOp) { + case "+" -> leftValue + rightValue; + case "-" -> leftValue - rightValue; + case "/" -> leftValue / rightValue; + case "*" -> leftValue * rightValue; + default -> throw new IllegalStateException(); + }; + return cell.meet(new LatticeElement(result)); + } + if (binOp.equals("*") && + ((left.isIntegerConstant() && left.kind == F_INT_ZERO) || + (right.isIntegerConstant() && right.kind == F_INT_ZERO))) { + return cell.meet(new LatticeElement(0L)); + } + if (left.kind == F_TOP || right.kind == F_TOP || + left.kind == F_INT_TOP || right.kind == F_INT_TOP || + left.kind == F_FLT_TOP || right.kind == F_FLT_TOP) + return false; + return cell.meetWithTypeBottom(resultType); + } @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -583,8 +751,7 @@ static final class ValueLattice { LatticeElement get(Register reg) { var cell = valueLattice.get(reg); if (cell == null) { - // Initial value is UNDEFINED/TOP - cell = new LatticeElement(V_UNDEFINED, 0); + cell = factTopFromType(reg.type); valueLattice.put(reg, cell); } return cell; diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java b/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java index 31912dc..a32834b 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java @@ -45,9 +45,12 @@ public Value interpret(ExecutionStack execStack, Frame frame) { instruction = currentBlock.instructions.get(ip); switch (instruction) { case Instruction.Ret retInst -> { - if (retInst.value() instanceof Operand.ConstantOperand constantOperand) { + if (retInst.value() instanceof Operand.IntConstantOperand constantOperand) { execStack.stack[base] = new Value.IntegerValue(constantOperand.value); } + else if (retInst.value() instanceof Operand.FloatConstantOperand constantOperand) { + execStack.stack[base] = new Value.FloatValue(constantOperand.value); + } else if (retInst.value() instanceof Operand.NullConstantOperand) { execStack.stack[base] = new Value.NullValue(); } @@ -63,9 +66,12 @@ else if (retInst.value() instanceof Operand.RegisterOperand registerOperand) { if (moveInst.from() instanceof Operand.RegisterOperand fromReg) { execStack.stack[base + toReg.frameSlot()] = execStack.stack[base + fromReg.frameSlot()]; } - else if (moveInst.from() instanceof Operand.ConstantOperand constantOperand) { + else if (moveInst.from() instanceof Operand.IntConstantOperand constantOperand) { execStack.stack[base + toReg.frameSlot()] = new Value.IntegerValue(constantOperand.value); } + else if (moveInst.from() instanceof Operand.FloatConstantOperand constantOperand) { + execStack.stack[base + toReg.frameSlot()] = new Value.FloatValue(constantOperand.value); + } else if (moveInst.from() instanceof Operand.NullConstantOperand) { execStack.stack[base + toReg.frameSlot()] = new Value.NullValue(); } @@ -87,12 +93,15 @@ else if (moveInst.from() instanceof Operand.NullConstantOperand) { condition = integerValue.value != 0; } else { - condition = value != null; + throw new InterpreterException("Condition expression must be Int type"); } } - else if (cbrInst.condition() instanceof Operand.ConstantOperand constantOperand) { + else if (cbrInst.condition() instanceof Operand.IntConstantOperand constantOperand) { condition = constantOperand.value != 0; } + else if (cbrInst.condition() instanceof Operand.FloatConstantOperand) { + throw new InterpreterException("Condition expression must be Int type"); + } else throw new IllegalStateException(); if (condition) currentBlock = cbrInst.trueBlock; @@ -110,9 +119,12 @@ else if (cbrInst.condition() instanceof Operand.ConstantOperand constantOperand) if (arg instanceof Operand.RegisterOperand param) { execStack.stack[reg] = execStack.stack[base + param.frameSlot()]; } - else if (arg instanceof Operand.ConstantOperand constantOperand) { + else if (arg instanceof Operand.IntConstantOperand constantOperand) { execStack.stack[reg] = new Value.IntegerValue(constantOperand.value); } + else if (arg instanceof Operand.FloatConstantOperand constantOperand) { + execStack.stack[reg] = new Value.FloatValue(constantOperand.value); + } else if (arg instanceof Operand.NullConstantOperand) { execStack.stack[reg] = new Value.NullValue(); } @@ -138,80 +150,79 @@ else if (arg instanceof Operand.NullConstantOperand) { default: throw new CompilerException("Invalid unary op"); } } + else if (unaryValue instanceof Value.FloatValue floatValue && unaryInst.unop.equals("-")) { + execStack.stack[base + unaryInst.result().frameSlot()] = new Value.FloatValue(-floatValue.value); + } else throw new IllegalStateException("Unexpected unary operand: " + unaryOperand); } case Instruction.Binary binaryInst -> { - long x, y; long value = 0; - boolean intOp = true; - if (binaryInst.binOp.equals("==") || binaryInst.binOp.equals("!=")) { - Operand.RegisterOperand nonNullLitOperand = null; - if (binaryInst.left() instanceof Operand.NullConstantOperand) { - nonNullLitOperand = (Operand.RegisterOperand)binaryInst.right(); - } - else if (binaryInst.right() instanceof Operand.NullConstantOperand) { - nonNullLitOperand = (Operand.RegisterOperand)binaryInst.left(); + Value leftValue = valueFromOperand(binaryInst.left(), execStack, base); + Value rightValue = valueFromOperand(binaryInst.right(), execStack, base); + if ((leftValue instanceof Value.NullValue || rightValue instanceof Value.NullValue) && + (binaryInst.binOp.equals("==") || binaryInst.binOp.equals("!="))) { + boolean equal = leftValue instanceof Value.NullValue && rightValue instanceof Value.NullValue; + value = switch (binaryInst.binOp) { + case "==" -> equal ? 1 : 0; + case "!=" -> equal ? 0 : 1; + default -> throw new IllegalStateException(); + }; + execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); + } + else { + if (leftValue instanceof Value.FloatValue left && rightValue instanceof Value.FloatValue right) { + double x = left.value; + double y = right.value; + switch (binaryInst.binOp) { + case "+" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x + y); + case "-" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x - y); + case "*" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x * y); + case "/" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x / y); + case "==" -> value = x == y ? 1 : 0; + case "!=" -> value = x != y ? 1 : 0; + case "<" -> value = x < y ? 1 : 0; + case ">" -> value = x > y ? 1 : 0; + case "<=" -> value = x <= y ? 1 : 0; + case ">=" -> value = x >= y ? 1 : 0; + default -> throw new IllegalStateException(); + } + if (binaryInst.binOp.equals("==") || binaryInst.binOp.equals("!=") || binaryInst.binOp.equals("<") || binaryInst.binOp.equals(">") || binaryInst.binOp.equals("<=") || binaryInst.binOp.equals(">=")) + execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); } - if (nonNullLitOperand != null) { - intOp = false; - Value otherValue = execStack.stack[base + nonNullLitOperand.frameSlot()]; + else if (leftValue instanceof Value.IntegerValue left && rightValue instanceof Value.IntegerValue right) { + long x = left.value; + long y = right.value; switch (binaryInst.binOp) { - case "==": { - value = otherValue instanceof Value.NullValue ? 1 : 0; - break; - } - case "!=": { - value = otherValue instanceof Value.NullValue ? 0 : 1; - break; - } - default: - throw new IllegalStateException(); + case "+": value = x + y; break; + case "-": value = x - y; break; + case "*": value = x * y; break; + case "/": value = x / y; break; + case "%": value = x % y; break; + case "==": value = x == y ? 1 : 0; break; + case "!=": value = x != y ? 1 : 0; break; + case "<": value = x < y ? 1: 0; break; + case ">": value = x > y ? 1 : 0; break; + case "<=": value = x <= y ? 1 : 0; break; + case ">=": value = x >= y ? 1 : 0; break; + default: throw new IllegalStateException(); } execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); } - } - if (intOp) { - if (binaryInst.left() instanceof Operand.ConstantOperand constant) - x = constant.value; - else if (binaryInst.left() instanceof Operand.RegisterOperand registerOperand) - x = ((Value.IntegerValue) execStack.stack[base + registerOperand.frameSlot()]).value; - else throw new IllegalStateException(); - if (binaryInst.right() instanceof Operand.ConstantOperand constant) - y = constant.value; - else if (binaryInst.right() instanceof Operand.RegisterOperand registerOperand) - y = ((Value.IntegerValue) execStack.stack[base + registerOperand.frameSlot()]).value; else throw new IllegalStateException(); - switch (binaryInst.binOp) { - case "+": value = x + y; break; - case "-": value = x - y; break; - case "*": value = x * y; break; - case "/": value = x / y; break; - case "%": value = x % y; break; - case "==": value = x == y ? 1 : 0; break; - case "!=": value = x != y ? 1 : 0; break; - case "<": value = x < y ? 1: 0; break; - case ">": value = x > y ? 1 : 0; break; - case "<=": value = x <= y ? 1 : 0; break; - case ">=": value = x >= y ? 1 : 0; break; - default: throw new IllegalStateException(); - } - execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); } } case Instruction.NewArray newArrayInst -> { long size = 0; Value initValue = null; - if (newArrayInst.len() instanceof Operand.ConstantOperand constantOperand) + if (newArrayInst.len() instanceof Operand.IntConstantOperand constantOperand) size = constantOperand.value; else if (newArrayInst.len() instanceof Operand.RegisterOperand registerOperand) { Value.IntegerValue indexValue = (Value.IntegerValue) execStack.stack[base + registerOperand.frameSlot()]; size = (long) indexValue.value; } - if (newArrayInst.initValue() instanceof Operand.ConstantOperand constantOperand) - initValue = new Value.IntegerValue(constantOperand.value); - else if (newArrayInst.initValue() instanceof Operand.RegisterOperand registerOperand) - initValue = execStack.stack[base + registerOperand.frameSlot()]; + if (newArrayInst.initValue() != null) + initValue = valueFromOperand(newArrayInst.initValue(), execStack, base); execStack.stack[base + newArrayInst.destOperand().frameSlot()] = new Value.ArrayValue(newArrayInst.type, size, initValue); } case Instruction.NewStruct newStructInst -> { @@ -221,7 +232,7 @@ else if (newArrayInst.initValue() instanceof Operand.RegisterOperand registerOpe if (arrayStoreInst.arrayOperand() instanceof Operand.RegisterOperand arrayOperand) { Value.ArrayValue arrayValue = (Value.ArrayValue) execStack.stack[base + arrayOperand.frameSlot()]; int index = 0; - if (arrayStoreInst.indexOperand() instanceof Operand.ConstantOperand constant) { + if (arrayStoreInst.indexOperand() instanceof Operand.IntConstantOperand constant) { index = (int) constant.value; } else if (arrayStoreInst.indexOperand() instanceof Operand.RegisterOperand registerOperand) { @@ -229,17 +240,7 @@ else if (arrayStoreInst.indexOperand() instanceof Operand.RegisterOperand regist index = (int) indexValue.value; } else throw new IllegalStateException(); - Value value; - if (arrayStoreInst.sourceOperand() instanceof Operand.ConstantOperand constantOperand) { - value = new Value.IntegerValue(constantOperand.value); - } - else if (arrayStoreInst.sourceOperand() instanceof Operand.NullConstantOperand) { - value = new Value.NullValue(); - } - else if (arrayStoreInst.sourceOperand() instanceof Operand.RegisterOperand registerOperand) { - value = execStack.stack[base + registerOperand.frameSlot()]; - } - else throw new IllegalStateException(); + Value value = valueFromOperand(arrayStoreInst.sourceOperand(), execStack, base); if (index == arrayValue.values.size()) arrayValue.values.add(value); else @@ -249,7 +250,7 @@ else if (arrayStoreInst.sourceOperand() instanceof Operand.RegisterOperand regis case Instruction.ArrayLoad arrayLoadInst -> { if (arrayLoadInst.arrayOperand() instanceof Operand.RegisterOperand arrayOperand) { Value.ArrayValue arrayValue = (Value.ArrayValue) execStack.stack[base + arrayOperand.frameSlot()]; - if (arrayLoadInst.indexOperand() instanceof Operand.ConstantOperand constant) { + if (arrayLoadInst.indexOperand() instanceof Operand.IntConstantOperand constant) { execStack.stack[base + arrayLoadInst.destOperand().frameSlot()] = arrayValue.values.get((int) constant.value); } else if (arrayLoadInst.indexOperand() instanceof Operand.RegisterOperand registerOperand) { @@ -263,17 +264,7 @@ else if (arrayLoadInst.indexOperand() instanceof Operand.RegisterOperand registe if (setFieldInst.structOperand() instanceof Operand.RegisterOperand structOperand) { Value.StructValue structValue = (Value.StructValue) execStack.stack[base + structOperand.frameSlot()]; int index = setFieldInst.fieldIndex; - Value value; - if (setFieldInst.sourceOperand() instanceof Operand.ConstantOperand constant) { - value = new Value.IntegerValue(constant.value); - } - else if (setFieldInst.sourceOperand() instanceof Operand.NullConstantOperand) { - value = new Value.NullValue(); - } - else if (setFieldInst.sourceOperand() instanceof Operand.RegisterOperand registerOperand) { - value = execStack.stack[base + registerOperand.frameSlot()]; - } - else throw new IllegalStateException(); + Value value = valueFromOperand(setFieldInst.sourceOperand(), execStack, base); structValue.fields[index] = value; } else throw new IllegalStateException(); } @@ -291,6 +282,18 @@ else if (setFieldInst.sourceOperand() instanceof Operand.RegisterOperand registe return returnValue; } + + private Value valueFromOperand(Operand operand, ExecutionStack execStack, int base) { + if (operand instanceof Operand.IntConstantOperand constant) + return new Value.IntegerValue(constant.value); + if (operand instanceof Operand.FloatConstantOperand constant) + return new Value.FloatValue(constant.value); + if (operand instanceof Operand.RegisterOperand registerOperand) + return execStack.stack[base + registerOperand.frameSlot()]; + if (operand instanceof Operand.NullConstantOperand) + return new Value.NullValue(); + throw new IllegalStateException(); + } static class Frame { Frame caller; int base; diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java b/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java index fc46c3b..07fb585 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java @@ -11,6 +11,12 @@ public IntegerValue(long value) { } public final long value; } + static public class FloatValue extends Value { + public FloatValue(double value) { + this.value = value; + } + public final double value; + } static public class NullValue extends Value { public NullValue() {} } diff --git a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestInterferenceGraph.java b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestInterferenceGraph.java index e20846f..0e20bff 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestInterferenceGraph.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestInterferenceGraph.java @@ -25,7 +25,7 @@ private CompiledFunction buildTest1() { "+", new Operand.RegisterOperand(a), new Operand.RegisterOperand(b), - new Operand.ConstantOperand(1, typeDictionary.INT))); + new Operand.IntConstantOperand(1, typeDictionary.INT))); function.code(new Instruction.Binary( "*", new Operand.RegisterOperand(c), @@ -35,7 +35,7 @@ private CompiledFunction buildTest1() { "+", new Operand.RegisterOperand(b), new Operand.RegisterOperand(c), - new Operand.ConstantOperand(1, typeDictionary.INT))); + new Operand.IntConstantOperand(1, typeDictionary.INT))); function.code(new Instruction.Binary( "*", new Operand.RegisterOperand(d), @@ -95,7 +95,7 @@ private CompiledFunction buildTest2() { BasicBlock b3 = function.createBlock(); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(a))); function.code(new Instruction.ConditionalBranch( function.currentBlock, @@ -105,7 +105,7 @@ private CompiledFunction buildTest2() { function.currentBlock.addSuccessor(b2); function.startBlock(b1); function.code(new Instruction.Move( - new Operand.ConstantOperand(2, typeDictionary.INT), + new Operand.IntConstantOperand(2, typeDictionary.INT), new Operand.RegisterOperand(b))); function.code(new Instruction.Move( new Operand.RegisterOperand(b), @@ -113,7 +113,7 @@ private CompiledFunction buildTest2() { function.jumpTo(b3); function.startBlock(b2); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(c))); function.code(new Instruction.Move( new Operand.RegisterOperand(c), @@ -169,10 +169,10 @@ public static CompiledFunction buildTest4() { Register t = regPool.newReg("t", typeDictionary.INT); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(a))); function.code(new Instruction.Move( - new Operand.ConstantOperand(2, typeDictionary.INT), + new Operand.IntConstantOperand(2, typeDictionary.INT), new Operand.RegisterOperand(b))); function.code(new Instruction.Move( new Operand.RegisterOperand(b), diff --git a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestLiveness.java b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestLiveness.java index 2fa0e6b..e02e74c 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestLiveness.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestLiveness.java @@ -254,7 +254,7 @@ static CompiledFunction buildTest3() { Register i = regPool.newReg("i", typeDictionary.INT); Register s = regPool.newReg("s", typeDictionary.INT); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(i))); BasicBlock b1 = function.createBlock(); BasicBlock b2 = function.createBlock(); @@ -270,7 +270,7 @@ static CompiledFunction buildTest3() { function.currentBlock.addSuccessor(b3); function.startBlock(b2); function.code(new Instruction.Move( - new Operand.ConstantOperand(0, typeDictionary.INT), + new Operand.IntConstantOperand(0, typeDictionary.INT), new Operand.RegisterOperand(s))); function.jumpTo(b3); function.startBlock(b3); @@ -283,7 +283,7 @@ static CompiledFunction buildTest3() { "+", new Operand.RegisterOperand(i), new Operand.RegisterOperand(i), - new Operand.ConstantOperand(1, typeDictionary.INT))); + new Operand.IntConstantOperand(1, typeDictionary.INT))); function.code(new Instruction.ConditionalBranch( function.currentBlock, new Operand.RegisterOperand(i), @@ -587,13 +587,13 @@ static CompiledFunction buildTestReddit() { Register v3_0 = regPool.newReg("v3_0", typeDictionary.INT); Register v4_0 = regPool.newReg("v4_0", typeDictionary.INT); function.code(new Instruction.Move( - new Operand.ConstantOperand(47, typeDictionary.INT), + new Operand.IntConstantOperand(47, typeDictionary.INT), new Operand.RegisterOperand(v0_0))); function.code(new Instruction.Move( - new Operand.ConstantOperand(42, typeDictionary.INT), + new Operand.IntConstantOperand(42, typeDictionary.INT), new Operand.RegisterOperand(v1_0))); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(v3_0))); BasicBlock b1 = function.createBlock(); BasicBlock b2 = function.createBlock(); @@ -607,19 +607,19 @@ static CompiledFunction buildTestReddit() { function.currentBlock.addSuccessor(b2); function.startBlock(b1); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(v1_1))); function.code(new Instruction.Move( - new Operand.ConstantOperand(5, typeDictionary.INT), + new Operand.IntConstantOperand(5, typeDictionary.INT), new Operand.RegisterOperand(v2_0))); function.jumpTo(b3); function.startBlock(b2); function.code(new Instruction.Move( - new Operand.ConstantOperand(2, typeDictionary.INT), + new Operand.IntConstantOperand(2, typeDictionary.INT), new Operand.RegisterOperand(v0_2))); function.code(new Instruction.Move( - new Operand.ConstantOperand(10, typeDictionary.INT), + new Operand.IntConstantOperand(10, typeDictionary.INT), new Operand.RegisterOperand(v2_2))); function.jumpTo(b3); diff --git a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSCCP.java b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSCCP.java index bd25216..3373852 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSCCP.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSCCP.java @@ -150,16 +150,16 @@ func foo()->Int { L6->L7=NOT Executable Lattices: j_3=1 -%t5_0=varying -k_3=varying -k_4=varying +%t5_0=int* +k_3=int* +k_4=int* j_4=1 i_0=1 j_0=1 k_0=0 -k_1=varying +k_1=int* j_1=1 -%t3_0=varying +%t3_0=int* %t4_0=1 After SCCP changes: L0: @@ -184,4 +184,84 @@ func foo()->Int { """; Assert.assertEquals(expected, actual); } + + // Exercises float constant propagation: a and b are variables (not + // front-end folded), so SCCP must propagate the float constants and + // fold a+b to 3.75, replacing the return operand. + @Test + public void testFloatConstantPropagation() { + String src = """ +func foo()->Float { + var a = 1.25 + var b = 2.5 + return a + b +} +"""; + String actual = compileSrc(src); + Assert.assertTrue("expected folded float return in:\n" + actual, + actual.contains("ret 3.75")); + } + + // Exercises the ref lattice's null-constant comparison folding + // (evalLogical: null == null -> 1). The comparison is not folded by the + // front end, so it reaches SCCP as a Binary over two null operands. + @Test + public void testNullConstantComparisonFolding() { + String src = """ +func foo()->Int { + return null == null +} +"""; + String actual = compileSrc(src); + Assert.assertTrue("expected folded null comparison in:\n" + actual, + actual.contains("ret 1")); + } + + // Exercises the ref lattice tracking a freshly allocated struct as + // not-null (NewStruct -> F_REF_NOT_NULL), which prints as "not-null" + // in the lattice dump. + @Test + public void testNewStructIsNotNull() { + String src = """ +struct Foo +{ + var i: Int +} +func foo()->Int +{ + var f = new Foo{ i = 1 } + return f.i +} +"""; + String actual = compileSrc(src); + Assert.assertTrue("expected a not-null lattice entry in:\n" + actual, + actual.contains("not-null")); + } + + // Exercises not-null vs null comparison folding in evalLogical: a freshly + // allocated (not-null) struct compared to null folds the condition to + // false, so the dead 'return 0' branch is eliminated after SCCP. + @Test + public void testNotNullComparedToNullFolds() { + String src = """ +struct Foo +{ + var i: Int +} +func foo()->Int +{ + var f: Foo? + f = new Foo{ i = 1 } + if (f == null) + return 0 + return 1 +} +"""; + String actual = compileSrc(src); + String postSccp = actual.substring(actual.indexOf("After SCCP changes:")); + Assert.assertFalse("dead 'return 0' branch should be folded away:\n" + actual, + postSccp.contains("ret 0")); + Assert.assertTrue("reachable 'return 1' should remain:\n" + actual, + postSccp.contains("ret 1")); + } } diff --git a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSADestructB.java b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSADestructB.java index 38e4eaf..4bc01c2 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSADestructB.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSADestructB.java @@ -1040,7 +1040,7 @@ static CompiledFunction buildLostCopyTest() { Register x1 = regPool.newReg("x1", typeDictionary.INT); function.code(new Instruction.ArgInstruction(new Operand.LocalRegisterOperand(p, argSymbol))); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(x1))); BasicBlock B2 = function.createBlock(); function.startBlock(B2); @@ -1050,7 +1050,7 @@ static CompiledFunction buildLostCopyTest() { function.code(new Instruction.Binary("+", new Operand.RegisterOperand(x3), new Operand.RegisterOperand(x2), - new Operand.ConstantOperand(1, typeDictionary.INT))); + new Operand.IntConstantOperand(1, typeDictionary.INT))); function.code(new Instruction.ConditionalBranch(B2, new Operand.RegisterOperand(p), B2, function.exit)); function.currentBlock.addSuccessor(B2); @@ -1149,10 +1149,10 @@ static CompiledFunction buildSwapTest() { Register b2 = regPool.newReg("b2", typeDictionary.INT); function.code(new Instruction.ArgInstruction(new Operand.LocalRegisterOperand(p, argSymbol))); function.code(new Instruction.Move( - new Operand.ConstantOperand(42, typeDictionary.INT), + new Operand.IntConstantOperand(42, typeDictionary.INT), new Operand.RegisterOperand(a1))); function.code(new Instruction.Move( - new Operand.ConstantOperand(24, typeDictionary.INT), + new Operand.IntConstantOperand(24, typeDictionary.INT), new Operand.RegisterOperand(b1))); BasicBlock B2 = function.createBlock(); function.startBlock(B2); diff --git a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSATransform.java b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSATransform.java index 65fb48d..07e9af8 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSATransform.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSATransform.java @@ -637,7 +637,7 @@ static CompiledFunction buildLostCopyTest() { Register x1 = regPool.newReg("x1", typeDictionary.INT); function.code(new Instruction.ArgInstruction(new Operand.LocalRegisterOperand(p, argSymbol))); function.code(new Instruction.Move( - new Operand.ConstantOperand(1, typeDictionary.INT), + new Operand.IntConstantOperand(1, typeDictionary.INT), new Operand.RegisterOperand(x1))); BasicBlock B2 = function.createBlock(); function.startBlock(B2); @@ -647,7 +647,7 @@ static CompiledFunction buildLostCopyTest() { function.code(new Instruction.Binary("+", new Operand.RegisterOperand(x3), new Operand.RegisterOperand(x2), - new Operand.ConstantOperand(1, typeDictionary.INT))); + new Operand.IntConstantOperand(1, typeDictionary.INT))); function.code(new Instruction.ConditionalBranch(B2, new Operand.RegisterOperand(p), B2, function.exit)); function.currentBlock.addSuccessor(B2); @@ -711,10 +711,10 @@ static CompiledFunction buildSwapTest() { Register b2 = regPool.newReg("b2", typeDictionary.INT); function.code(new Instruction.ArgInstruction(new Operand.LocalRegisterOperand(p, argSymbol))); function.code(new Instruction.Move( - new Operand.ConstantOperand(42, typeDictionary.INT), + new Operand.IntConstantOperand(42, typeDictionary.INT), new Operand.RegisterOperand(a1))); function.code(new Instruction.Move( - new Operand.ConstantOperand(24, typeDictionary.INT), + new Operand.IntConstantOperand(24, typeDictionary.INT), new Operand.RegisterOperand(b1))); BasicBlock B2 = function.createBlock(); function.startBlock(B2); diff --git a/optvm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java b/optvm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java index c7c0fb5..26d3174 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java @@ -1,6 +1,11 @@ package com.compilerprogramming.ezlang.interpreter; import com.compilerprogramming.ezlang.compiler.Compiler; +import com.compilerprogramming.ezlang.compiler.CompiledFunction; +import com.compilerprogramming.ezlang.compiler.Instruction; +import com.compilerprogramming.ezlang.compiler.Operand; +import com.compilerprogramming.ezlang.exceptions.InterpreterException; +import com.compilerprogramming.ezlang.types.Symbol; import com.compilerprogramming.ezlang.compiler.Options; import org.junit.Assert; import org.junit.Test; @@ -853,6 +858,172 @@ func main()->Int quicksort(nums, 0, 5); return eq(nums,expected,6) } +"""; + var value = compileAndRun(src, "main"); + Assert.assertNotNull(value); + Assert.assertTrue(value instanceof Value.IntegerValue integerValue && + integerValue.value == 1); + } + + @Test(expected = InterpreterException.class) + public void testFloatConstantBranchConditionRejectedByInterpreter() { + String src = """ + func foo()->Int { + if (1) + return 1; + return 0; + } + """; + var compiler = new Compiler(); + var typeDict = compiler.compileSrc(src, Options.NONE); + var functionSymbol = (Symbol.FunctionTypeSymbol) typeDict.lookup("foo"); + var function = (CompiledFunction) functionSymbol.code(); + for (int i = 0; i < function.entry.instructions.size(); i++) { + if (function.entry.instructions.get(i) instanceof Instruction.ConditionalBranch cbr) { + function.entry.instructions.set(i, new Instruction.ConditionalBranch( + function.entry, + new Operand.FloatConstantOperand(1.0, typeDict.FLOAT), + cbr.trueBlock, + cbr.falseBlock)); + break; + } + } + new Interpreter(typeDict).run("foo"); + } + @Test + public void testFloatArithmetic() { + String src = """ + func add(a: Float, b: Float)->Float { + return a+b; + } + func foo()->Float { + return add(1.25,2.5); + } + """; + var value = compileAndRun(src, "foo"); + Assert.assertNotNull(value); + Assert.assertTrue(value instanceof Value.FloatValue floatValue + && Math.abs(floatValue.value - 3.75) < 0.000001); + } + + @Test + public void testFloatArray() { + String src = """ + func foo()->Float { + var t = new [Float] {1.0,2.25,3.5}; + t[1] = t[1] + 1.25; + return t[1]; + } + """; + var value = compileAndRun(src, "foo"); + Assert.assertNotNull(value); + Assert.assertTrue(value instanceof Value.FloatValue floatValue + && Math.abs(floatValue.value - 3.5) < 0.000001); + } + + // A float value flows through an if/else phi, so SSA destruction (the + // Briggs path under the OPT presets) must copy a float phi operand + // correctly. choose(0)=2.5, choose(1)=1.5 -> 4.0. + @Test + public void testFloatPhiThroughSSADestruction() { + String src = """ + func choose(c: Int)->Float { + var x = 1.5; + if (c == 0) + x = 2.5; + return x; + } + func foo()->Float { + return choose(0) + choose(1); + } + """; + var value = compileAndRun(src, "foo"); + Assert.assertNotNull(value); + Assert.assertTrue(value instanceof Value.FloatValue floatValue + && Math.abs(floatValue.value - 4.0) < 0.000001); + } + + // A null value flows through an if/else phi, so SSA destruction must copy + // a null phi operand correctly. choose(0)=null, choose(1)=not-null. + @Test + public void testNullPhiThroughSSADestruction() { + String src = """ + struct Foo + { + var i: Int + } + func choose(c: Int)->Foo? { + var x: Foo? + x = new Foo{ i = 7 } + if (c == 0) + x = null + return x + } + func foo()->Int { + return choose(0) == null && choose(1) != null + } + """; + var value = compileAndRun(src, "foo"); + Assert.assertNotNull(value); + Assert.assertTrue(value instanceof Value.IntegerValue integerValue + && integerValue.value == 1); + } + + @Test + public void testFunction111() { + String src = """ +func swap(arr: [Float], i: Int, j: Int) { + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +func partition(arr: [Float], low: Int, high: Int)->Int { + var pivot = arr[high]; + var i = low; + var j = low; + while (j < high) { + if (arr[j] < pivot) { + swap(arr, i, j); + i = i + 1; + } + j = j + 1; + } + swap(arr, i, high); + return i; +} + +func quicksort(arr: [Float], low: Int, high: Int) { + if (low < high) { + var p = partition(arr, low, high); + quicksort(arr, low, p - 1); + quicksort(arr, p + 1, high); + } +} + +func eq(a: [Int], b: [Int], n: Int)->Int +{ + var result = 1 + var i = 0 + while (i < n) + { + if (a[i] != b[i]) + { + result = 0 + break + } + i = i + 1 + } + return result +} + +func main()->Int +{ + var nums = new [Float]{33.4, 10.2, 55.1, 71.9, 29.9, 3.5}; + var expected = new [Float]{3.5,10.2,29.9,33.4,55.1,71.9} + quicksort(nums, 0, 5); + return eq(nums,expected,6) +} """; var value = compileAndRun(src, "main"); Assert.assertNotNull(value); diff --git a/parser/src/main/java/com/compilerprogramming/ezlang/parser/Parser.java b/parser/src/main/java/com/compilerprogramming/ezlang/parser/Parser.java index e4b1cc7..d185a4a 100644 --- a/parser/src/main/java/com/compilerprogramming/ezlang/parser/Parser.java +++ b/parser/src/main/java/com/compilerprogramming/ezlang/parser/Parser.java @@ -411,7 +411,7 @@ else if (isToken(currentToken, "new")) { } } default -> { - error(currentToken, "syntax error, expected nested expr, integer value or variable"); + error(currentToken, "syntax error, expected nested expr, numeric value or variable"); return null; } } diff --git a/parser/src/test/java/com/compilerprogramming/ezlang/parser/TestParser.java b/parser/src/test/java/com/compilerprogramming/ezlang/parser/TestParser.java index b4966c3..3a9fac5 100644 --- a/parser/src/test/java/com/compilerprogramming/ezlang/parser/TestParser.java +++ b/parser/src/test/java/com/compilerprogramming/ezlang/parser/TestParser.java @@ -33,6 +33,13 @@ func bar() -> Test { var v = new Test { intArray = new [Tree] {} } return v } +func floatAdd(a: Float, b: Float)->Float { + return a+b +} +func floatArrayCreate()->[Float] { + var array = new [Float] {len=10,1.0,2.0,3.0} + return array +} func main() { var m = 42 var t: Tree diff --git a/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java b/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java index 19964ff..ce9f237 100644 --- a/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java +++ b/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java @@ -175,6 +175,7 @@ private void compileWhile(AST.WhileStmt whileStmt) { currentBreakTarget = exitBlock; currentContinueTarget = loopHead; startBlock(loopHead); + checkConditionType(whileStmt.condition.type, whileStmt.lineNumber); boolean indexed = compileExpr(whileStmt.condition); if (indexed) codeIndexedLoad(); @@ -189,6 +190,11 @@ private void compileWhile(AST.WhileStmt whileStmt) { currentBreakTarget = savedBreakTarget; } + private void checkConditionType(EZType conditionType, int lineNumber) { + if (!(conditionType instanceof EZType.EZTypeInteger)) + throw new CompilerException("Condition expression must be Int type", lineNumber); + } + private boolean isBlockTerminated(BasicBlock block) { return (block.instructions.size() > 0 && block.instructions.getLast().isTerminal()); @@ -212,6 +218,7 @@ private void compileIf(AST.IfElseStmt ifElseStmt) { boolean needElse = ifElseStmt.elseStmt != null; BasicBlock elseBlock = needElse ? createBlock() : null; BasicBlock exitBlock = createBlock(); + checkConditionType(ifElseStmt.condition.type, ifElseStmt.lineNumber); boolean indexed = compileExpr(ifElseStmt.condition); if (indexed) codeIndexedLoad(); @@ -408,6 +415,8 @@ private boolean compileSymbolExpr(AST.NameExpr symbolExpr) { } private boolean codeBoolean(AST.BinaryExpr binaryExpr) { + checkConditionType(binaryExpr.expr1.type, binaryExpr.lineNumber); + checkConditionType(binaryExpr.expr2.type, binaryExpr.lineNumber); boolean isAnd = binaryExpr.op.str.equals("&&"); BasicBlock l1 = createBlock(); BasicBlock l2 = createBlock(); @@ -426,7 +435,7 @@ private boolean codeBoolean(AST.BinaryExpr binaryExpr) { jumpTo(l3); startBlock(l2); // Below we must write to the same temp - code(new Instruction.Move(new Operand.ConstantOperand(isAnd ? 0 : 1, typeDictionary.INT), temp)); + code(new Instruction.Move(new Operand.IntConstantOperand(isAnd ? 0 : 1, typeDictionary.INT), temp)); jumpTo(l3); startBlock(l3); // leave temp on virtual stack @@ -455,10 +464,10 @@ private boolean compileBinaryExpr(AST.BinaryExpr binaryExpr) { case "!=": value = 0; break; default: throw new CompilerException("Invalid binary op", binaryExpr.lineNumber); } - pushConstant(value, typeDictionary.INT); + pushIntConstant(value, typeDictionary.INT); } - else if (left instanceof Operand.ConstantOperand leftconstant && - right instanceof Operand.ConstantOperand rightconstant) { + else if (left instanceof Operand.IntConstantOperand leftconstant && + right instanceof Operand.IntConstantOperand rightconstant) { long value = 0; switch (opCode) { case "+": value = leftconstant.value + rightconstant.value; break; @@ -471,10 +480,26 @@ else if (left instanceof Operand.ConstantOperand leftconstant && case "<": value = leftconstant.value < rightconstant.value ? 1: 0; break; case ">": value = leftconstant.value > rightconstant.value ? 1 : 0; break; case "<=": value = leftconstant.value <= rightconstant.value ? 1 : 0; break; - case ">=": value = leftconstant.value <= rightconstant.value ? 1 : 0; break; + case ">=": value = leftconstant.value >= rightconstant.value ? 1 : 0; break; default: throw new CompilerException("Invalid binary op", binaryExpr.lineNumber); } - pushConstant(value, leftconstant.type); + pushIntConstant(value, binaryExpr.type); + } + else if (left instanceof Operand.FloatConstantOperand leftconstant && + right instanceof Operand.FloatConstantOperand rightconstant) { + switch (opCode) { + case "+" -> pushFloatConstant(leftconstant.value + rightconstant.value, binaryExpr.type); + case "-" -> pushFloatConstant(leftconstant.value - rightconstant.value, binaryExpr.type); + case "*" -> pushFloatConstant(leftconstant.value * rightconstant.value, binaryExpr.type); + case "/" -> pushFloatConstant(leftconstant.value / rightconstant.value, binaryExpr.type); + case "==" -> pushIntConstant(leftconstant.value == rightconstant.value ? 1 : 0, binaryExpr.type); + case "!=" -> pushIntConstant(leftconstant.value != rightconstant.value ? 1 : 0, binaryExpr.type); + case "<" -> pushIntConstant(leftconstant.value < rightconstant.value ? 1 : 0, binaryExpr.type); + case ">" -> pushIntConstant(leftconstant.value > rightconstant.value ? 1 : 0, binaryExpr.type); + case "<=" -> pushIntConstant(leftconstant.value <= rightconstant.value ? 1 : 0, binaryExpr.type); + case ">=" -> pushIntConstant(leftconstant.value >= rightconstant.value ? 1 : 0, binaryExpr.type); + default -> throw new CompilerException("Invalid binary op", binaryExpr.lineNumber); + } } else { var temp = createTemp(binaryExpr.type); @@ -490,11 +515,17 @@ private boolean compileUnaryExpr(AST.UnaryExpr unaryExpr) { codeIndexedLoad(); opCode = unaryExpr.op.str; Operand top = pop(); - if (top instanceof Operand.ConstantOperand constant) { + if (top instanceof Operand.IntConstantOperand constant) { switch (opCode) { - case "-": pushConstant(-constant.value, constant.type); break; + case "-": pushIntConstant(-constant.value, constant.type); break; // Maybe below we should explicitly set Int - case "!": pushConstant(constant.value == 0?1:0, constant.type); break; + case "!": pushIntConstant(constant.value == 0?1:0, typeDictionary.INT); break; + default: throw new CompilerException("Invalid unary op", unaryExpr.lineNumber); + } + } + else if (top instanceof Operand.FloatConstantOperand constant) { + switch (opCode) { + case "-": pushFloatConstant(-constant.value, constant.type); break; default: throw new CompilerException("Invalid unary op", unaryExpr.lineNumber); } } @@ -507,15 +538,21 @@ private boolean compileUnaryExpr(AST.UnaryExpr unaryExpr) { private boolean compileConstantExpr(AST.LiteralExpr constantExpr) { if (constantExpr.type instanceof EZType.EZTypeInteger) - pushConstant(constantExpr.value.num.intValue(), constantExpr.type); + pushIntConstant(constantExpr.value.num.intValue(), constantExpr.type); + else if (constantExpr.type instanceof EZType.EZTypeFloat) + pushFloatConstant(constantExpr.value.num.doubleValue(), constantExpr.type); else if (constantExpr.type instanceof EZType.EZTypeNull) pushNullConstant(constantExpr.type); else throw new CompilerException("Invalid constant type", constantExpr.lineNumber); return false; } - private void pushConstant(long value, EZType type) { - pushOperand(new Operand.ConstantOperand(value, type)); + private void pushIntConstant(long value, EZType type) { + pushOperand(new Operand.IntConstantOperand(value, type)); + } + + private void pushFloatConstant(double value, EZType type) { + pushOperand(new Operand.FloatConstantOperand(value, type)); } private void pushNullConstant(EZType type) { @@ -531,7 +568,9 @@ private Operand.TempRegisterOperand createTemp(EZType type) { } EZType typeOfOperand(Operand operand) { - if (operand instanceof Operand.ConstantOperand constant) + if (operand instanceof Operand.IntConstantOperand constant) + return constant.type; + else if (operand instanceof Operand.FloatConstantOperand constant) return constant.type; else if (operand instanceof Operand.NullConstantOperand nullConstantOperand) return nullConstantOperand.type; @@ -549,7 +588,8 @@ private Operand.TempRegisterOperand createTempAndMove(Operand src) { private Operand.RegisterOperand ensureTemp() { Operand top = top(); - if (top instanceof Operand.ConstantOperand + if (top instanceof Operand.IntConstantOperand + || top instanceof Operand.FloatConstantOperand || top instanceof Operand.NullConstantOperand || top instanceof Operand.LocalRegisterOperand) { return createTempAndMove(pop()); diff --git a/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java b/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java index d2315f3..40bcffe 100644 --- a/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java +++ b/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java @@ -6,9 +6,9 @@ public class Operand { EZType type; - public static class ConstantOperand extends Operand { + public static class IntConstantOperand extends Operand { public final long value; - public ConstantOperand(long value, EZType type) { + public IntConstantOperand(long value, EZType type) { this.value = value; this.type = type; } @@ -18,6 +18,17 @@ public String toString() { } } + public static class FloatConstantOperand extends Operand { + public final double value; + public FloatConstantOperand(double value, EZType type) { + this.value = value; + this.type = type; + } + @Override + public String toString() { + return String.valueOf(value); + } + } public static class NullConstantOperand extends Operand { public NullConstantOperand(EZType type) { this.type = type; diff --git a/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java b/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java index 617de1c..6faeace 100644 --- a/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java +++ b/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Interpreter.java @@ -4,7 +4,6 @@ import com.compilerprogramming.ezlang.compiler.CompiledFunction; import com.compilerprogramming.ezlang.compiler.Instruction; import com.compilerprogramming.ezlang.compiler.Operand; -import com.compilerprogramming.ezlang.exceptions.CompilerException; import com.compilerprogramming.ezlang.exceptions.InterpreterException; import com.compilerprogramming.ezlang.types.Symbol; import com.compilerprogramming.ezlang.types.EZType; @@ -45,8 +44,11 @@ public Value interpret(ExecutionStack execStack, Frame frame) { instruction = currentBlock.instructions.get(ip); switch (instruction) { case Instruction.Ret retInst -> { - if (retInst.value() instanceof Operand.ConstantOperand constantOperand) { - execStack.stack[base] = new Value.IntegerValue(constantOperand.value); + if (retInst.value() instanceof Operand.IntConstantOperand intConstantOperand) { + execStack.stack[base] = new Value.IntegerValue(intConstantOperand.value); + } + else if (retInst.value() instanceof Operand.FloatConstantOperand constantOperand) { + execStack.stack[base] = new Value.FloatValue(constantOperand.value); } else if (retInst.value() instanceof Operand.NullConstantOperand) { execStack.stack[base] = new Value.NullValue(); @@ -63,8 +65,11 @@ else if (retInst.value() instanceof Operand.RegisterOperand registerOperand) { if (moveInst.from() instanceof Operand.RegisterOperand fromReg) { execStack.stack[base + toReg.frameSlot()] = execStack.stack[base + fromReg.frameSlot()]; } - else if (moveInst.from() instanceof Operand.ConstantOperand constantOperand) { - execStack.stack[base + toReg.frameSlot()] = new Value.IntegerValue(constantOperand.value); + else if (moveInst.from() instanceof Operand.IntConstantOperand intConstantOperand) { + execStack.stack[base + toReg.frameSlot()] = new Value.IntegerValue(intConstantOperand.value); + } + else if (moveInst.from() instanceof Operand.FloatConstantOperand constantOperand) { + execStack.stack[base + toReg.frameSlot()] = new Value.FloatValue(constantOperand.value); } else if (moveInst.from() instanceof Operand.NullConstantOperand) { execStack.stack[base + toReg.frameSlot()] = new Value.NullValue(); @@ -87,11 +92,14 @@ else if (moveInst.from() instanceof Operand.NullConstantOperand) { condition = integerValue.value != 0; } else { - condition = value != null; + throw new InterpreterException("Condition expression must be Int type"); } } - else if (cbrInst.condition() instanceof Operand.ConstantOperand constantOperand) { - condition = constantOperand.value != 0; + else if (cbrInst.condition() instanceof Operand.IntConstantOperand intConstantOperand) { + condition = intConstantOperand.value != 0; + } + else if (cbrInst.condition() instanceof Operand.FloatConstantOperand) { + throw new InterpreterException("Condition expression must be Int type"); } else throw new IllegalStateException(); if (condition) @@ -110,8 +118,11 @@ else if (cbrInst.condition() instanceof Operand.ConstantOperand constantOperand) if (arg instanceof Operand.RegisterOperand param) { execStack.stack[reg] = execStack.stack[base + param.frameSlot()]; } - else if (arg instanceof Operand.ConstantOperand constantOperand) { - execStack.stack[reg] = new Value.IntegerValue(constantOperand.value); + else if (arg instanceof Operand.IntConstantOperand intConstantOperand) { + execStack.stack[reg] = new Value.IntegerValue(intConstantOperand.value); + } + else if (arg instanceof Operand.FloatConstantOperand constantOperand) { + execStack.stack[reg] = new Value.FloatValue(constantOperand.value); } else if (arg instanceof Operand.NullConstantOperand) { execStack.stack[reg] = new Value.NullValue(); @@ -138,80 +149,79 @@ else if (arg instanceof Operand.NullConstantOperand) { default: throw new InterpreterException("Invalid unary op"); } } + else if (unaryValue instanceof Value.FloatValue floatValue && unaryInst.unop.equals("-")) { + execStack.stack[base + unaryInst.result().frameSlot()] = new Value.FloatValue(-floatValue.value); + } else throw new IllegalStateException("Unexpected unary operand: " + unaryOperand); } case Instruction.Binary binaryInst -> { - long x, y; long value = 0; - boolean intOp = true; - if (binaryInst.binOp.equals("==") || binaryInst.binOp.equals("!=")) { - Operand.RegisterOperand nonNullLitOperand = null; - if (binaryInst.left() instanceof Operand.NullConstantOperand) { - nonNullLitOperand = (Operand.RegisterOperand)binaryInst.right(); - } - else if (binaryInst.right() instanceof Operand.NullConstantOperand) { - nonNullLitOperand = (Operand.RegisterOperand)binaryInst.left(); + Value leftValue = valueFromOperand(binaryInst.left(), execStack, base); + Value rightValue = valueFromOperand(binaryInst.right(), execStack, base); + if ((leftValue instanceof Value.NullValue || rightValue instanceof Value.NullValue) && + (binaryInst.binOp.equals("==") || binaryInst.binOp.equals("!="))) { + boolean equal = leftValue instanceof Value.NullValue && rightValue instanceof Value.NullValue; + value = switch (binaryInst.binOp) { + case "==" -> equal ? 1 : 0; + case "!=" -> equal ? 0 : 1; + default -> throw new IllegalStateException(); + }; + execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); + } + else { + if (leftValue instanceof Value.FloatValue left && rightValue instanceof Value.FloatValue right) { + double x = left.value; + double y = right.value; + switch (binaryInst.binOp) { + case "+" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x + y); + case "-" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x - y); + case "*" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x * y); + case "/" -> execStack.stack[base + binaryInst.result().frameSlot()] = new Value.FloatValue(x / y); + case "==" -> value = x == y ? 1 : 0; + case "!=" -> value = x != y ? 1 : 0; + case "<" -> value = x < y ? 1 : 0; + case ">" -> value = x > y ? 1 : 0; + case "<=" -> value = x <= y ? 1 : 0; + case ">=" -> value = x >= y ? 1 : 0; + default -> throw new IllegalStateException(); + } + if (binaryInst.binOp.equals("==") || binaryInst.binOp.equals("!=") || binaryInst.binOp.equals("<") || binaryInst.binOp.equals(">") || binaryInst.binOp.equals("<=") || binaryInst.binOp.equals(">=")) + execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); } - if (nonNullLitOperand != null) { - intOp = false; - Value otherValue = execStack.stack[base + nonNullLitOperand.frameSlot()]; + else if (leftValue instanceof Value.IntegerValue left && rightValue instanceof Value.IntegerValue right) { + long x = left.value; + long y = right.value; switch (binaryInst.binOp) { - case "==": { - value = otherValue instanceof Value.NullValue ? 1 : 0; - break; - } - case "!=": { - value = otherValue instanceof Value.NullValue ? 0 : 1; - break; - } - default: - throw new IllegalStateException(); + case "+": value = x + y; break; + case "-": value = x - y; break; + case "*": value = x * y; break; + case "/": value = x / y; break; + case "%": value = x % y; break; + case "==": value = x == y ? 1 : 0; break; + case "!=": value = x != y ? 1 : 0; break; + case "<": value = x < y ? 1: 0; break; + case ">": value = x > y ? 1 : 0; break; + case "<=": value = x <= y ? 1 : 0; break; + case ">=": value = x >= y ? 1 : 0; break; + default: throw new IllegalStateException(); } execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); } - } - if (intOp) { - if (binaryInst.left() instanceof Operand.ConstantOperand constant) - x = constant.value; - else if (binaryInst.left() instanceof Operand.RegisterOperand registerOperand) - x = ((Value.IntegerValue) execStack.stack[base + registerOperand.frameSlot()]).value; - else throw new IllegalStateException(); - if (binaryInst.right() instanceof Operand.ConstantOperand constant) - y = constant.value; - else if (binaryInst.right() instanceof Operand.RegisterOperand registerOperand) - y = ((Value.IntegerValue) execStack.stack[base + registerOperand.frameSlot()]).value; else throw new IllegalStateException(); - switch (binaryInst.binOp) { - case "+": value = x + y; break; - case "-": value = x - y; break; - case "*": value = x * y; break; - case "/": value = x / y; break; - case "%": value = x % y; break; - case "==": value = x == y ? 1 : 0; break; - case "!=": value = x != y ? 1 : 0; break; - case "<": value = x < y ? 1: 0; break; - case ">": value = x > y ? 1 : 0; break; - case "<=": value = x <= y ? 1 : 0; break; - case ">=": value = x >= y ? 1 : 0; break; - default: throw new IllegalStateException(); - } - execStack.stack[base + binaryInst.result().frameSlot()] = new Value.IntegerValue(value); } } case Instruction.NewArray newArrayInst -> { long size = 0; Value initValue = null; - if (newArrayInst.len() instanceof Operand.ConstantOperand constantOperand) - size = constantOperand.value; + if (newArrayInst.len() instanceof Operand.IntConstantOperand intConstantOperand) + size = intConstantOperand.value; else if (newArrayInst.len() instanceof Operand.RegisterOperand registerOperand) { Value.IntegerValue indexValue = (Value.IntegerValue) execStack.stack[base + registerOperand.frameSlot()]; size = (long) indexValue.value; } - if (newArrayInst.initValue() instanceof Operand.ConstantOperand constantOperand) - initValue = new Value.IntegerValue(constantOperand.value); - else if (newArrayInst.initValue() instanceof Operand.RegisterOperand registerOperand) - initValue = execStack.stack[base + registerOperand.frameSlot()]; + if (newArrayInst.initValue() != null) + initValue = valueFromOperand(newArrayInst.initValue(), execStack, base); execStack.stack[base + newArrayInst.destOperand().frameSlot()] = new Value.ArrayValue(newArrayInst.type, size, initValue); } case Instruction.NewStruct newStructInst -> { @@ -221,7 +231,7 @@ else if (newArrayInst.initValue() instanceof Operand.RegisterOperand registerOpe if (arrayStoreInst.arrayOperand() instanceof Operand.RegisterOperand arrayOperand) { Value.ArrayValue arrayValue = (Value.ArrayValue) execStack.stack[base + arrayOperand.frameSlot()]; int index = 0; - if (arrayStoreInst.indexOperand() instanceof Operand.ConstantOperand constant) { + if (arrayStoreInst.indexOperand() instanceof Operand.IntConstantOperand constant) { index = (int) constant.value; } else if (arrayStoreInst.indexOperand() instanceof Operand.RegisterOperand registerOperand) { @@ -229,17 +239,7 @@ else if (arrayStoreInst.indexOperand() instanceof Operand.RegisterOperand regist index = (int) indexValue.value; } else throw new IllegalStateException(); - Value value; - if (arrayStoreInst.sourceOperand() instanceof Operand.ConstantOperand constantOperand) { - value = new Value.IntegerValue(constantOperand.value); - } - else if (arrayStoreInst.sourceOperand() instanceof Operand.NullConstantOperand) { - value = new Value.NullValue(); - } - else if (arrayStoreInst.sourceOperand() instanceof Operand.RegisterOperand registerOperand) { - value = execStack.stack[base + registerOperand.frameSlot()]; - } - else throw new IllegalStateException(); + Value value = valueFromOperand(arrayStoreInst.sourceOperand(), execStack, base); if (index == arrayValue.values.size()) arrayValue.values.add(value); else @@ -249,7 +249,7 @@ else if (arrayStoreInst.sourceOperand() instanceof Operand.RegisterOperand regis case Instruction.ArrayLoad arrayLoadInst -> { if (arrayLoadInst.arrayOperand() instanceof Operand.RegisterOperand arrayOperand) { Value.ArrayValue arrayValue = (Value.ArrayValue) execStack.stack[base + arrayOperand.frameSlot()]; - if (arrayLoadInst.indexOperand() instanceof Operand.ConstantOperand constant) { + if (arrayLoadInst.indexOperand() instanceof Operand.IntConstantOperand constant) { execStack.stack[base + arrayLoadInst.destOperand().frameSlot()] = arrayValue.values.get((int) constant.value); } else if (arrayLoadInst.indexOperand() instanceof Operand.RegisterOperand registerOperand) { @@ -263,17 +263,7 @@ else if (arrayLoadInst.indexOperand() instanceof Operand.RegisterOperand registe if (setFieldInst.structOperand() instanceof Operand.RegisterOperand structOperand) { Value.StructValue structValue = (Value.StructValue) execStack.stack[base + structOperand.frameSlot()]; int index = setFieldInst.fieldIndex; - Value value; - if (setFieldInst.sourceOperand() instanceof Operand.ConstantOperand constant) { - value = new Value.IntegerValue(constant.value); - } - else if (setFieldInst.sourceOperand() instanceof Operand.NullConstantOperand) { - value = new Value.NullValue(); - } - else if (setFieldInst.sourceOperand() instanceof Operand.RegisterOperand registerOperand) { - value = execStack.stack[base + registerOperand.frameSlot()]; - } - else throw new IllegalStateException(); + Value value = valueFromOperand(setFieldInst.sourceOperand(), execStack, base); structValue.fields[index] = value; } else throw new IllegalStateException(); } @@ -290,6 +280,18 @@ else if (setFieldInst.sourceOperand() instanceof Operand.RegisterOperand registe return returnValue; } + + private Value valueFromOperand(Operand operand, ExecutionStack execStack, int base) { + if (operand instanceof Operand.IntConstantOperand constant) + return new Value.IntegerValue(constant.value); + if (operand instanceof Operand.FloatConstantOperand constant) + return new Value.FloatValue(constant.value); + if (operand instanceof Operand.RegisterOperand registerOperand) + return execStack.stack[base + registerOperand.frameSlot()]; + if (operand instanceof Operand.NullConstantOperand) + return new Value.NullValue(); + throw new IllegalStateException(); + } static class Frame { Frame caller; int base; diff --git a/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java b/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java index fc46c3b..63da59e 100644 --- a/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java +++ b/registervm/src/main/java/com/compilerprogramming/ezlang/interpreter/Value.java @@ -11,7 +11,12 @@ public IntegerValue(long value) { } public final long value; } - static public class NullValue extends Value { + static public class FloatValue extends Value { + public FloatValue(double value) { + this.value = value; + } + public final double value; + } static public class NullValue extends Value { public NullValue() {} } static public class ArrayValue extends Value { diff --git a/registervm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java b/registervm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java index f1bc9d0..ac15361 100644 --- a/registervm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java +++ b/registervm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java @@ -1,6 +1,11 @@ package com.compilerprogramming.ezlang.interpreter; import com.compilerprogramming.ezlang.compiler.Compiler; +import com.compilerprogramming.ezlang.compiler.CompiledFunction; +import com.compilerprogramming.ezlang.compiler.Instruction; +import com.compilerprogramming.ezlang.compiler.Operand; +import com.compilerprogramming.ezlang.exceptions.InterpreterException; +import com.compilerprogramming.ezlang.types.Symbol; import org.junit.Assert; import org.junit.Test; @@ -498,4 +503,59 @@ func main()->Int integerValue.value == 1); } + @Test(expected = InterpreterException.class) + public void testFloatConstantBranchConditionRejectedByInterpreter() { + String src = """ + func foo()->Int { + if (1) + return 1; + return 0; + } + """; + var compiler = new Compiler(); + var typeDict = compiler.compileSrc(src); + var functionSymbol = (Symbol.FunctionTypeSymbol) typeDict.lookup("foo"); + var function = (CompiledFunction) functionSymbol.code(); + for (int i = 0; i < function.entry.instructions.size(); i++) { + if (function.entry.instructions.get(i) instanceof Instruction.ConditionalBranch cbr) { + function.entry.instructions.set(i, new Instruction.ConditionalBranch( + function.entry, + new Operand.FloatConstantOperand(1.0, typeDict.FLOAT), + cbr.trueBlock, + cbr.falseBlock)); + break; + } + } + new Interpreter(typeDict).run("foo"); + } + @Test + public void testFloatArithmetic() { + String src = """ + func add(a: Float, b: Float)->Float { + return a+b; + } + func foo()->Float { + return add(1.25,2.5); + } + """; + var value = compileAndRun(src, "foo"); + Assert.assertNotNull(value); + Assert.assertTrue(value instanceof Value.FloatValue floatValue + && Math.abs(floatValue.value - 3.75) < 0.000001); + } + + @Test + public void testFloatArray() { + String src = """ + func foo()->Float { + var t = new [Float] {1.0,2.25,3.5}; + t[1] = t[1] + 1.25; + return t[1]; + } + """; + var value = compileAndRun(src, "foo"); + Assert.assertNotNull(value); + Assert.assertTrue(value instanceof Value.FloatValue floatValue + && Math.abs(floatValue.value - 3.5) < 0.000001); + } } diff --git a/seaofnodes/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java b/seaofnodes/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java index a93e5cb..6051d08 100644 --- a/seaofnodes/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java +++ b/seaofnodes/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java @@ -93,11 +93,15 @@ public TypeDictionary createAST(String src) { private void populateDefaultTypes(Map types) { - // Pre-create int, [int] and *[int] types + // Pre-create primitive and primitive array types types.put(typeDictionary.INT.name(), TypeInteger.BOT); + types.put(typeDictionary.FLOAT.name(), TypeFloat.F64); var intArrayType = TypeStruct.makeAry(TypeInteger.U32, _code.getALIAS(), TypeInteger.BOT, _code.getALIAS()); var ptrIntArrayType = TypeMemPtr.make(intArrayType); types.put("[" + typeDictionary.INT.name() + "]", ptrIntArrayType); + var floatArrayType = TypeStruct.makeAry(TypeInteger.U32, _code.getALIAS(), TypeFloat.F64, _code.getALIAS()); + var ptrFloatArrayType = TypeMemPtr.make(floatArrayType); + types.put("[" + typeDictionary.FLOAT.name() + "]", ptrFloatArrayType); // Also get the types created by default for (Type t: Type.gather()) { types.put(t.str(), t); @@ -186,6 +190,9 @@ else if (type instanceof EZType.EZTypeInteger || type instanceof EZType.EZTypeVoid) { return TypeInteger.BOT.str(); } + else if (type instanceof EZType.EZTypeFloat) { + return TypeFloat.F64.str(); + } else if (type instanceof EZType.EZTypeNull) { return TypeNil.NIL.str(); } @@ -571,7 +578,12 @@ else if (type instanceof EZType.EZTypeStruct typeStruct) { private Node compileUnaryExpr(AST.UnaryExpr unaryExpr) { String opCode = unaryExpr.op.str; switch (opCode) { - case "-": return peep(new MinusNode(compileExpr(unaryExpr.expr)).widen()); + case "-": { + Node expr = compileExpr(unaryExpr.expr); + return unaryExpr.type instanceof EZType.EZTypeFloat + ? peep(new MinusFNode(expr)) + : peep(new MinusNode(expr).widen()); + } // Maybe below we should explicitly set Int case "!": return peep(new NotNode(compileExpr(unaryExpr.expr))); default: throw new CompilerException("Invalid unary op", unaryExpr.lineNumber); @@ -588,34 +600,34 @@ private Node compileBinaryExpr(AST.BinaryExpr binaryExpr) { case "||": throw new CompilerException("Not yet implemented", binaryExpr.lineNumber); case "==": - idx=2; lhs = new BoolNode.EQ(lhs, null); + idx=2; lhs = binaryExpr.expr1.type instanceof EZType.EZTypeFloat ? new BoolNode.EQF(lhs, null) : new BoolNode.EQ(lhs, null); break; case "!=": - idx=2; lhs = new BoolNode.EQ(lhs, null); negate=true; + idx=2; lhs = binaryExpr.expr1.type instanceof EZType.EZTypeFloat ? new BoolNode.EQF(lhs, null) : new BoolNode.EQ(lhs, null); negate=true; break; case "<=": - idx=2; lhs = new BoolNode.LE(lhs, null); + idx=2; lhs = binaryExpr.expr1.type instanceof EZType.EZTypeFloat ? new BoolNode.LEF(lhs, null) : new BoolNode.LE(lhs, null); break; case "<": - idx=2; lhs = new BoolNode.LT(lhs, null); + idx=2; lhs = binaryExpr.expr1.type instanceof EZType.EZTypeFloat ? new BoolNode.LTF(lhs, null) : new BoolNode.LT(lhs, null); break; case ">=": - idx=1; lhs = new BoolNode.LE(null, lhs); + idx=1; lhs = binaryExpr.expr1.type instanceof EZType.EZTypeFloat ? new BoolNode.LEF(null, lhs) : new BoolNode.LE(null, lhs); break; case ">": - idx=1; lhs = new BoolNode.LT(null, lhs); + idx=1; lhs = binaryExpr.expr1.type instanceof EZType.EZTypeFloat ? new BoolNode.LTF(null, lhs) : new BoolNode.LT(null, lhs); break; case "+": - idx=2; lhs = new AddNode(lhs,null); + idx=2; lhs = binaryExpr.type instanceof EZType.EZTypeFloat ? new AddFNode(lhs,null) : new AddNode(lhs,null); break; case "-": - idx=2; lhs = new SubNode(lhs,null); + idx=2; lhs = binaryExpr.type instanceof EZType.EZTypeFloat ? new SubFNode(lhs,null) : new SubNode(lhs,null); break; case "*": - idx=2; lhs = new MulNode(lhs,null); + idx=2; lhs = binaryExpr.type instanceof EZType.EZTypeFloat ? new MulFNode(lhs,null) : new MulNode(lhs,null); break; case "/": - idx=2; lhs = new DivNode(lhs,null); + idx=2; lhs = binaryExpr.type instanceof EZType.EZTypeFloat ? new DivFNode(lhs,null) : new DivNode(lhs,null); break; default: throw new CompilerException("Not yet implemented", binaryExpr.lineNumber); @@ -630,6 +642,8 @@ private Node compileBinaryExpr(AST.BinaryExpr binaryExpr) { private Node compileConstantExpr(AST.LiteralExpr constantExpr) { if (constantExpr.type instanceof EZType.EZTypeInteger) return con(constantExpr.value.num.intValue()); + else if (constantExpr.type instanceof EZType.EZTypeFloat) + return con(TypeFloat.constant(constantExpr.value.num.doubleValue())); else if (constantExpr.type instanceof EZType.EZTypeNull) return NIL; else throw new CompilerException("Invalid constant type", constantExpr.lineNumber); @@ -798,6 +812,7 @@ private Node compileWhile(AST.WhileStmt whileStmt) { _xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis // Parse predicate + checkConditionType(whileStmt.condition.type, whileStmt.lineNumber); var pred = compileExpr(whileStmt.condition); // IfNode takes current control and predicate @@ -854,7 +869,13 @@ private Node compileWhile(AST.WhileStmt whileStmt) { return ZERO; } + private void checkConditionType(EZType conditionType, int lineNumber) { + if (!(conditionType instanceof EZType.EZTypeInteger)) + throw new CompilerException("Condition expression must be Int type", lineNumber); + } + private Node compileIf(AST.IfElseStmt ifElseStmt) { + checkConditionType(ifElseStmt.condition.type, ifElseStmt.lineNumber); var pred = compileExpr(ifElseStmt.condition).keep(); // IfNode takes current control and predicate Node ifNode = new IfNode(ctrl(), pred).peephole(); diff --git a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java index 2d82f06..b5f3041 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -47,16 +47,20 @@ private void setVirtualRegisters(Scope scope) { var type = varSymbol.type; LatticeElement elem; if (varSymbol instanceof Symbol.ParameterSymbol) { - if (type.isPrimitive()) + if (type instanceof EZType.EZTypeInteger) elem = new LatticeElement(F_INT_BOTTOM); + else if (type instanceof EZType.EZTypeFloat) + elem = new LatticeElement(F_FLT_BOTTOM); else if (type instanceof EZType.EZTypeNullable) elem = new LatticeElement(F_REF_BOTTOM); else elem = new LatticeElement(F_REF_NOT_NULL); } else { - elem = type.isPrimitive() + elem = type instanceof EZType.EZTypeInteger ? new LatticeElement(F_INT_TOP) + : type instanceof EZType.EZTypeFloat + ? new LatticeElement(F_FLT_TOP) : new LatticeElement(F_REF_TOP); } latticeElements.add(elem); @@ -67,16 +71,6 @@ else if (type instanceof EZType.EZTypeNullable) } } - // TOP - // / \ - // REF_TOP INT_TOP - // / \ / | \ - // NULL NOT_NULL ZERO NZC NZV - // \ / \ | / - // REF_BOTTOM INT_BOTTOM - // \ / - // BOTTOM - static final byte F_TOP = 0; // no usable fact yet static final byte F_REF_TOP = 1; // known reference type, value unknown @@ -90,12 +84,17 @@ else if (type instanceof EZType.EZTypeNullable) static final byte F_INT_NONZERO_VARYING = 8; static final byte F_INT_BOTTOM = 9; // may be zero or non-zero - static final byte F_BOTTOM = 10; // impossible / contradiction + static final byte F_FLT_TOP = 10; // known float type, value unknown + static final byte F_FLT_CONST = 11; + static final byte F_FLT_BOTTOM = 12; // non-const float + + static final byte F_BOTTOM = 13; // impossible / contradiction // Associated with each register static final class LatticeElement { public byte kind; private long intValue; + private double floatValue; public LatticeElement(byte kind) { this.kind = kind; @@ -108,8 +107,14 @@ public LatticeElement(long value) { kind = F_INT_TOP; setIntValue(value); } + public LatticeElement(double value) { + kind = F_FLT_TOP; + setFloatValue(value); + } LatticeElement copy() { - return new LatticeElement(kind, intValue); + LatticeElement copy = new LatticeElement(kind, intValue); + copy.floatValue = floatValue; + return copy; } boolean isTrue() { @@ -121,6 +126,23 @@ boolean isFalse() { boolean isIntegerConstant() { return kind == F_INT_ZERO || kind == F_INT_NONZERO_CONST; } + boolean isFloatConstant() { + return kind == F_FLT_CONST; + } + boolean setFloatValue(double value) { + byte prevKind = kind; + if (kind == F_FLT_TOP) { + this.floatValue = value; + this.kind = F_FLT_CONST; + } + else if (kind == F_FLT_CONST && Double.compare(value, floatValue) != 0) { + this.kind = F_FLT_BOTTOM; + } + else if (!isFloat()) { + throw new CompilerException("Cannot assign float value to non-float type"); + } + return kind != prevKind; + } boolean setIntValue(long value) { byte prevKind = kind; if (kind == F_INT_TOP) { @@ -161,6 +183,10 @@ boolean meet(LatticeElement other) { intValue != other.intValue) { kind = F_INT_NONZERO_VARYING; } + else if (kind == F_FLT_CONST && + Double.compare(floatValue, other.floatValue) != 0) { + kind = F_FLT_BOTTOM; + } return kind != old; } @@ -176,6 +202,12 @@ boolean meet(LatticeElement other) { return kind != old; } + // float lattice + if (isFloat(kind) && isFloat(other.kind)) { + meetFloat(other); + return kind != old; + } + // impossible kind = F_BOTTOM; return kind != old; @@ -257,13 +289,30 @@ private static boolean isReference(byte kind) { return kind >= F_REF_TOP && kind <= F_REF_BOTTOM; } + private void meetFloat(LatticeElement other) { + if (kind == F_FLT_TOP) { + copyFrom(other); + return; + } + if (other.kind == F_FLT_TOP) + return; + if (kind == F_FLT_CONST && + (other.kind == F_FLT_BOTTOM || + (other.kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0))) { + kind = F_FLT_BOTTOM; + } + } private static boolean isInteger(byte kind) { return kind >= F_INT_TOP && kind <= F_INT_BOTTOM; } + private static boolean isFloat(byte kind) { + return kind >= F_FLT_TOP && kind <= F_FLT_BOTTOM; + } void copyFrom(LatticeElement other) { LatticeElement copy = other.copy(); this.kind = copy.kind; this.intValue = copy.intValue; + this.floatValue = copy.floatValue; } boolean isTop() { return kind == F_TOP; @@ -281,6 +330,10 @@ boolean isInteger() { return isInteger(kind); } + boolean isFloat() { + return isFloat(kind); + } + boolean isNull() { return kind == F_REF_NULL; } @@ -315,7 +368,11 @@ public String toString() { case F_INT_ZERO -> "0"; case F_INT_NONZERO_CONST -> Long.toString(intValue); case F_INT_NONZERO_VARYING -> "non-zero"; - case F_INT_BOTTOM -> "int?"; + case F_INT_BOTTOM -> "int*"; + + case F_FLT_TOP -> "float"; + case F_FLT_CONST -> Double.toString(floatValue); + case F_FLT_BOTTOM -> "float*"; case F_BOTTOM -> "⊥"; @@ -327,12 +384,12 @@ public String toString() { public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; LatticeElement that = (LatticeElement) o; - return kind == that.kind && intValue == that.intValue; + return kind == that.kind && intValue == that.intValue && Double.compare(floatValue, that.floatValue) == 0; } @Override public int hashCode() { - return Objects.hash(kind, intValue); + return Objects.hash(kind, intValue, floatValue); } } static final class Lattice { @@ -451,7 +508,11 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { return new LatticeElement(F_REF_NULL); } if (lit.value.kind == Token.Kind.NUM) { - return new LatticeElement(Long.parseLong(lit.value.str)); + if (lit.type instanceof EZType.EZTypeInteger) + return new LatticeElement(lit.value.num.longValue()); + if (lit.type instanceof EZType.EZTypeFloat) + return new LatticeElement(lit.value.num.doubleValue()); + return factFromType(lit.type); } } @@ -463,7 +524,11 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { return new LatticeElement(-v.intValue); if (v.isInteger()) return new LatticeElement(F_INT_BOTTOM); - throw new CompilerException("Cannot apply - to non integer"); + if (v.isFloatConstant()) + return new LatticeElement(-v.floatValue); + if (v.isFloat()) + return new LatticeElement(F_FLT_BOTTOM); + throw new CompilerException("Cannot apply - to non numeric value"); } if (un.op.str.equals("!")) { @@ -499,11 +564,13 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { if (b.kind == F_INT_ZERO) throw new CompilerException("Division by zero"); value = a.intValue / b.intValue; + isArith = true; } case "%" -> { if (b.kind == F_INT_ZERO) throw new CompilerException("Division by zero"); value = a.intValue % b.intValue; + isArith = true; } case "==" -> { @@ -543,6 +610,41 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { } return result; } + if (a.isFloatConstant() && b.isFloatConstant()) { + double floatValue = 0.0; + long intValue = 0; + boolean isArith = false; + switch (bin.op.str) { + case "+" -> { + floatValue = a.floatValue + b.floatValue; + isArith = true; + } + case "-" -> { + floatValue = a.floatValue - b.floatValue; + isArith = true; + } + case "*" -> { + floatValue = a.floatValue * b.floatValue; + isArith = true; + } + case "/" -> { + floatValue = a.floatValue / b.floatValue; + isArith = true; + } + case "==" -> intValue = a.floatValue == b.floatValue ? 1 : 0; + case "!=" -> intValue = a.floatValue != b.floatValue ? 1 : 0; + case "<" -> intValue = a.floatValue < b.floatValue ? 1 : 0; + case "<=" -> intValue = a.floatValue <= b.floatValue ? 1 : 0; + case ">" -> intValue = a.floatValue > b.floatValue ? 1 : 0; + case ">=" -> intValue = a.floatValue >= b.floatValue ? 1 : 0; + default -> throw new CompilerException("Unknown binary operator " + bin.op.str); + } + if (isArith) + return new LatticeElement(floatValue); + return intValue == 1 + ? new LatticeElement(F_INT_NONZERO_CONST, 1) + : new LatticeElement(F_INT_ZERO); + } return factFromType(bin.type); } @@ -558,8 +660,10 @@ else if (type instanceof EZType.EZTypeNull) else if (type instanceof EZType.EZTypeArray || type instanceof EZType.EZTypeStruct) return new LatticeElement(F_REF_NOT_NULL); - else if (type.isPrimitive()) + else if (type instanceof EZType.EZTypeInteger) return new LatticeElement(F_INT_BOTTOM); + else if (type instanceof EZType.EZTypeFloat) + return new LatticeElement(F_FLT_BOTTOM); } return new LatticeElement(F_BOTTOM); } @@ -616,9 +720,15 @@ Lattice transferBlock(FlowCFG.FlowBlock block, Lattice in) { } private void checkAssignment(EZType type, LatticeElement lattice) { - if (type.isPrimitive()) { + if (type instanceof EZType.EZTypeInteger) { if (!lattice.isInteger()) - throw new CompilerException("Cannot assign reference/null to int"); + throw new CompilerException("Cannot assign non-int value to int"); + return; + } + + if (type instanceof EZType.EZTypeFloat) { + if (!lattice.isFloat()) + throw new CompilerException("Cannot assign non-float value to float"); return; } diff --git a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/SemaAssignTypes.java b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/SemaAssignTypes.java index cd913e3..10c9c76 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/SemaAssignTypes.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/SemaAssignTypes.java @@ -67,6 +67,17 @@ public void exit(AST.BinaryExpr binaryExpr) { // booleans are int too binaryExpr.type = typeDictionary.INT; } + else if (binaryExpr.expr1.type instanceof EZType.EZTypeFloat && + binaryExpr.expr2.type instanceof EZType.EZTypeFloat) { + if (binaryExpr.op.str.equals("%") || + binaryExpr.op.str.equals("&&") || + binaryExpr.op.str.equals("||")) + throw new CompilerException("Binary operator " + binaryExpr.op + " not supported for Float operands", binaryExpr.lineNumber); + binaryExpr.type = switch (binaryExpr.op.str) { + case "==", "!=", "<=", "<", ">", ">=" -> typeDictionary.INT; + default -> typeDictionary.FLOAT; + }; + } else if (((binaryExpr.expr1.type instanceof EZType.EZTypeNull && binaryExpr.expr2.type instanceof EZType.EZTypeNullable) || (binaryExpr.expr1.type instanceof EZType.EZTypeNullable && @@ -90,6 +101,9 @@ public void exit(AST.UnaryExpr unaryExpr) { if (unaryExpr.expr.type instanceof EZType.EZTypeInteger) { unaryExpr.type = unaryExpr.expr.type; } + else if (unaryExpr.expr.type instanceof EZType.EZTypeFloat && unaryExpr.op.str.equals("-")) { + unaryExpr.type = unaryExpr.expr.type; + } else { throw new CompilerException("Unary operator " + unaryExpr.op + " not supported for operand", unaryExpr.lineNumber); } @@ -166,7 +180,7 @@ public ASTVisitor enter(AST.LiteralExpr literalExpr) { if (literalExpr.type != null) return this; if (literalExpr.value.kind == Token.Kind.NUM) { - literalExpr.type = typeDictionary.INT; + literalExpr.type = literalExpr.value.str.contains(".") ? typeDictionary.FLOAT : typeDictionary.INT; } else if (literalExpr.value.kind == Token.Kind.IDENT && literalExpr.value.str.equals("null")) { @@ -323,6 +337,16 @@ public void exit(AST.BlockStmt blockStmt) { currentScope = currentScope.parent; } + + @Override + public void exit(AST.IfElseStmt ifElseStmt) { + checkConditionType(ifElseStmt.condition.type, ifElseStmt.lineNumber); + } + + @Override + public void exit(AST.WhileStmt whileStmt) { + checkConditionType(whileStmt.condition.type, whileStmt.lineNumber); + } @Override public void exit(AST.AssignStmt assignStmt) { validType(assignStmt.nameExpr.type, false, assignStmt.lineNumber); @@ -343,6 +367,12 @@ private void validType(EZType t, boolean allowNull, int lineNumber) { throw new CompilerException("Null type not allowed", lineNumber); } + + private void checkConditionType(EZType conditionType, int lineNumber) { + validType(conditionType, false, lineNumber); + if (!(conditionType instanceof EZType.EZTypeInteger)) + throw new CompilerException("Condition expression must be Int type", lineNumber); + } private void checkAssignmentCompatible(EZType var, EZType value, int lineNumber) { if (!var.isAssignable(value)) throw new CompilerException("Value of type " + value + " cannot be assigned to type " + var, lineNumber); diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaAssignTypes.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaAssignTypes.java index e0f29af..03cd68a 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaAssignTypes.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaAssignTypes.java @@ -480,4 +480,53 @@ func foo() { """; analyze(src, "foo", "func foo()"); } + @Test + public void testFloatArithmeticAndArray() { + String src = """ + func foo(a: Float, b: Float)->Float + { + var f = new [Float] {1.5, 2.5} + f[0] = a + b + return f[0] + } +"""; + analyze(src, "foo", "func foo(a: Float,b: Float)->Float"); + } + + @Test + public void testFloatComparisonReturnsInt() { + String src = """ + func foo(a: Float, b: Float)->Int + { + return a < b + } +"""; + analyze(src, "foo", "func foo(a: Float,b: Float)->Int"); + } + @Test(expected = CompilerException.class) + public void testFloatIfConditionRejected() { + String src = """ + func foo()->Int + { + if (1.0) + return 1 + return 0 + } +"""; + analyze(src, "foo", "func foo()->Int"); + } + + @Test(expected = CompilerException.class) + public void testFloatWhileConditionRejected() { + String src = """ + func foo()->Int + { + while (1.0) { + return 1 + } + return 0 + } +"""; + analyze(src, "foo", "func foo()->Int"); + } } diff --git a/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java b/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java index b6031a6..f6e7be2 100644 --- a/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java +++ b/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/CompiledFunction.java @@ -146,6 +146,7 @@ private void compileWhile(AST.WhileStmt whileStmt) { currentBreakTarget = exitBlock; currentContinueTarget = loopHead; startBlock(loopHead); + checkConditionType(whileStmt.condition.type, whileStmt.lineNumber); boolean indexed = compileExpr(whileStmt.condition); if (indexed) codeIndexedLoad(); @@ -159,6 +160,11 @@ private void compileWhile(AST.WhileStmt whileStmt) { currentBreakTarget = savedBreakTarget; } + private void checkConditionType(EZType conditionType, int lineNumber) { + if (!(conditionType instanceof EZType.EZTypeInteger)) + throw new CompilerException("Condition expression must be Int type", lineNumber); + } + private boolean isBlockTerminated(BasicBlock block) { return (block.instructions.size() > 0 && block.instructions.getLast().isTerminal()); @@ -182,6 +188,7 @@ private void compileIf(AST.IfElseStmt ifElseStmt) { boolean needElse = ifElseStmt.elseStmt != null; BasicBlock elseBlock = needElse ? createBlock() : null; BasicBlock exitBlock = createBlock(); + checkConditionType(ifElseStmt.condition.type, ifElseStmt.lineNumber); boolean indexed = compileExpr(ifElseStmt.condition); if (indexed) codeIndexedLoad(); @@ -278,7 +285,7 @@ private boolean compileFieldExpr(AST.GetFieldExpr fieldExpr) { boolean indexed = compileExpr(fieldExpr.object); if (indexed) codeIndexedLoad(); - code(new Instruction.PushConst(fieldIndex)); + code(new Instruction.pushIntConstant(fieldIndex)); return true; } @@ -297,7 +304,7 @@ private boolean compileSetFieldExpr(AST.SetFieldExpr setFieldExpr) { throw new CompilerException("Field " + setFieldExpr.fieldName + " not found in struct " + structType.name, setFieldExpr.lineNumber); if (!(setFieldExpr instanceof AST.InitFieldExpr)) compileExpr(setFieldExpr.object); - code(new Instruction.PushConst(fieldIndex)); + code(new Instruction.pushIntConstant(fieldIndex)); boolean indexed = compileExpr(setFieldExpr.value); if (indexed) codeIndexedLoad(); @@ -344,6 +351,8 @@ private boolean compileSymbolExpr(AST.NameExpr symbolExpr) { } private boolean codeBoolean(AST.BinaryExpr binaryExpr) { + checkConditionType(binaryExpr.expr1.type, binaryExpr.lineNumber); + checkConditionType(binaryExpr.expr2.type, binaryExpr.lineNumber); boolean isAnd = binaryExpr.op.str.equals("&&"); BasicBlock l1 = createBlock(); BasicBlock l2 = createBlock(); @@ -363,7 +372,7 @@ private boolean codeBoolean(AST.BinaryExpr binaryExpr) { jumpTo(l3); startBlock(l2); // Below we must write to the same temp - code(new Instruction.PushConst(isAnd ? 0 : 1)); + code(new Instruction.pushIntConstant(isAnd ? 0 : 1)); jumpTo(l3); startBlock(l3); // leaves result on stack @@ -384,10 +393,10 @@ private boolean compileBinaryExpr(AST.BinaryExpr binaryExpr) { if (indexed) codeIndexedLoad(); switch (binOp) { - case "+" -> opCode = Instruction.ADD_I; - case "-" -> opCode = Instruction.SUB_I; - case "*" -> opCode = Instruction.MUL_I; - case "/" -> opCode = Instruction.DIV_I; + case "+" -> opCode = binaryExpr.type instanceof EZType.EZTypeFloat ? Instruction.ADD_F : Instruction.ADD_I; + case "-" -> opCode = binaryExpr.type instanceof EZType.EZTypeFloat ? Instruction.SUB_F : Instruction.SUB_I; + case "*" -> opCode = binaryExpr.type instanceof EZType.EZTypeFloat ? Instruction.MUL_F : Instruction.MUL_I; + case "/" -> opCode = binaryExpr.type instanceof EZType.EZTypeFloat ? Instruction.DIV_F : Instruction.DIV_I; case "%" -> opCode = Instruction.MOD_I; case "==" -> opCode = Instruction.EQ; case "!=" -> opCode = Instruction.NE; @@ -407,7 +416,7 @@ private boolean compileUnaryExpr(AST.UnaryExpr unaryExpr) { if (indexed) code(new Instruction.LoadIndexed()); switch (unaryExpr.op.str) { - case "-" -> opCode = Instruction.NEG_I; + case "-" -> opCode = unaryExpr.type instanceof EZType.EZTypeFloat ? Instruction.NEG_F : Instruction.NEG_I; case "!" -> opCode = Instruction.NOT; default -> throw new CompilerException("Invalid binary op", unaryExpr.lineNumber); } @@ -416,7 +425,10 @@ private boolean compileUnaryExpr(AST.UnaryExpr unaryExpr) { } private boolean compileConstantExpr(AST.LiteralExpr constantExpr) { - code(new Instruction.PushConst(constantExpr.value.num.intValue())); + if (constantExpr.type instanceof EZType.EZTypeFloat) + code(new Instruction.PushFloatConst(constantExpr.value.num.doubleValue())); + else + code(new Instruction.pushIntConstant(constantExpr.value.num.intValue())); return false; } diff --git a/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java b/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java index fc0237d..a2fcbc2 100644 --- a/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java +++ b/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java @@ -30,6 +30,12 @@ public class Instruction { public static final int GT = 22; public static final int LE = 23; public static final int GE = 24; + public static final int PUSH_F = 25; + public static final int ADD_F = 26; + public static final int SUB_F = 27; + public static final int MUL_F = 28; + public static final int DIV_F = 29; + public static final int NEG_F = 30; static final String[] opNames = { "ret", @@ -56,7 +62,13 @@ public class Instruction { "lt", "gt", "le", - "ge" + "ge", + "pushf", + "addf", + "subf", + "mulf", + "divf", + "negf" }; public final int opcode; @@ -73,9 +85,9 @@ public StringBuilder toStr(StringBuilder sb) { return sb.append(opNames[opcode]); } - public static class PushConst extends Instruction { + public static class pushIntConstant extends Instruction { public final int value; - public PushConst(int value) { + public pushIntConstant(int value) { super(PUSH_I); this.value = value; } @@ -85,6 +97,17 @@ public StringBuilder toStr(StringBuilder sb) { } } + public static class PushFloatConst extends Instruction { + public final double value; + public PushFloatConst(double value) { + super(PUSH_F); + this.value = value; + } + @Override + public StringBuilder toStr(StringBuilder sb) { + return super.toStr(sb).append(" ").append(value); + } + } public static class BinaryOp extends Instruction { public BinaryOp(int opcode) { super(opcode); diff --git a/types/src/main/java/com/compilerprogramming/ezlang/types/EZType.java b/types/src/main/java/com/compilerprogramming/ezlang/types/EZType.java index 4d2e602..ce76516 100644 --- a/types/src/main/java/com/compilerprogramming/ezlang/types/EZType.java +++ b/types/src/main/java/com/compilerprogramming/ezlang/types/EZType.java @@ -7,7 +7,7 @@ import java.util.Objects; /** - * Currently, we support Int, Struct, and Array of Int/Struct. + * Currently, we support Int, Float, Struct, and Array of Int/Float/Struct. * Arrays and Structs are reference types. */ public abstract class EZType { @@ -17,10 +17,11 @@ public abstract class EZType { static final byte TUNKNOWN = 1; static final byte TNULL = 2; static final byte TINT = 3; // Int, Bool - static final byte TNULLABLE = 4; // Null, or not null ptr - static final byte TFUNC = 5; // Function types - static final byte TSTRUCT = 6; - static final byte TARRAY = 7; + static final byte TFLOAT = 4; // Float + static final byte TNULLABLE = 5; // Null, or not null ptr + static final byte TFUNC = 6; // Function types + static final byte TSTRUCT = 7; + static final byte TARRAY = 8; public final byte tclass; // type class public final String name; // type name, always unique @@ -106,6 +107,16 @@ public boolean isPrimitive() { } } + public static class EZTypeFloat extends EZType { + public EZTypeFloat() { + super(TFLOAT, "Float"); + } + @Override + public boolean isPrimitive() { + return true; + } + } + public static class EZTypeStruct extends EZType { ArrayList fieldNames = new ArrayList<>(); ArrayList fieldTypes = new ArrayList<>(); diff --git a/types/src/main/java/com/compilerprogramming/ezlang/types/TypeDictionary.java b/types/src/main/java/com/compilerprogramming/ezlang/types/TypeDictionary.java index 7c0260a..522d307 100644 --- a/types/src/main/java/com/compilerprogramming/ezlang/types/TypeDictionary.java +++ b/types/src/main/java/com/compilerprogramming/ezlang/types/TypeDictionary.java @@ -5,12 +5,14 @@ public final class TypeDictionary extends Scope { public final EZType.EZTypeUnknown UNKNOWN; public final EZType.EZTypeInteger INT; + public final EZType.EZTypeFloat FLOAT; public final EZType.EZTypeNull NULL; public final EZType.EZTypeVoid VOID; public TypeDictionary() { super(null); INT = (EZType.EZTypeInteger) intern(new EZType.EZTypeInteger()); + FLOAT = (EZType.EZTypeFloat) intern(new EZType.EZTypeFloat()); UNKNOWN = (EZType.EZTypeUnknown) intern(new EZType.EZTypeUnknown()); NULL = (EZType.EZTypeNull) intern(new EZType.EZTypeNull()); VOID = (EZType.EZTypeVoid) intern(new EZType.EZTypeVoid()); @@ -21,6 +23,10 @@ public EZType makeArrayType(EZType elementType, boolean isNullable) { var arrayType = intern(new EZType.EZTypeArray(ti)); return isNullable ? intern(new EZType.EZTypeNullable(arrayType)) : arrayType; } + case EZType.EZTypeFloat tf -> { + var arrayType = intern(new EZType.EZTypeArray(tf)); + return isNullable ? intern(new EZType.EZTypeNullable(arrayType)) : arrayType; + } case EZType.EZTypeStruct ts -> { var arrayType = intern(new EZType.EZTypeArray(ts)); return isNullable ? intern(new EZType.EZTypeNullable(arrayType)) : arrayType; diff --git a/types/src/test/java/com/compilerprogramming/ezlang/types/TestTypes.java b/types/src/test/java/com/compilerprogramming/ezlang/types/TestTypes.java index d64c7e2..250f3da 100644 --- a/types/src/test/java/com/compilerprogramming/ezlang/types/TestTypes.java +++ b/types/src/test/java/com/compilerprogramming/ezlang/types/TestTypes.java @@ -51,4 +51,13 @@ public void testTypes() { Assert.assertFalse(nullableS1Type.isAssignable(s2Type)); } + @Test + public void testFloatTypes() { + var typeDict = new TypeDictionary(); + Assert.assertTrue(typeDict.FLOAT.isPrimitive()); + Assert.assertTrue(typeDict.FLOAT.isAssignable(typeDict.FLOAT)); + Assert.assertFalse(typeDict.FLOAT.isAssignable(typeDict.INT)); + var floatArray = typeDict.makeArrayType(typeDict.FLOAT, false); + Assert.assertEquals("[Float]", floatArray.name()); + } } From 950c238ca86abc4be620e7d6f4f447c3cc6d0fb0 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 11:54:52 +0100 Subject: [PATCH 02/15] Update --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 74ac081..d6c782e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ replay_pid* .idea -target \ No newline at end of file +target + +*.o +.run \ No newline at end of file From ff296b274332dac2a3351d1edacf9f074a50b8ba Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:26:18 +0100 Subject: [PATCH 03/15] Add float type to the language description --- .../ezlang/antlr/EZLanguage.g4 | 6 +- docs/ez-lang.rst | 372 ++++++++++++++++++ 2 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 docs/ez-lang.rst diff --git a/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 b/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 index bfcd0c8..cce44c5 100644 --- a/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 +++ b/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 @@ -28,6 +28,7 @@ typeName nominalType : 'Int' + | 'Float' | IDENTIFIER ('?')? ; @@ -86,7 +87,7 @@ additionExpression ; multiplicationExpression - : unaryExpression (('*' | '/' ) unaryExpression)* + : unaryExpression (('*' | '/' | '%') unaryExpression)* ; unaryExpression @@ -116,6 +117,7 @@ fieldExpression primaryExpression : INTEGER_LITERAL + | FLOAT_LITERAL | IDENTIFIER | '(' orExpression ')' | 'new' typeName initExpression @@ -158,6 +160,8 @@ INTEGER_LITERAL ; DEC_LITERAL: DEC_DIGIT (DEC_DIGIT | '_')*; +FLOAT_LITERAL: DEC_DIGIT+ '.' DEC_DIGIT*; + fragment DEC_DIGIT: [0-9]; diff --git a/docs/ez-lang.rst b/docs/ez-lang.rst new file mode 100644 index 0000000..82af72f --- /dev/null +++ b/docs/ez-lang.rst @@ -0,0 +1,372 @@ +The EeZee Programming Language +============================== + +The EeZee programming language is a toy language with just enough features to allow +experimenting with various compiler techniques. + +The base language is intentionally very small. Eventually there will be extended versions +that allow functional and object oriented paradigms. + +Language features +----------------- +* User defined functions +* Integer type +* Floating point type +* User defined ``struct`` types +* One dimensional arrays +* Basic control flow such as ``if`` and ``while`` statements + +Keywords +-------- +Following are keywords in the language:: + + func var Int Float struct if else while break continue return null + +Source Unit +----------- + +The EeZee language does not have the concept of modules or imports. Each source file must be +self contained. + +There is no predefined ``main`` function in a source unit. The runtime should allow +any defined function to be invoked by supplying appropriate arguments. + +Types +----- + +The only primitive types in the language are the integer type ``Int`` and the floating point type ``Float``. +The sizes of these types are unspecified, the default implementation sets them as 64-bits in size. + +There is not a distinct boolean type, non-zero integer values evaluate as true, and zero evaluates as false. + +Users can define one-dimensional arrays and structs. + +Arrays and structs are implicitly reference types, i.e. instances of these types are +allocated on the heap. + +The language does not specify whether the heap is garbage collected or manually managed, it is +up to the implementation. + +A ``struct`` type is a named aggregate with one or more fields. Fields may be of any supported +type. Struct types are nominal, i.e. each struct type is identified uniquely by its name. +Multiple definitions of a struct type are not allowed. + +An array type is declared by enclosing the element type in brackets, i.e. ``[`` and ``]``. + +There is a ``Null`` type, with a predefined literal named ``null`` of this type. + +When declaring fields or variables of reference types, users may suffix the type name with ``?`` to +indicate a ``Nullable`` type. A ``Null`` is an implicit subtype of all ``Nullable`` types. + +Examples:: + + struct Tree { + var left: Tree? + var right: Tree? + } + struct Test { + var intArray: [Int] + } + struct TreeArray { + var array: [Tree?]? + } + +The language does not require forward declarations. + +Functions +--------- + +Users can declare functions, each function must have a unique name. + +Functions cannot be overloaded. Functions are not closures. + +Functions can accept one or more arguments and may optionally return a result. + +The ``func`` keyword introduces a function declaration. + +Examples:: + + func fib(n: Int)->Int { + var f1=1 + var f2=1 + var i=n + while( i>1 ){ + var temp = f1+f2 + f1=f2 + f2=temp + i=i-1 + } + return f2 + } + + func foo()->Int { + return fib(10) + } + +Literals +-------- + +The only literals are integer values, floating point values, and ``null``. + +Variables and Fields +-------------------- + +The ``var`` keyword is used to introduce a new variable in the current lexical scope, +or to add a field to a struct. + +There are two forms of this: + +When introducing variables, you can supply an initializer; this removes the need to +specify a type. Examples:: + + var i = 1 + var j = foo() + +In this form the type of the variable is inferred from the initializer's type. + +The second form is more suited when declaring fields in a struct. In this form +a type is required - initializer cannot be set. + +Example:: + + struct T + { + var f: Int + var arry: [Int] + } + +Creating new instances of Arrays +-------------------------------- + +The ``new`` keyword is used to create array instances. + +It must be followed by an array type name and an initializer. + +The array initializer must be a comma separated list of values, enclosed in ``{`` and ``}``. + +The array is sized based on number of values in the initializer. + +Alternatively the array initializer may have a field named ``len`` that specifies the size of the +array, and a field named ``value`` to specify the value to use. + +Examples:: + + var arry = new [Int] {1,2,3} + var arry2 = new [Int] {len=10, value=0} + +The second example creates an array with 10 elements and sets the initial value to 0. + +Creating new instances of structs +--------------------------------- + +The ``new`` keyword is used to create struct instances. + +It must be followed by the struct type name and an initializer. + +The struct initializer must be a comma separated list of field initializers, enclosed in ``{`` and ``}``. + +A field initializer has the form of name followed by ``=`` followed by an expression. + +Examples:: + + var stats = new Stats { age=10, height=100 } + + +Control Flow +------------ + +The language is block structured. + +A block is enclosed in ``{`` and ``}`` and introduces a lexical scope. + +The ``if`` statement allows branching based on a condition. The condition must be an +integer expression; a value of zero is ``false``, any other value is ``true``. + +The ``if`` statement can have an optional ``else`` branch. + +The only looping construct is the ``while`` statement; this executes the sub statement +as long as the supplied integer condition evaluates to a non zero value. + +The ``break`` statement exits a loop. + +The ``continue`` statement branches to the beginning of the loop. + +The ``return`` statement takes an expression if the function is meant to return a value. +It causes the currently executing function to terminate. + +Expressions +----------- + +Following table describes the available operators by their precedence (low to high): + ++------------+-----------------+----------+ +| Operator | Meaning | Type | +| | | | ++============+=================+==========+ +| ``||`` | logical or | Binary | ++------------+-----------------+----------+ +| ``&&`` | logical and | Binary | ++------------+-----------------+----------+ +| ``==`` | relational | Binary | +| ``!=`` | | | +| ``<`` | | | +| ``<=`` | | | +| ``>`` | | | +| ``>=`` | | | ++------------+-----------------+----------+ +| ``+`` | addition | Binary | +| ``-`` | | | ++------------+-----------------+----------+ +| ``*`` | multiplication | Binary | +| ``/`` | | | +| ``%`` | | | ++------------+-----------------+----------+ +| ``-`` | negate | Unary | +| ``!`` | | | ++------------+-----------------+----------+ +| ``(...)``, | function call, | Postfix | +| ``[]``, | array index, | | +| ``.`` ID | field access | | ++------------+-----------------+----------+ + + + +Grammar +------- + +The following grammar describes the language syntax:: + + program + : declaration+ EOF + ; + + declaration + : structDeclaration + | functionDeclaration + ; + + structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + + fields + : varDeclaration+ + ; + + varDeclaration + : 'var' IDENTIFIER ':' typeName ';'? + ; + + typeName + : nominalType + | arrayType + ; + + nominalType + : 'Int' + | 'Float' + | IDENTIFIER ('?')? + ; + + arrayType + : '[' nominalType ']' ('?')? + ; + + functionDeclaration + : 'func' IDENTIFIER '(' parameters? ')' ('->' typeName)? block + ; + + parameters + : parameter (',' parameter)* + ; + + parameter + : IDENTIFIER ':' typeName + ; + + block + : '{' statement* '}' + ; + + statement + : 'if' '(' expression ')' statement + | 'if' '(' expression ')' statement 'else' statement + | 'while' '(' expression ')' statement + | postfixExpression '=' expression ';'? + | block + | 'break' ';'? + | 'continue' ';'? + | varDeclaration + | 'var' IDENTIFIER '=' expression ';'? + | 'return' orExpression? ';'? + | expression ';'? + ; + + expression + : orExpression + ; + + orExpression + : andExpression ('||' andExpression)* + ; + + andExpression + : relationalExpression ('&&' relationalExpression)* + ; + + relationalExpression + : additionExpression (('==' | '!='| '>'| '<'| '>='| '<=') additionExpression)* + ; + + additionExpression + : multiplicationExpression (('+' | '-') multiplicationExpression)* + ; + + multiplicationExpression + : unaryExpression (('*' | '/' | '%') unaryExpression)* + ; + + unaryExpression + : ('-' | '!') unaryExpression + | postfixExpression + ; + + postfixExpression + : primaryExpression (indexExpression | callExpression | fieldExpression)* + ; + + indexExpression + : '[' orExpression ']' + ; + + callExpression + : '(' arguments? ')' + ; + + arguments + : orExpression (',' orExpression)* + ; + + fieldExpression + : '.' IDENTIFIER + ; + + primaryExpression + : INTEGER_LITERAL + | FLOAT_LITERAL + | IDENTIFIER + | '(' orExpression ')' + | 'new' typeName initExpression + ; + + initExpression + : '{' initializers? '}' + ; + + initializers + : initializer (',' initializer)* + ; + + initializer + : (IDENTIFIER '=')? orExpression + ; From 5658b3ce014f75b6d61b81a77a38a11d50d08ccb Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:30:58 +0100 Subject: [PATCH 04/15] Add liveness doc --- docs/liveness.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/liveness.md diff --git a/docs/liveness.md b/docs/liveness.md new file mode 100644 index 0000000..3edcc59 --- /dev/null +++ b/docs/liveness.md @@ -0,0 +1,119 @@ +# Liveness Analysis + +This document describes the algorithm implemented by +`com.compilerprogramming.ezlang.compiler.Liveness`. + +## Goal + +For every reachable basic block, the implementation computes these sets: + +- `UEVar`: registers used before any local definition in the block. +- `varKill`: registers defined in the block. +- `phiDefs`: registers defined by leading phi nodes in the block. +- `phiUses`: registers used by successor phis on outgoing edges from this block. +- `liveIn`: registers live at block entry. +- `liveOut`: registers live at block exit. + +The implementation supports ordinary CFG liveness and adds special handling +for SSA phi semantics. + +## Algorithm + +1. Collect reachable blocks from the function entry using a postorder traversal + of the forward CFG. + +2. Allocate empty `LiveSet` instances for every block. Each set is sized from + the function's register pool: + + ```text + UEVar, varKill, liveIn, liveOut, phiUses, phiDefs + ``` + +3. Initialize local block sets. + + First, scan leading phi instructions in each block. For every phi: + + ```text + x = phi(...) + ``` + + add `x` to `block.phiDefs`. + + Then scan all instructions in the block. + + For phi instructions, each input is treated as live on the corresponding + predecessor edge, not live-in to the phi's own block. For each phi input at + index `i`: + + ```text + pred = block.predecessors[i] + pred.phiUses += phi.input[i] + ``` + + There is one special case: if the predecessor is the same block and the input + is also a phi definition in that block, the implementation skips adding it to + `phiUses`. This is intended to avoid over-marking same-block phi cycles. + + For non-phi instructions, standard upward-exposed use/def collection is + performed: + + ```text + for each use in instruction.uses: + if use not in block.varKill: + block.UEVar += use + + if instruction defines a register: + block.varKill += def + ``` + +4. Iterate to a fixed point. + + Repeatedly recompute each block's `liveOut` and `liveIn` until no `liveIn` + set changes. + + The live-out formula is: + + ```text + liveOut(B) = + union over successors S of (liveIn(S) - phiDefs(S)) + union phiUses(B) + ``` + + This means successor phi definitions are not considered live-out of the + predecessor, while phi inputs on predecessor edges are. + + The live-in formula is: + + ```text + liveIn(B) = + phiDefs(B) + union UEVar(B) + union (liveOut(B) - varKill(B)) + ``` + + This means phi results are considered live at entry to their block, normal + upward-exposed uses are live at entry, and values live-out remain live-in + unless killed by a definition inside the block. + +5. Store the final sets directly on each `BasicBlock`, and mark the function as + having liveness information. + +## Phi Semantics + +The implementation models a phi as if edge copies existed: + +```text +B1 -> B3: x3 = x1 +B2 -> B3: x3 = x2 +B3: + x3 = phi(x1, x2) +``` + +So: + +- `x1` is live-out of `B1`, but not live-in to `B3` because of the phi. +- `x2` is live-out of `B2`, but not live-in to `B3`. +- `x3` is live-in to `B3`, but not live-out of `B1` or `B2` just because of the + phi definition. + +This is the purpose of splitting phi information into `phiUses` and `phiDefs`. From 4ebf8e3f54e4c143f97bdc4e4bfe50de7377a137 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:35:51 +0100 Subject: [PATCH 05/15] Add ssa construction doc --- docs/ssa-construction.md | 198 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/ssa-construction.md diff --git a/docs/ssa-construction.md b/docs/ssa-construction.md new file mode 100644 index 0000000..31de0d2 --- /dev/null +++ b/docs/ssa-construction.md @@ -0,0 +1,198 @@ +# SSA Construction + +This document describes the algorithm implemented by +`com.compilerprogramming.ezlang.compiler.EnterSSA`. + +## Goal + +The pass transforms a non-SSA `CompiledFunction` into SSA form. It inserts phi +nodes where multiple definitions of the same original register can meet, then +renames every definition and use so that each SSA register has exactly one +definition. + +The implementation follows the usual dominance-frontier construction described +by Briggs et al., with an additional liveness check during phi placement. In this +codebase, source variables and temporaries are represented by `Register` +instances. During SSA construction, each version is represented by an +`SSARegister` that keeps the original register id as its `nonSSAId()`. + +## Data Structures + +- `domTree`: dominator tree and dominance frontiers for the reachable CFG. +- `blocks`: reachable basic blocks in reverse postorder. +- `nonLocalNames`: original registers used before a local definition in at + least one block. +- `blockSets`: for each original register, the set of blocks that define it. +- `counters`: the next SSA version number for each original register. +- `stacks`: the current SSA name stack for each original register. + +## Algorithm + +1. Compute dominance information. + + ```text + domTree <- DominatorTree(function.entry) + blocks <- domTree.blocks + ``` + + The dominator tree also computes the dominance frontier for each reachable + block. + +2. Find non-local names and definition blocks. + + For each block, scan instructions from top to bottom with a block-local + `varKill` set: + + ```text + for each block in blocks + varKill <- empty set + + for each instruction in block + for each used register r + if r is not in varKill + nonLocalNames[r] <- r + + if instruction defines register r + add r to varKill + add block to blockSets[r] + ``` + + A register is non-local if some block can use it before defining it locally. + Registers that are only used after local definitions do not need phi nodes. + +3. Compute liveness. + + ```text + Liveness(function) + ``` + + Phi placement uses `liveIn` to avoid inserting phi nodes for values that are + dead at a join block. + +4. Insert phi nodes. + + For each non-local register `v`, start with the blocks that define `v`. Walk + the iterated dominance frontier with a worklist: + + ```text + for each non-local register v + visited <- empty set + worklist <- blockSets[v] + + while worklist is not empty + b <- pop(worklist) + mark b as visited + + for each d in dominanceFrontier(b) + if v is live-in at d + insert phi for v in d unless one already exists + + if d has not been visited + push d onto worklist + ``` + + The inserted phi initially has the form: + + ```text + v = phi(v, v, ...) + ``` + + with one input per predecessor. The rename pass rewrites the result and all + inputs to the appropriate SSA versions. + +5. Initialize version state. + + ```text + for each original register v + counters[v] <- 0 + stacks[v] <- empty stack + ``` + +6. Rename variables by walking the dominator tree. + + ```text + search(function.entry) + ``` + +7. Mark the function as SSA and invalidate old liveness. + + ```text + function.isSSA <- true + function.hasLiveness <- false + ``` + +## Rename Search + +`search(block)` performs a preorder walk of the dominator tree. The stack for an +original register always contains the SSA name visible at the current program +point. + +1. Rename phi results. + + ```text + for each phi v = phi(...) in block + v_i <- makeVersion(v) + replace phi result with v_i + ``` + + `makeVersion(v)` creates a new `SSARegister`, pushes it onto `stacks[v]`, + and increments `counters[v]`. + +2. Rename ordinary instruction uses, then definitions. + + ```text + for each non-phi instruction in block + for each used register x + replace x with top(stacks[x]) + + if instruction defines register v + v_i <- makeVersion(v) + replace definition with v_i + ``` + + Uses are rewritten before the instruction definition is renamed, so an + instruction such as `x = x + 1` reads the old visible version of `x` and + defines a new version. + +3. Fill phi inputs in successor blocks. + + ```text + for each successor s of block + j <- s.whichPred(block) + + for each phi in s + v <- phi input j + replace input j with top(stacks[v]) + ``` + + The predecessor index selects the phi operand that corresponds to the edge + from the current block to the successor. + +4. Recurse into dominated children. + + ```text + for each child c of block in the dominator tree + search(c) + ``` + +5. Pop definitions introduced in this block. + + ```text + for each instruction in block + if instruction is a phi or defines a register v + pop(stacks[v]) + ``` + + Popping restores the visible name from the dominating scope. + +## Notes + +The class comment describes this pass as semi-pruned SSA. The implementation also +uses block liveness during phi placement, so it performs the pruned-style check: + +```text +insert phi for v in d only if v is live-in at d +``` + +This keeps dead phi nodes out of the IR and avoids rename failures caused by phi +inputs with no available reaching definition. From f01cff38fc9883573c30a582f280df588faddd5f Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:36:00 +0100 Subject: [PATCH 06/15] Add ssa construction doc --- .../java/com/compilerprogramming/ezlang/compiler/EnterSSA.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/EnterSSA.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/EnterSSA.java index 4425ae9..9b59295 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/EnterSSA.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/EnterSSA.java @@ -9,6 +9,8 @@ * The algorithm is described in the paper * 'Practical Improvements to the Construction and Destruction * of Single Static Assigment Form' by Preston Briggs. + * + * See docs/ssa-construction.md for a description of this implementation. */ public class EnterSSA { From 0e57a0a5df8cf0b6b4ee7d1d9f3d3539e0111aa2 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:38:22 +0100 Subject: [PATCH 07/15] Add ssa construction doc --- docs/ir-design-instructions.md | 153 ++++++++++++++++++++++++++++++ docs/ir-virtual-registers.md | 168 +++++++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 docs/ir-design-instructions.md create mode 100644 docs/ir-virtual-registers.md diff --git a/docs/ir-design-instructions.md b/docs/ir-design-instructions.md new file mode 100644 index 0000000..53c31c9 --- /dev/null +++ b/docs/ir-design-instructions.md @@ -0,0 +1,153 @@ +This is part of the series that discusses what you must think about when deciding how to represent IRs. + +* [Part 1 - Virtual Registers](https://github.com/CompilerProgramming/ez-lang/wiki/Virtual-Registers) + +# Linear IR + +The discussion here is primarily for linear Intermediate Representations. By linear we mean that the instructions are organized into sequences that are logically executed one after the other. This is in contrast to graph IRs where instructions are organized as a graph and the order of execution is not sufficiently defined by the graph. + +# Organize Instructions Into Basic Blocks + +For many algorithms we need to operate on the control flow graph. It is important to organize the instructions into Basic Blocks, where each BB encapsulates instructions that are executed sequentially. The final instruction in the BB must a jump to some other BB. + +```java +public class BasicBlock { + List instructions; +} +``` + +# Control Flow Graph + +The Basic Blocks should contain links to predecessor and successor BBs - this forms the Control Flow graph. The links are driven by the jump instructions that terminate each BB. + +```java +public class BasicBlock { + List instructions; + List successors; + List predecessors; +} +``` + +# Instruction Operands + +Instructions deal with values, some of those may be Virtual Registers, some Constants, and even Basic Blocks, for example in jump Instructions. We therefore create a type called Operand that represents values handled by an Instruction. Operands can have different subtypes in an OO design (or an Operand can be a union or sumtype in non-OO languages) - where each subtype is for a specific type of value. + +```java +class Operand {} +class RegisterOperand extends Operand {} +class ConstantOperand extends Operand {} +``` + +# Instructions Define and Use Registers + +From the optimization point of view, it is important to model the Instruction in such a way that we can easily handle following requirements: + +* Does the instruction define a Register - i.e. assigns a value to it? +* Does it use Registers defined by other instructions +* Replace a register that is defined +* Replace a specific use of a Register - or replace all uses - a Register may be replaced by another Register or by something else such as a Constant. + +Whether an Instruction defines a Register and/or uses Values including Registers depends on the Instruction type. So each Instruction type sets its own requirements, however, all Instructions satisfy the interface requirements above, so that the optimizer can treat each instruction uniformly. + +```java +class Instruction { + // Does this instruction define a value? + boolean definesVar(); + // Get the defined value + Register def(); + // Get register uses + List uses(); + // Replace register that is defined + void replaceDef(Register newReg); + // Replace uses + void replaceUses(Register[] newUses); + // Replace specific use + boolean replaceUse(Register oldUse, Register newUse); + // Replace specific register with constant + void replaceWithConstant(Register register, ConstantOperand constantOperand); +} +``` + +# Phis Are Special + +Phi instructions also define a Register and use one or more Registers, but Phis do not have the same semantics as other instructions in many situations, such as when computing Liveness. So a consideration is how to model Phi instructions, whether they obey the general interface or have their own specialized interface. In EeZee language we give Phi instructions their own interface, which is similar to the general instruction but ensures that Phis cannot be erroneously manipulated by the optimizer. This has a trade off because there are scenarios where Phis do not require the special treatment. + +```java +class Phi extends Instruction { + // def = value + Register value(); + void replaceValue(Register newReg); + + // input = use + int numInputs(); + Operand input(int i); + Register inputAsRegister(int i); + boolean isRegisterInput(int i); + Register[] inputRegisters(); + void replaceInput(int i, Register newReg); + void removeInput(int i); +} +``` + +# Instructions Can Be Replaced + +The optimizer must be able to: + +* Identify an instruction +* Replace it with a new Instruction +* Or delete it + +Thus every Instruction must have an identity. It is also helpful for an Instruction to know which BB it lives in. + +# Define Data Structures + +The compiler must be able to operate and manipulate BBs, Instructions, Operands and Registers - so it is important to model these as data structures, and not encode them into a serialized form until the end. Some languages such as Lua directly encode instructions - this is only suitable for single pass compilers that do not do optimizations. + +# Def Use Chains + +Def use chains link Instructions to Registers - in SSA form, each Register can have a set of Instructions that use it. In non-SSA form, a Register may have multiple definitions and each definition its own set of Uses. Think how this will be handled. + +# Walking Basic Blocks + +Basic Blocks may need to be traversed in specific order such as Reverse Post Order (RPO). To support this auxiliary information has to be recorded for each Basic Block. + +# Dominator Trees and Dominance Frontiers + +The optimizer will most likely need to compute and maintain Dominator Trees and Dominance Frontiers. The algorithms that compute these and the data structures for maintaining this information will typically be attached to Basic Blocks. + +It is useful for Basic Blocks to be assigned a unique ID from a densely packed sequence of integers, this makes for a good way to generate names for the BBs, but also can be used to key into auxiliary data structures. + +```java +public class BasicBlock { + /** + * Unique BB ID + */ + int bid; + + /** + * The preorder traversal number. + */ + int pre; + /** + * The depth of the BB in the dominator tree + */ + int domDepth; + /** + * Reverse post order traversal number. + */ + int rpo; + /** + * Immediate dominator is the closest strict dominator. + */ + BasicBlock idom; + /** + * Nodes for whom this node is the immediate dominator, + * thus the dominator tree. + */ + List dominatedChildren; + /** + * Dominance frontier + */ + Set dominationFrontier; +} +``` diff --git a/docs/ir-virtual-registers.md b/docs/ir-virtual-registers.md new file mode 100644 index 0000000..827c179 --- /dev/null +++ b/docs/ir-virtual-registers.md @@ -0,0 +1,168 @@ +# Introduction + +Compilers often use named slots to represent locations where a value will be stored. These named slots are virtual in the sense that they model an abstract machine with unlimited registers. However these virtual registers are eventually mapped to a CPU register or to a location in the function's stack frame. + +In our compiler we use the term Register to denote such a virtual slot. + +Instructions can define a Register, and/or use one or more Registers. Defining means assigning a value to a Register. + +# Design Considerations + +There are several requirements we need to meet when deciding how to represent these virtual registers. + +## Each Source Variable Declaration Gets New Register + +The first requirement is that every variable declared in a function must get a unique slot. In the source language a variable name may be reused in multiple scopes, but from a compiler's perspective, each variable declaration is unique, because each has a unique lifetime and type. + +Consider this example: + +``` +func foo(x: Int) +{ + if (x == 0) + { + var i = 0 + } + if (x == 1) + { + var i = 1 + } +} +``` +The variable `i` has two defined instances, each has a different lifetime. From a compiler's standpoint, each instance of `i` must be mapped to a distinct virtual register. + +## Each Virtual Register Eventually Gets Assigned To A Potentially Shared Physical Slot + +The second requirement is that although each virtual register is unique - multiple virtual registers may get mapped to the same physical slot eventually. If we are targeting a CPU then this final location may be a CPU register or a memory location in the function's stack frame. In optvm module, we target an abstract virtual machine, a VM that is. This abstract machine supports unlimited number of virtual registers per function. Each such register is a slot in the function's stack frame. We call this a `frameSlot` in the implementation. + +So in the above example, after compiling the two virtual registers that represent `i` may end up pointing to the same `frameSlot`. + +| source variable | virtual register ID | frame slot | +| --------------- | ------------------- | ---------- | +| i | 1 | 0 | +| i | 2 | 0 | + +Why does this happen? For efficiency and better use of memory - if two variables do not overlap in their lifetimes, they can share the same slot. More about this when we discuss Register Allocation, in a future write-up. + +## In SSA Form A Register May Be Versioned + +Consider this example: + +``` +func foo(x: Int)->Int +{ + var y = 0 + if (x == 1) + { + y = 5 + } + return y +} +``` + +Since this function has a single declaration of the variable `y` initially this variable gets a unique virtual Register slot. + +But after SSA form this function looks something like this: + +``` +func foo(x: Int)->Int +{ + var y = 0 + var y1: Int + if (x == 1) + { + y1 = 5 + } + var y2 = phi(y,y1) + return y2 +} +``` + +We now have the original virtual Register `y` but also two new versions of this: `y1` and `y2`. + +The requirement is that we need to be able to tell that these are all versions of `y`. + +To support this we use a specialized type of virtual Register called SSARegister. Here is how this might look for above example: + +| name | register type | virtual register ID | frame slot | original Reg Number | SSA version | +| ------ | -------------- | ------------------- | ---------- | ------------------- | ----------- | +| y | Virtual | 1 | 1 | NA | NA | +| y1 | SSA | 2 | -1 | 1 | 1 | +| y2 | SSA | 3 | -1 | 1 | 2 | + +Observe following: + +* The name of the SSA register is a concatenation of the original name with SSA version. +* The SSA register's original Reg Number refers back to the register ID for `y`. +* The frame slot is set to -1 on SSA registers - SSA registers are converted to regular registers when the compiler exits SSA. See below on life cycle of the frame slot. +* The register ID is always unique per register. + +The compiler can treat each Register as unique by referring to the register ID, or ignore versions and treat all three registers as part of the same underlying name. + +### Life Cycle of the Frame Slot + +| Stage | Register type | Frame Slot | +| -------- | ------------- | ---------- | +| Pre-SSA | Virtual | Same as Register ID | +| SSA | Virtual | Same as Register ID | +| SSA | SSA Register | -1 | +| Post Register Allocation | Virtual | Slot assigned by Register Allocator | + +## Each Register Has Unique Integer ID + +It is useful to assign each Register an ID from a densely packed sequence of integers. This enables using the Register's ID as a bit in a bitset for various calculations, for example, in the Liveness calculation or building of Interference Graph. + +## Def Use Chains + +If a compiler only ever uses SSA form then we can attach the Def-Use chain for each Register to the Register itself. + +In the EeZee compiler we would like to support optimizations on non-SSA form as well as SSA form. Hence we cannot assume that there will only be a single definition for a Register. We therefore maintain Def Use Chains independently of Registers. + +# Comparison with LLVM + +LLVM IR has a similar concept to our Register - its called a Value. But there are some differences: + +* LLVM IR is always SSA. +* LLVM Instructions are also Values, whereas in EeZee compiler, Instructions and Registers do not share a common base type. +* Actually, LLVM uses Value as the base type for many different types of entities. + +# Comparison with Simple + +In Simple, which is a Sea of Nodes implementation, instructions are represented as Nodes. Some instructions +result in values. An instance of a Node represents the value of the Instruction, the class of the Node gives the operation type of the Instruction. Nodes are always SSA, therefore, Simple maintains Def Use chains directly on Nodes. + +# Implementation Details + +The class definitions are shown below. Some fields have been excluded for clarity: + +```java +public class Register { + public final int id; + protected final String name; + protected int frameSlot; + + public int nonSSAId() { + return id; + } + + public static class SSARegister extends Register { + public final int ssaVersion; + public final int originalRegNumber; + public SSARegister(Register original, int id, int version) { + super(id, original.name+"_"+version, original.type, -1); + this.originalRegNumber = original.id; + this.ssaVersion = version; + } + @Override + public int nonSSAId() { + return originalRegNumber; + } + } +} +``` + +* The method named `nonSSAId()` is used by the compiler when it needs to ignore the SSA version. + +# See Also + +* [IR Design - Instructions](https://github.com/CompilerProgramming/ez-lang/wiki/IR-Design-%E2%80%90-Instructions) \ No newline at end of file From 31d80a50e24a8ebe2f3eb84085fa852ff0d4fa8c Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:45:13 +0100 Subject: [PATCH 08/15] Add ssa construction doc --- docs/ir-design-instructions.md | 24 ++++++++++++++++++------ docs/ir-virtual-registers.md | 7 ++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/ir-design-instructions.md b/docs/ir-design-instructions.md index 53c31c9..8e415f5 100644 --- a/docs/ir-design-instructions.md +++ b/docs/ir-design-instructions.md @@ -1,6 +1,6 @@ This is part of the series that discusses what you must think about when deciding how to represent IRs. -* [Part 1 - Virtual Registers](https://github.com/CompilerProgramming/ez-lang/wiki/Virtual-Registers) +* [Part 1 - Virtual Registers](ir-virtual-registers.md) # Linear IR @@ -8,7 +8,7 @@ The discussion here is primarily for linear Intermediate Representations. By lin # Organize Instructions Into Basic Blocks -For many algorithms we need to operate on the control flow graph. It is important to organize the instructions into Basic Blocks, where each BB encapsulates instructions that are executed sequentially. The final instruction in the BB must a jump to some other BB. +For many algorithms we need to operate on the control flow graph. It is important to organize the instructions into Basic Blocks, where each BB encapsulates instructions that are executed sequentially. In this implementation, non-exit blocks are normally terminated by a jump or branch to another BB. ```java public class BasicBlock { @@ -30,12 +30,16 @@ public class BasicBlock { # Instruction Operands -Instructions deal with values, some of those may be Virtual Registers, some Constants, and even Basic Blocks, for example in jump Instructions. We therefore create a type called Operand that represents values handled by an Instruction. Operands can have different subtypes in an OO design (or an Operand can be a union or sumtype in non-OO languages) - where each subtype is for a specific type of value. +Instructions deal with values, some of those may be Virtual Registers and some Constants. We therefore create a type called Operand that represents values handled by an Instruction. Operands can have different subtypes in an OO design (or an Operand can be a union or sumtype in non-OO languages) - where each subtype is for a specific type of value. + +Control-flow targets are modeled separately. For example, `Instruction.Jump` and `Instruction.ConditionalBranch` store their target `BasicBlock`s as instruction fields, while register and constant values appear in the instruction's operand list. ```java class Operand {} class RegisterOperand extends Operand {} -class ConstantOperand extends Operand {} +class IntConstantOperand extends Operand {} +class FloatConstantOperand extends Operand {} +class NullConstantOperand extends Operand {} ``` # Instructions Define and Use Registers @@ -47,7 +51,7 @@ From the optimization point of view, it is important to model the Instruction in * Replace a register that is defined * Replace a specific use of a Register - or replace all uses - a Register may be replaced by another Register or by something else such as a Constant. -Whether an Instruction defines a Register and/or uses Values including Registers depends on the Instruction type. So each Instruction type sets its own requirements, however, all Instructions satisfy the interface requirements above, so that the optimizer can treat each instruction uniformly. +Whether an Instruction defines a Register and/or uses Values including Registers depends on the Instruction type. So each Instruction type sets its own requirements. Most non-phi instructions satisfy the interface requirements above, so that optimizers can treat them uniformly. Phi instructions and temporary parallel-copy instructions are deliberately special-cased; see below. ```java class Instruction { @@ -64,7 +68,7 @@ class Instruction { // Replace specific use boolean replaceUse(Register oldUse, Register newUse); // Replace specific register with constant - void replaceWithConstant(Register register, ConstantOperand constantOperand); + void replaceUseWithConstant(Register register, Operand constantOperand); } ``` @@ -72,6 +76,8 @@ class Instruction { Phi instructions also define a Register and use one or more Registers, but Phis do not have the same semantics as other instructions in many situations, such as when computing Liveness. So a consideration is how to model Phi instructions, whether they obey the general interface or have their own specialized interface. In EeZee language we give Phi instructions their own interface, which is similar to the general instruction but ensures that Phis cannot be erroneously manipulated by the optimizer. This has a trade off because there are scenarios where Phis do not require the special treatment. +In the implementation, `Phi.def()`, `Phi.replaceDef()`, `Phi.replaceUses()`, and `Phi.replaceUse()` throw `UnsupportedOperationException`, and `Phi.uses()` returns an empty list. Code that needs phi definitions or inputs must use the phi-specific methods. + ```java class Phi extends Instruction { // def = value @@ -85,10 +91,16 @@ class Phi extends Instruction { boolean isRegisterInput(int i); Register[] inputRegisters(); void replaceInput(int i, Register newReg); + boolean replaceInput(Register oldReg, Register newReg); + void addInput(Register register); void removeInput(int i); } ``` +# Parallel Copies Are Temporary + +SSA destruction can temporarily introduce `ParallelCopyInstruction`. Like phi nodes, parallel copies do not support the ordinary def/use interface. They are sequenced into ordinary move instructions before the pass finishes. + # Instructions Can Be Replaced The optimizer must be able to: diff --git a/docs/ir-virtual-registers.md b/docs/ir-virtual-registers.md index 827c179..9156849 100644 --- a/docs/ir-virtual-registers.md +++ b/docs/ir-virtual-registers.md @@ -94,7 +94,7 @@ Observe following: * The name of the SSA register is a concatenation of the original name with SSA version. * The SSA register's original Reg Number refers back to the register ID for `y`. -* The frame slot is set to -1 on SSA registers - SSA registers are converted to regular registers when the compiler exits SSA. See below on life cycle of the frame slot. +* The frame slot is set to -1 on SSA registers when they are created. Exiting SSA removes phi nodes and inserts copies, but it does not convert `SSARegister` objects back into plain `Register` objects. Register allocation later assigns frame slots to the registers that remain in the IR. * The register ID is always unique per register. The compiler can treat each Register as unique by referring to the register ID, or ignore versions and treat all three registers as part of the same underlying name. @@ -106,7 +106,8 @@ The compiler can treat each Register as unique by referring to the register ID, | Pre-SSA | Virtual | Same as Register ID | | SSA | Virtual | Same as Register ID | | SSA | SSA Register | -1 | -| Post Register Allocation | Virtual | Slot assigned by Register Allocator | +| After exiting SSA | Virtual and SSA registers that remain in the IR | Existing slots are unchanged until register allocation | +| Post Register Allocation | Any register that remains in the IR | Slot assigned by Register Allocator | ## Each Register Has Unique Integer ID @@ -165,4 +166,4 @@ public class Register { # See Also -* [IR Design - Instructions](https://github.com/CompilerProgramming/ez-lang/wiki/IR-Design-%E2%80%90-Instructions) \ No newline at end of file +* [IR Design - Instructions](ir-design-instructions.md) From 5fc7d097d7d774ab3d6c0e6be209fa7a08e733b5 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:45:36 +0100 Subject: [PATCH 09/15] Add IR overview doc --- docs/ir-overview.rst | 208 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 docs/ir-overview.rst diff --git a/docs/ir-overview.rst b/docs/ir-overview.rst new file mode 100644 index 0000000..e1ac24a --- /dev/null +++ b/docs/ir-overview.rst @@ -0,0 +1,208 @@ +============================ +Intermediate Representations +============================ + +An input program in the source language may go through many intermediate representations within +a compiler before it is in a form ready for execution. + +One of the first such intermediate representations that we have seen is the +the Abstract Syntax Tree (AST), which is mainly concerned with the grammar of the source language. + +From the AST, we generate a different kind of intermediate representation, one that is more amenable +to the manipulations required during optimization and execution. There are many such representations; we will +limit ourselves to the following. + +* Stack based IR +* Register based IR +* Sea of Nodes IR + +Stack-Based IR +============== + +The stack based IR encodes stack operations as part of the intermediate representation. Lets look at a simple +example:: + + func foo(n: Int)->Int { + return n+1; + } + +Produces:: + + L0: + load 0 + pushi 1 + addi + jump L1 + L1: + +The stack based IR is so called because many of the intructions in the IR push and pop values to/from an evaluation stack at +runtime. Above for example, we have the following instructions: + +* ``load 0`` - this pushes the value of the input parameter ``n`` to the stack. The ``0`` here identifies the location of the variable ``n``. +* ``pushi 1`` - pushes the constant ``1`` to the stack. +* ``addi`` - pops the two topmost values on the stack, and computes the sum and pushes this to the stack + +So at the end of the program we are left with the sum of ``n+1`` on the stack, and this forms the return +value of the function. + +In this IR, control flow can be represented either using labels and branching instructions, or by grouping +instructions into basic blocks, and linking basic blocks through jump instructions. These two approaches are +equivalent, you can think of a label as indicating the start of a basic block, and a jump as ending +a basic block. + +The idea is that inside a basic block, instructions execute linearly one after the other. +Each basic block ends with a branching instruction, something like a goto or a conditional jump. + +Here is a simple example of input source code and the IR you might see:: + + func foo()->Int + { + return 1 == 1 && 2 == 2 + } + +This results in IR that may look like this:: + + L0: + pushi 1 + pushi 1 + eq + cbr L2 L3 + L2: + pushi 2 + pushi 2 + eq + jump L4 + L3: + pushi 0 + jump L4 + L4: + jump L1 + L1: + +Each basic block begins with a label, which is just the unique name of the block. + +* The ``jump`` instruction transfers control from a basic block to another. +* The ``cbr`` instruction is the conditional branch. It consumes the top most value from the stack, + and if this value is true (in this case, a non-zero value), then control is transferred + to the first block, else to the second block. +* The ``eq`` instruction pops the two topmost values from the stack, compares them and pushes a result: + ``1`` for true or ``0`` for false. + +Advantages +---------- +* The IR is compact to represent in stored form as most instructions do not have operands. + This is a reason why many languages choose to encode their compiled code in + this form. Examples are Java, C#, Web Assembly. +* The IR can be executed easily by an Interpreter. +* Relatively easy to generate IR from an AST. + +Disadvantages +------------- +* Not easy to implement optimizations. +* For a reader it is hard to trace values as they flow through instructions, + as it requires tracking them through a conceptual stack. +* Harder to analyze the IR, although there are methods available to do so. + +Examples +-------- +* `Example implementation in EeZee Programming Language `_. +* `Java Specifications `_. +* `Web Assembly Specifications `_. + +Register Based IR or Three-Address IR +===================================== + +This intermediate representation uses named slots called virtual registers in the instruction when referencing +values. Lets look at the same example we saw above:: + + func foo(n: Int)->Int { + return n+1; + } + +Produces:: + + L0: + %t1 = n+1 + ret %t1 + goto L1 + L1: + +The instructions above are as follows: + +* ``%t1 = n+1`` - is a typical three-address instruction of the form ``result = value1 operator value2``. The name ``%t1`` + refers to a temporary, whereas ``n`` refers to the input argument ``n``. Both of these names are virtual registers. +* ``ret %t1`` - is the return instruction, in this instance it references the temporary. + +The virtual registers in the IR are so called because they do not map to real registers in the target physical machine. +Instead these are just named slots in the abstract machine responsible for executing the IR. Typically, the abstract machine +will assign each virtual register a unique location in its stack frame. So we still end up using the function's +stack frame, but the IR references locations within the stack frame directly using these virtual names, rather than implicitly +through push and pop instructions. During optimization some of the virtual registers will end up in real hardware registers. + +Control flow is represented the same way as for the stack IR. Revisiting the same source example from above, we get following +IR:: + + L0: + %t0 = 1==1 + if %t0 goto L2 else goto L3 + L2: + %t0 = 2==2 + goto L4 + L3: + %t0 = 0 + goto L4 + L4: + ret %t0 + goto L1 + L1: + + +Advantages +---------- +* Readability: the flow of values is easier to trace, whereas with a stack IR you need to conceptualize a stack somewhere, + and track values being pushed and popped. +* Fewer instructions are needed compared to stack IR. +* The IR can be executed easily by an Interpreter. +* Most optimization algorithms can be applied to this form of IR. +* The IR can represent Static Single Assignment (SSA) in a natural way. + +Disadvantages +------------- +* Each instruction has operands, hence representing the IR in serialized form takes more space. +* Harder to generate the IR during compilation. + +Examples +-------- +* `Example basic register IR in EeZee Programming Language `_. +* `Example register IR including SSA form and optimizations in EeZee Programming Language `_. +* `LLVM instruction set `_. +* `Android Dalvik IR `_. + +Sea of Nodes IR +=============== +The final example we will look at is known as the Sea of Nodes IR. + +This IR is quite different from the IRs we described above. + +The key features of this IR are: + +* Instructions are NOT organized into Basic Blocks - instead, intructions form a graph, where + each instruction has as its inputs the definitions it uses. +* Instructions that produce data values are not directly bound to a Basic Block, instead they "float" around, + the order being defined purely in terms of the dependencies between the instructions. +* Control flow is represented in a similar way, and control flows between control flow + instructions. Dependencies between data instructions and control intructions occur at few well + defined places. +* The IR as described above cannot be readily executed, because to execute the IR, the instructions + must be scheduled; you can think of this as a process that puts the instructions into a traditional + Basic Block IR as described earlier. + +Describing Sea of Nodes IR is quite involved. For now, I direct you to the `Simple project `_; this +is an ongoing effort to explain the Sea of Nodes IR representation and how to implement it. + +Beyond how the IR is represented, the main benefits of the Sea of Nodes IR are that: + +* It is an SSA IR +* Various optimizations such as peephole optimizations, value numbering and common subexpressions elimination, + dead code elimination, occur as the IR is built. +* The SoN IR can generate optimized code quickly, suitable for Just-In-Time (JIT) compilers. From 254ff5eb6275c81df370d0316d123e3b5b6c2395 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 16:57:40 +0100 Subject: [PATCH 10/15] Add IR overview doc --- docs/ir-overview.rst | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/ir-overview.rst b/docs/ir-overview.rst index e1ac24a..49b531d 100644 --- a/docs/ir-overview.rst +++ b/docs/ir-overview.rst @@ -6,7 +6,7 @@ An input program in the source language may go through many intermediate represe a compiler before it is in a form ready for execution. One of the first such intermediate representations that we have seen is the -the Abstract Syntax Tree (AST), which is mainly concerned with the grammar of the source language. +Abstract Syntax Tree (AST), which is mainly concerned with the grammar of the source language. From the AST, we generate a different kind of intermediate representation, one that is more amenable to the manipulations required during optimization and execution. There are many such representations; we will @@ -19,7 +19,7 @@ limit ourselves to the following. Stack-Based IR ============== -The stack based IR encodes stack operations as part of the intermediate representation. Lets look at a simple +The stack based IR encodes stack operations as part of the intermediate representation. Let's look at a simple example:: func foo(n: Int)->Int { @@ -35,7 +35,7 @@ Produces:: jump L1 L1: -The stack based IR is so called because many of the intructions in the IR push and pop values to/from an evaluation stack at +The stack based IR is so called because many of the instructions in the IR push and pop values to/from an evaluation stack at runtime. Above for example, we have the following instructions: * ``load 0`` - this pushes the value of the input parameter ``n`` to the stack. The ``0`` here identifies the location of the variable ``n``. @@ -51,7 +51,7 @@ equivalent, you can think of a label as indicating the start of a basic block, a a basic block. The idea is that inside a basic block, instructions execute linearly one after the other. -Each basic block ends with a branching instruction, something like a goto or a conditional jump. +Each non-exit basic block normally ends with a branching instruction, something like a goto or a conditional jump. Here is a simple example of input source code and the IR you might see:: @@ -105,7 +105,7 @@ Disadvantages Examples -------- -* `Example implementation in EeZee Programming Language `_. +* `Example implementation in EeZee Programming Language <../stackvm>`_. * `Java Specifications `_. * `Web Assembly Specifications `_. @@ -113,7 +113,7 @@ Register Based IR or Three-Address IR ===================================== This intermediate representation uses named slots called virtual registers in the instruction when referencing -values. Lets look at the same example we saw above:: +values. Let's look at the same example we saw above:: func foo(n: Int)->Int { return n+1; @@ -133,11 +133,10 @@ The instructions above are as follows: refers to a temporary, whereas ``n`` refers to the input argument ``n``. Both of these names are virtual registers. * ``ret %t1`` - is the return instruction, in this instance it references the temporary. -The virtual registers in the IR are so called because they do not map to real registers in the target physical machine. -Instead these are just named slots in the abstract machine responsible for executing the IR. Typically, the abstract machine -will assign each virtual register a unique location in its stack frame. So we still end up using the function's -stack frame, but the IR references locations within the stack frame directly using these virtual names, rather than implicitly -through push and pop instructions. During optimization some of the virtual registers will end up in real hardware registers. +The virtual registers in the IR are so called because they are not necessarily registers in a target physical machine. +In the register VM and optimizing VM modules, they are named slots in the abstract machine responsible for executing the IR. The interpreter reads and writes these values through frame slots in the function's stack frame, so the IR references locations directly using virtual names rather than implicitly through push and pop instructions. + +The optimizing VM can run register allocation to map many virtual registers onto a smaller set of VM frame slots when their lifetimes do not overlap. That is different from native code generation, where a later backend may map values to real hardware registers or stack locations. Control flow is represented the same way as for the stack IR. Revisiting the same source example from above, we get following IR:: @@ -173,8 +172,8 @@ Disadvantages Examples -------- -* `Example basic register IR in EeZee Programming Language `_. -* `Example register IR including SSA form and optimizations in EeZee Programming Language `_. +* `Example basic register IR in EeZee Programming Language <../registervm>`_. +* `Example register IR including SSA form and optimizations in EeZee Programming Language <../optvm>`_. * `LLVM instruction set `_. * `Android Dalvik IR `_. @@ -186,12 +185,12 @@ This IR is quite different from the IRs we described above. The key features of this IR are: -* Instructions are NOT organized into Basic Blocks - instead, intructions form a graph, where +* Instructions are NOT organized into Basic Blocks - instead, instructions form a graph, where each instruction has as its inputs the definitions it uses. * Instructions that produce data values are not directly bound to a Basic Block, instead they "float" around, the order being defined purely in terms of the dependencies between the instructions. * Control flow is represented in a similar way, and control flows between control flow - instructions. Dependencies between data instructions and control intructions occur at few well + instructions. Dependencies between data instructions and control instructions occur at few well defined places. * The IR as described above cannot be readily executed, because to execute the IR, the instructions must be scheduled; you can think of this as a process that puts the instructions into a traditional From 8a0aeb588f31f8c159fc38525bf2bdbf72646672 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 17:29:25 +0100 Subject: [PATCH 11/15] Add description of ssa destruction --- docs/ssa-destruction-briggs.md | 265 +++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 docs/ssa-destruction-briggs.md diff --git a/docs/ssa-destruction-briggs.md b/docs/ssa-destruction-briggs.md new file mode 100644 index 0000000..599cd51 --- /dev/null +++ b/docs/ssa-destruction-briggs.md @@ -0,0 +1,265 @@ +# SSA Destruction Using Briggs + +This document describes the algorithm implemented by +`com.compilerprogramming.ezlang.compiler.ExitSSABriggs`. + +## Goal + +The pass converts a `CompiledFunction` from SSA form back to ordinary +three-address form. It removes phi nodes by inserting the copy operations that +the phi nodes implied on predecessor edges. + +The implementation is based on the SSA destruction algorithm described by +Preston Briggs et al. in "Practical Improvements to the Construction and +Destruction of Static Single Assignment Form". The original algorithm has two +main parts: + +- Walk the dominator tree, replacing uses with any currently active temporary + names. +- At each block, schedule the copies needed for phi nodes in successor blocks, + avoiding the lost-copy and swap problems. + +## Setup + +`ExitSSABriggs` starts with an SSA function and computes the information needed +by the scheduler: + +```text +require function.isSSA +compute liveness +tree <- DominatorTree(function.entry) +initialize one name stack per register +insertCopies(function.entry) +remove all phi instructions +function.isSSA <- false +``` + +Liveness is required because the scheduler must know whether overwriting a phi +destination would destroy a value that is still live out of the current +predecessor block. + +The dominator tree determines the order of the recursive walk. Name stacks are +used only for temporary names created to preserve live phi destinations. When a +temporary is pushed for a register, dominated instructions that use that register +are rewritten to use the temporary until the walk leaves that dominated region. + +## Dominator-Tree Walk + +The method `insertCopies(block)` corresponds to the original +`insert_copies(block)` routine. + +For each block: + +1. Replace ordinary instruction uses. + + ```text + for each instruction i in block + replace each register use u with top(stacks[u]) when the stack is not empty + ``` + + Phi instructions are skipped here because their inputs are handled as + predecessor-edge copies by `scheduleCopies`. + +2. Schedule copies for successor phis. + + ```text + scheduleCopies(block, pushed) + ``` + + `pushed` records the register ids whose stacks were extended while handling + this block. + +3. Recurse into dominated children. + + ```text + for each child c of block in the dominator tree + insertCopies(c) + ``` + +4. Pop names pushed in this block. + + ```text + for each name in pushed + pop(stacks[name]) + ``` + +This preorder traversal makes any temporary introduced for a phi destination +visible to the blocks dominated by the phi's block, and only to those blocks. + +## Copy Scheduling + +`scheduleCopies(block, pushed)` implements the three scheduling passes from the +Briggs description. For the current predecessor `block`, it looks at every +successor `s` and every phi in `s`. If `block` is predecessor `j` of `s`, then +the phi's `j`th input is the source value that must be copied on this edge. + +For a phi: + +```text +dest = phi result in successor s +src = phi input selected by predecessor index j +``` + +the conceptual edge copy is: + +```text +dest <- src +``` + +In the implementation this pair is stored as a `CopyItem`, along with the +successor block where the phi is defined. + +### Pass 1: Initialize Data Structures + +Build the set of copies that this predecessor block must provide: + +```text +copySet <- empty +map <- empty +usedByAnother <- empty + +for each successor s of block + j <- s.whichPred(block) + + for each phi in s + dest <- phi.value + src <- phi.input(j) + + copySet <- copySet union {(src, dest, s)} + map[dest] <- dest + + if src is a register + map[src] <- src + usedByAnother[src] <- true +``` + +`map` tracks the current location of each register name as copies are emitted. +Initially, every register maps to itself. Later, when a copy or temporary changes +where a value lives, `map` is updated. + +`usedByAnother` marks register sources that appear as inputs to other phi-copy +pairs. Constants do not participate in dependency cycles, so the implementation +records them in `copySet` but not in `usedByAnother` or `map`. + +### Pass 2: Seed The Worklist + +Copies whose destination is not needed as a source by another pending copy can be +emitted first: + +```text +workList <- empty + +for each copy (src, dest) in copySet + if dest is not in usedByAnother + move copy from copySet to workList +``` + +These copies cannot destroy a value that another pending copy still needs. + +### Pass 3: Emit Copies + +The scheduler repeatedly drains the worklist. For each copy `(src, dest)`: + +1. Preserve a live destination if needed. + + ```text + if dest is live-out of block + t <- new temp + insert "t = dest" after the phi that defines dest in the successor block + push t on stacks[dest] + remember dest in pushed + ``` + + This is the lost-copy protection. If `dest` already holds a value that is + live after this edge, overwriting it in the predecessor would be incorrect. + Saving `dest` to a temporary at the phi definition and pushing that temporary + makes later dominated uses read the preserved value. + +2. Insert the edge copy at the end of the predecessor block, before its final + branch: + + ```text + if src is a register + insert "dest = map[src]" at the end of block + map[src] <- dest + else + insert "dest = src" at the end of block + ``` + + The code supports integer, float, and null constants as phi inputs. + +3. If the source register is itself the destination of another pending copy, + move that pending copy to the worklist: + + ```text + if src names a destination in copySet + move that copy from copySet to workList + ``` + + This exposes the next copy whose dependency has just been satisfied. + +If the worklist becomes empty but `copySet` is still non-empty, the remaining +copies form a cycle, such as a swap: + +```text +a <- b +b <- a +``` + +To break the cycle, the implementation chooses one pending copy and saves its +destination at the end of the current block: + +```text +t <- dest +map[dest] <- t +move the chosen copy to workList +``` + +The saved destination can now be overwritten safely, and subsequent copies read +the old value through `map[dest]`. + +## Insertion Points + +There are two insertion points: + +- Edge copies are inserted at the end of the predecessor block, immediately + before the final branch instruction. +- Lost-copy preservation moves are inserted immediately after the phi node that + defines the destination in the successor block. + +This differs slightly from a literal edge-splitting formulation, but it has the +same effect for this IR because the predecessor index identifies which phi input +belongs to the outgoing edge. + +When a register edge copy is inserted just before a conditional branch, the +implementation also rewrites that branch if it still uses the copied source. +This local fix is not part of the Briggs pseudocode, but it keeps the branch +consistent with the newly inserted move sequence in this IR. + +## Final Phi Removal + +After the dominator-tree walk has scheduled every required copy, all phi +instructions are removed: + +```text +for each block in dominator-tree order + remove instructions that are Phi +``` + +The inserted moves now represent the data flow that the phi nodes previously +encoded. The pass then marks the function as no longer being in SSA form. + +## Why The Scheduler Is Needed + +Replacing each phi with naive predecessor copies can be wrong. Two classic +problems motivate Briggs' scheduler: + +- Lost copy: a copy overwrites a phi destination even though the old value is + still live and needed later. +- Swap problem: two or more copies depend on each other's old values, so any + simple linear order destroys one of the inputs. + +The live-out check handles lost copies by saving live destinations into +temporaries and rewriting dominated uses through the name stack. The cycle +handling handles swaps by saving one destination into a temporary and using +`map` to route later copies through that saved value. From 1a970e0b2a79592c0de6b6d2cd1919f23396c2ce Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 18:18:04 +0100 Subject: [PATCH 12/15] Refactor LatticeElement and move it to types module so that it is shared by both semantic and optvm modules --- .../SparseConditionalConstantPropagation.java | 182 +-------- .../ezlang/semantic/NullableAnalysis.java | 341 +---------------- .../ezlang/types/LatticeElement.java | 361 ++++++++++++++++++ 3 files changed, 374 insertions(+), 510 deletions(-) create mode 100644 types/src/main/java/com/compilerprogramming/ezlang/types/LatticeElement.java diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java index a2f4049..14e1829 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/SparseConditionalConstantPropagation.java @@ -2,9 +2,12 @@ import com.compilerprogramming.ezlang.exceptions.CompilerException; import com.compilerprogramming.ezlang.types.EZType; +import com.compilerprogramming.ezlang.types.LatticeElement; import java.util.*; +import static com.compilerprogramming.ezlang.types.LatticeElement.*; + /** * Implementation of Sparse Conditional Constant Propagation based on descriptions * in: @@ -203,7 +206,7 @@ private void replaceVarsWithConstants() { for (var register: valueLattice.getRegisters()) { var latticeElement = valueLattice.get(register); if (latticeElement.isReplaceableConstant()) { - var constant = latticeElement.asOperand(register.type); + var constant = asOperand(latticeElement, register.type); var defUseChain = this.ssaEdges.get(register); if (defUseChain == null) continue; @@ -221,149 +224,6 @@ private void replaceVarsWithConstants() { } } - static final byte F_TOP = 0; // no usable fact yet - - static final byte F_REF_TOP = 1; // known reference type, value unknown - static final byte F_REF_NOT_NULL = 2; - static final byte F_REF_NULL = 3; - static final byte F_REF_BOTTOM = 4; // maybe null - - static final byte F_INT_TOP = 5; // known int type, value unknown - static final byte F_INT_ZERO = 6; - static final byte F_INT_NONZERO_CONST = 7; - static final byte F_INT_NONZERO_VARYING = 8; - static final byte F_INT_BOTTOM = 9; // may be zero or non-zero - - static final byte F_FLT_TOP = 10; // known float type, value unknown - static final byte F_FLT_CONST = 11; - static final byte F_FLT_BOTTOM = 12; // non-const float - - static final byte F_BOTTOM = 13; // impossible / contradiction - - // Associated with each register - static final class LatticeElement { - private byte kind; - private long intValue; - private double floatValue; - - LatticeElement(byte kind) { this.kind = kind; } - LatticeElement(byte kind, long value) { this.kind = kind; this.intValue = value; } - LatticeElement(long value) { kind = F_INT_TOP; setIntValue(value); } - LatticeElement(double value) { kind = F_FLT_TOP; setFloatValue(value); } - LatticeElement copy() { var copy = new LatticeElement(kind, intValue); copy.floatValue = floatValue; return copy; } - void copyFrom(LatticeElement other) { kind = other.kind; intValue = other.intValue; floatValue = other.floatValue; } - - boolean isTrue() { return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; } - boolean isFalse() { return kind == F_INT_ZERO; } - boolean isIntegerConstant() { return kind == F_INT_ZERO || kind == F_INT_NONZERO_CONST; } - boolean isFloatConstant() { return kind == F_FLT_CONST; } - boolean isNullConstant() { return kind == F_REF_NULL; } - boolean isDefiniteReference() { return kind == F_REF_NULL || kind == F_REF_NOT_NULL; } - boolean isReplaceableConstant() { return isIntegerConstant() || isFloatConstant() || isNullConstant(); } - - boolean setIntValue(long value) { - byte old = kind; - if (kind == F_TOP || kind == F_INT_TOP) { intValue = value; kind = value == 0 ? F_INT_ZERO : F_INT_NONZERO_CONST; } - else if (kind == F_INT_ZERO && value != 0) kind = F_INT_BOTTOM; - else if (kind == F_INT_NONZERO_CONST && (value == 0 || value != intValue)) kind = F_INT_BOTTOM; - else if (!isInteger()) kind = F_BOTTOM; - return kind != old; - } - boolean setFloatValue(double value) { - byte old = kind; - if (kind == F_TOP || kind == F_FLT_TOP) { floatValue = value; kind = F_FLT_CONST; } - else if (kind == F_FLT_CONST && Double.compare(value, floatValue) != 0) kind = F_FLT_BOTTOM; - else if (!isFloat()) kind = F_BOTTOM; - return kind != old; - } - boolean meetWithTypeBottom(EZType type) { return meet(factBottomFromType(type)); } - - boolean meet(LatticeElement other) { - byte old = kind; - long oldInt = intValue; - double oldFloat = floatValue; - if (kind == F_TOP) { copyFrom(other); return changed(old, oldInt, oldFloat); } - if (kind == F_BOTTOM || other.kind == F_TOP) return false; - if (other.kind == F_BOTTOM) { kind = F_BOTTOM; return changed(old, oldInt, oldFloat); } - if (kind == other.kind) { - if (kind == F_INT_NONZERO_CONST && intValue != other.intValue) kind = F_INT_NONZERO_VARYING; - else if (kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0) kind = F_FLT_BOTTOM; - return changed(old, oldInt, oldFloat); - } - if (isReference(kind) && isReference(other.kind)) { kind = meetReference(other); return changed(old, oldInt, oldFloat); } - if (isInteger(kind) && isInteger(other.kind)) { meetInteger(other); return changed(old, oldInt, oldFloat); } - if (isFloat(kind) && isFloat(other.kind)) { meetFloat(other); return changed(old, oldInt, oldFloat); } - kind = F_BOTTOM; - return changed(old, oldInt, oldFloat); - } - private boolean changed(byte oldKind, long oldInt, double oldFloat) { return kind != oldKind || intValue != oldInt || Double.compare(floatValue, oldFloat) != 0; } - private byte meetReference(LatticeElement other) { - if (kind == F_REF_TOP) return other.kind; - if (other.kind == F_REF_TOP) return kind; - return switch (kind) { - case F_REF_NULL -> other.kind == F_REF_NULL ? F_REF_NULL : F_REF_BOTTOM; - case F_REF_NOT_NULL -> other.kind == F_REF_NOT_NULL ? F_REF_NOT_NULL : F_REF_BOTTOM; - case F_REF_BOTTOM -> F_REF_BOTTOM; - default -> F_BOTTOM; - }; - } - private void meetInteger(LatticeElement other) { - if (kind == F_INT_TOP) { copyFrom(other); return; } - if (other.kind == F_INT_TOP) return; - switch (kind) { - case F_INT_ZERO -> { if (other.kind != F_INT_ZERO) kind = F_INT_BOTTOM; } - case F_INT_NONZERO_CONST -> { - switch (other.kind) { - case F_INT_NONZERO_CONST -> { if (intValue != other.intValue) kind = F_INT_NONZERO_VARYING; } - case F_INT_NONZERO_VARYING -> kind = F_INT_NONZERO_VARYING; - case F_INT_ZERO, F_INT_BOTTOM -> kind = F_INT_BOTTOM; - } - } - case F_INT_NONZERO_VARYING -> { if (other.kind == F_INT_ZERO || other.kind == F_INT_BOTTOM) kind = F_INT_BOTTOM; } - case F_INT_BOTTOM -> { } - } - } - private void meetFloat(LatticeElement other) { - if (kind == F_FLT_TOP) { copyFrom(other); return; } - if (other.kind == F_FLT_TOP) return; - if (kind == F_FLT_CONST && (other.kind == F_FLT_BOTTOM || (other.kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0))) - kind = F_FLT_BOTTOM; - } - boolean isReference() { return isReference(kind); } - boolean isInteger() { return isInteger(kind); } - boolean isFloat() { return isFloat(kind); } - private static boolean isReference(byte kind) { return kind >= F_REF_TOP && kind <= F_REF_BOTTOM; } - private static boolean isInteger(byte kind) { return kind >= F_INT_TOP && kind <= F_INT_BOTTOM; } - private static boolean isFloat(byte kind) { return kind >= F_FLT_TOP && kind <= F_FLT_BOTTOM; } - - Operand asOperand(EZType type) { - if (kind == F_INT_ZERO) return new Operand.IntConstantOperand(0, type); - if (kind == F_INT_NONZERO_CONST) return new Operand.IntConstantOperand(intValue, type); - if (kind == F_FLT_CONST) return new Operand.FloatConstantOperand(floatValue, type); - if (kind == F_REF_NULL) return new Operand.NullConstantOperand(type); - throw new IllegalStateException("Lattice value is not a constant: " + this); - } - @Override - public String toString() { - return switch (kind) { - case F_TOP -> "T"; - case F_REF_TOP -> "ref"; - case F_INT_TOP -> "int"; - case F_FLT_TOP -> "flt"; - case F_REF_NOT_NULL -> "not-null"; - case F_REF_NULL -> "null"; - case F_REF_BOTTOM -> "maybe-null"; - case F_INT_BOTTOM -> "int*"; - case F_FLT_BOTTOM -> "flt*"; - case F_INT_ZERO -> "0"; - case F_INT_NONZERO_CONST -> Long.toString(intValue); - case F_INT_NONZERO_VARYING -> "non-zero"; - case F_FLT_CONST -> Double.toString(floatValue); - case F_BOTTOM -> "⊥"; - default -> throw new CompilerException("Unknown lattice kind: " + kind); - }; - } - } // A CFG edge static final class FlowEdge { BasicBlock source; @@ -555,35 +415,13 @@ private static LatticeElement factFromOperand(Operand operand) { throw new IllegalStateException("Unexpected constant operand: " + operand); } - private static LatticeElement factTopFromType(EZType type) { - if (type instanceof EZType.EZTypeInteger) - return new LatticeElement(F_INT_TOP); - if (type instanceof EZType.EZTypeFloat) - return new LatticeElement(F_FLT_TOP); - if (isReferenceType(type)) - return new LatticeElement(F_REF_TOP); - return new LatticeElement(F_TOP); - } - - private static LatticeElement factBottomFromType(EZType type) { - if (type instanceof EZType.EZTypeInteger) - return new LatticeElement(F_INT_BOTTOM); - if (type instanceof EZType.EZTypeFloat) - return new LatticeElement(F_FLT_BOTTOM); - if (type instanceof EZType.EZTypeNull) - return new LatticeElement(F_REF_NULL); - if (isReferenceType(type)) - return new LatticeElement(F_REF_BOTTOM); - return new LatticeElement(F_BOTTOM); + private static Operand asOperand(LatticeElement element, EZType type) { + if (element.kind == F_INT_ZERO) return new Operand.IntConstantOperand(0, type); + if (element.kind == F_INT_NONZERO_CONST) return new Operand.IntConstantOperand(element.intValue, type); + if (element.kind == F_FLT_CONST) return new Operand.FloatConstantOperand(element.floatValue, type); + if (element.kind == F_REF_NULL) return new Operand.NullConstantOperand(type); + throw new IllegalStateException("Lattice value is not a constant: " + element); } - - private static boolean isReferenceType(EZType type) { - return type instanceof EZType.EZTypeNullable || - type instanceof EZType.EZTypeArray || - type instanceof EZType.EZTypeStruct || - type instanceof EZType.EZTypeNull; - } - private static EZType arrayLoadType(Instruction.ArrayLoad arrayLoadInst) { EZType type = aggregateBaseType(operandType(arrayLoadInst.arrayOperand())); if (type instanceof EZType.EZTypeArray arrayType) diff --git a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java index b5f3041..8d98562 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -4,12 +4,15 @@ import com.compilerprogramming.ezlang.lexer.Token; import com.compilerprogramming.ezlang.parser.AST; import com.compilerprogramming.ezlang.types.EZType; +import com.compilerprogramming.ezlang.types.LatticeElement; import com.compilerprogramming.ezlang.types.Scope; import com.compilerprogramming.ezlang.types.Symbol; import com.compilerprogramming.ezlang.types.TypeDictionary; import java.util.*; +import static com.compilerprogramming.ezlang.types.LatticeElement.*; + /** * Input is a CFG over AST. the CFG should capture control flow that is * relevant for null analysis. @@ -71,327 +74,6 @@ else if (type instanceof EZType.EZTypeNullable) } } - static final byte F_TOP = 0; // no usable fact yet - - static final byte F_REF_TOP = 1; // known reference type, value unknown - static final byte F_REF_NOT_NULL = 2; - static final byte F_REF_NULL = 3; - static final byte F_REF_BOTTOM = 4; // maybe null - - static final byte F_INT_TOP = 5; // known int type, value unknown - static final byte F_INT_ZERO = 6; - static final byte F_INT_NONZERO_CONST = 7; - static final byte F_INT_NONZERO_VARYING = 8; - static final byte F_INT_BOTTOM = 9; // may be zero or non-zero - - static final byte F_FLT_TOP = 10; // known float type, value unknown - static final byte F_FLT_CONST = 11; - static final byte F_FLT_BOTTOM = 12; // non-const float - - static final byte F_BOTTOM = 13; // impossible / contradiction - - // Associated with each register - static final class LatticeElement { - public byte kind; - private long intValue; - private double floatValue; - - public LatticeElement(byte kind) { - this.kind = kind; - } - public LatticeElement(byte kind,long value) { - this.kind = kind; - this.intValue = value; - } - public LatticeElement(long value) { - kind = F_INT_TOP; - setIntValue(value); - } - public LatticeElement(double value) { - kind = F_FLT_TOP; - setFloatValue(value); - } - LatticeElement copy() { - LatticeElement copy = new LatticeElement(kind, intValue); - copy.floatValue = floatValue; - return copy; - } - - boolean isTrue() { - return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; - } - boolean isFalse() { - return kind == F_INT_ZERO; - } - boolean isIntegerConstant() { - return kind == F_INT_ZERO || kind == F_INT_NONZERO_CONST; - } - boolean isFloatConstant() { - return kind == F_FLT_CONST; - } - boolean setFloatValue(double value) { - byte prevKind = kind; - if (kind == F_FLT_TOP) { - this.floatValue = value; - this.kind = F_FLT_CONST; - } - else if (kind == F_FLT_CONST && Double.compare(value, floatValue) != 0) { - this.kind = F_FLT_BOTTOM; - } - else if (!isFloat()) { - throw new CompilerException("Cannot assign float value to non-float type"); - } - return kind != prevKind; - } - boolean setIntValue(long value) { - byte prevKind = kind; - if (kind == F_INT_TOP) { - this.intValue = value; - this.kind = value == 0 ? F_INT_ZERO : F_INT_NONZERO_CONST; - } - else if (kind == F_INT_ZERO && value != 0) { - this.kind = F_INT_BOTTOM; - } - else if (kind == F_INT_NONZERO_CONST && (value == 0 || value != intValue)) { - this.kind = F_INT_BOTTOM; - } - else if (!isInteger()) { - throw new CompilerException("Cannot assign integer value to reference type"); - } - return (kind != prevKind); - } - boolean meet(LatticeElement other) { - byte old = kind; - - // universal top/bottom - if (kind == F_TOP) { - copyFrom(other); - return true; - } - - if (kind == F_BOTTOM || other.kind == F_TOP) - return false; - - if (other.kind == F_BOTTOM) { - kind = F_BOTTOM; - return kind != old; - } - - // same value - if (kind == other.kind) { - if (kind == F_INT_NONZERO_CONST && - intValue != other.intValue) { - kind = F_INT_NONZERO_VARYING; - } - else if (kind == F_FLT_CONST && - Double.compare(floatValue, other.floatValue) != 0) { - kind = F_FLT_BOTTOM; - } - return kind != old; - } - - // reference lattice - if (isReference(kind) && isReference(other.kind)) { - kind = meetReference(other); - return kind != old; - } - - // integer lattice - if (isInteger(kind) && isInteger(other.kind)) { - meetInteger(other); - return kind != old; - } - - // float lattice - if (isFloat(kind) && isFloat(other.kind)) { - meetFloat(other); - return kind != old; - } - - // impossible - kind = F_BOTTOM; - return kind != old; - } - - private byte meetReference(LatticeElement other) { - - if (kind == F_REF_TOP) - return other.kind; - - if (other.kind == F_REF_TOP) - return kind; - - return switch (kind) { - - case F_REF_NULL -> - other.kind == F_REF_NULL - ? F_REF_NULL - : F_REF_BOTTOM; - - case F_REF_NOT_NULL -> - other.kind == F_REF_NOT_NULL - ? F_REF_NOT_NULL - : F_REF_BOTTOM; - - case F_REF_BOTTOM -> - F_REF_BOTTOM; - - default -> - F_BOTTOM; - }; - } - - private void meetInteger(LatticeElement other) { - - if (kind == F_INT_TOP) { - copyFrom(other); - return; - } - - if (other.kind == F_INT_TOP) - return; - - switch (kind) { - - case F_INT_ZERO -> { - if (other.kind != F_INT_ZERO) - kind = F_INT_BOTTOM; - } - - case F_INT_NONZERO_CONST -> { - switch (other.kind) { - - case F_INT_NONZERO_CONST -> { - if (intValue != other.intValue) - kind = F_INT_NONZERO_VARYING; - } - - case F_INT_NONZERO_VARYING -> - kind = F_INT_NONZERO_VARYING; - - case F_INT_ZERO, - F_INT_BOTTOM -> - kind = F_INT_BOTTOM; - } - } - - case F_INT_NONZERO_VARYING -> { - if (other.kind == F_INT_ZERO || - other.kind == F_INT_BOTTOM) - kind = F_INT_BOTTOM; - } - - case F_INT_BOTTOM -> { - } - } - } - private static boolean isReference(byte kind) { - return kind >= F_REF_TOP && kind <= F_REF_BOTTOM; - } - - private void meetFloat(LatticeElement other) { - if (kind == F_FLT_TOP) { - copyFrom(other); - return; - } - if (other.kind == F_FLT_TOP) - return; - if (kind == F_FLT_CONST && - (other.kind == F_FLT_BOTTOM || - (other.kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0))) { - kind = F_FLT_BOTTOM; - } - } - private static boolean isInteger(byte kind) { - return kind >= F_INT_TOP && kind <= F_INT_BOTTOM; - } - private static boolean isFloat(byte kind) { - return kind >= F_FLT_TOP && kind <= F_FLT_BOTTOM; - } - void copyFrom(LatticeElement other) { - LatticeElement copy = other.copy(); - this.kind = copy.kind; - this.intValue = copy.intValue; - this.floatValue = copy.floatValue; - } - boolean isTop() { - return kind == F_TOP; - } - - boolean isBottom() { - return kind == F_BOTTOM; - } - - boolean isReference() { - return isReference(kind); - } - - boolean isInteger() { - return isInteger(kind); - } - - boolean isFloat() { - return isFloat(kind); - } - - boolean isNull() { - return kind == F_REF_NULL; - } - - boolean isNotNull() { - return kind == F_REF_NOT_NULL; - } - - boolean isMaybeNull() { - return kind == F_REF_BOTTOM; - } - - boolean isZero() { - return kind == F_INT_ZERO; - } - - boolean isNonZero() { - return kind == F_INT_NONZERO_CONST || - kind == F_INT_NONZERO_VARYING; - } - @Override - public String toString() { - return switch (kind) { - case F_TOP -> "⊤"; - - case F_REF_TOP -> "ref"; - case F_REF_NOT_NULL -> "not-null"; - case F_REF_NULL -> "null"; - case F_REF_BOTTOM -> "maybe-null"; - - case F_INT_TOP -> "int"; - case F_INT_ZERO -> "0"; - case F_INT_NONZERO_CONST -> Long.toString(intValue); - case F_INT_NONZERO_VARYING -> "non-zero"; - case F_INT_BOTTOM -> "int*"; - - case F_FLT_TOP -> "float"; - case F_FLT_CONST -> Double.toString(floatValue); - case F_FLT_BOTTOM -> "float*"; - - case F_BOTTOM -> "⊥"; - - default -> throw new CompilerException("Unknown lattice kind: " + kind); - }; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - LatticeElement that = (LatticeElement) o; - return kind == that.kind && intValue == that.intValue && Double.compare(floatValue, that.floatValue) == 0; - } - - @Override - public int hashCode() { - return Objects.hash(kind, intValue, floatValue); - } - } static final class Lattice { final LatticeElement[] vars; @@ -651,23 +333,6 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { return new LatticeElement(F_BOTTOM); } - LatticeElement factFromType(EZType type) { - if (type != null) { - if (type instanceof EZType.EZTypeNullable) - return new LatticeElement(F_REF_BOTTOM); - else if (type instanceof EZType.EZTypeNull) - return new LatticeElement(F_REF_NULL); - else if (type instanceof EZType.EZTypeArray || - type instanceof EZType.EZTypeStruct) - return new LatticeElement(F_REF_NOT_NULL); - else if (type instanceof EZType.EZTypeInteger) - return new LatticeElement(F_INT_BOTTOM); - else if (type instanceof EZType.EZTypeFloat) - return new LatticeElement(F_FLT_BOTTOM); - } - return new LatticeElement(F_BOTTOM); - } - public void doAnalysis(FlowCFG.FlowGraph cfg) { Queue worklist = new ArrayDeque<>(); diff --git a/types/src/main/java/com/compilerprogramming/ezlang/types/LatticeElement.java b/types/src/main/java/com/compilerprogramming/ezlang/types/LatticeElement.java new file mode 100644 index 0000000..1d7e178 --- /dev/null +++ b/types/src/main/java/com/compilerprogramming/ezlang/types/LatticeElement.java @@ -0,0 +1,361 @@ +package com.compilerprogramming.ezlang.types; + +import com.compilerprogramming.ezlang.exceptions.CompilerException; + +import java.util.Objects; + +/** + * Shared lattice fact for value/nullability analyses. + */ +public final class LatticeElement { + public static final byte F_TOP = 0; // no usable fact yet + + public static final byte F_REF_TOP = 1; // known reference type, value unknown + public static final byte F_REF_NOT_NULL = 2; + public static final byte F_REF_NULL = 3; + public static final byte F_REF_BOTTOM = 4; // maybe null + + public static final byte F_INT_TOP = 5; // known int type, value unknown + public static final byte F_INT_ZERO = 6; + public static final byte F_INT_NONZERO_CONST = 7; + public static final byte F_INT_NONZERO_VARYING = 8; + public static final byte F_INT_BOTTOM = 9; // may be zero or non-zero + + public static final byte F_FLT_TOP = 10; // known float type, value unknown + public static final byte F_FLT_CONST = 11; + public static final byte F_FLT_BOTTOM = 12; // non-const float + + public static final byte F_BOTTOM = 13; // impossible / contradiction + + public byte kind; + public long intValue; + public double floatValue; + + public LatticeElement(byte kind) { + this.kind = kind; + } + + public LatticeElement(byte kind, long value) { + this.kind = kind; + this.intValue = value; + } + + public LatticeElement(long value) { + kind = F_INT_TOP; + setIntValue(value); + } + + public LatticeElement(double value) { + kind = F_FLT_TOP; + setFloatValue(value); + } + + public LatticeElement copy() { + var copy = new LatticeElement(kind, intValue); + copy.floatValue = floatValue; + return copy; + } + + public void copyFrom(LatticeElement other) { + kind = other.kind; + intValue = other.intValue; + floatValue = other.floatValue; + } + + public boolean isTrue() { + return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; + } + + public boolean isFalse() { + return kind == F_INT_ZERO; + } + + public boolean isIntegerConstant() { + return kind == F_INT_ZERO || kind == F_INT_NONZERO_CONST; + } + + public boolean isFloatConstant() { + return kind == F_FLT_CONST; + } + + public boolean isNullConstant() { + return kind == F_REF_NULL; + } + + public boolean isDefiniteReference() { + return kind == F_REF_NULL || kind == F_REF_NOT_NULL; + } + + public boolean isReplaceableConstant() { + return isIntegerConstant() || isFloatConstant() || isNullConstant(); + } + + public boolean setIntValue(long value) { + byte oldKind = kind; + long oldInt = intValue; + double oldFloat = floatValue; + if (kind == F_TOP || kind == F_INT_TOP) { + intValue = value; + kind = value == 0 ? F_INT_ZERO : F_INT_NONZERO_CONST; + } + else if (kind == F_INT_ZERO && value != 0) { + kind = F_INT_BOTTOM; + } + else if (kind == F_INT_NONZERO_CONST && (value == 0 || value != intValue)) { + kind = F_INT_BOTTOM; + } + else if (!isInteger()) { + kind = F_BOTTOM; + } + return changed(oldKind, oldInt, oldFloat); + } + + public boolean setFloatValue(double value) { + byte oldKind = kind; + long oldInt = intValue; + double oldFloat = floatValue; + if (kind == F_TOP || kind == F_FLT_TOP) { + floatValue = value; + kind = F_FLT_CONST; + } + else if (kind == F_FLT_CONST && Double.compare(value, floatValue) != 0) { + kind = F_FLT_BOTTOM; + } + else if (!isFloat()) { + kind = F_BOTTOM; + } + return changed(oldKind, oldInt, oldFloat); + } + + public boolean meetWithTypeBottom(EZType type) { + return meet(factBottomFromType(type)); + } + + public boolean meet(LatticeElement other) { + byte oldKind = kind; + long oldInt = intValue; + double oldFloat = floatValue; + + if (kind == F_TOP) { + copyFrom(other); + return changed(oldKind, oldInt, oldFloat); + } + if (kind == F_BOTTOM || other.kind == F_TOP) + return false; + if (other.kind == F_BOTTOM) { + kind = F_BOTTOM; + return changed(oldKind, oldInt, oldFloat); + } + if (kind == other.kind) { + if (kind == F_INT_NONZERO_CONST && intValue != other.intValue) + kind = F_INT_NONZERO_VARYING; + else if (kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0) + kind = F_FLT_BOTTOM; + return changed(oldKind, oldInt, oldFloat); + } + if (isReference(kind) && isReference(other.kind)) { + kind = meetReference(other); + return changed(oldKind, oldInt, oldFloat); + } + if (isInteger(kind) && isInteger(other.kind)) { + meetInteger(other); + return changed(oldKind, oldInt, oldFloat); + } + if (isFloat(kind) && isFloat(other.kind)) { + meetFloat(other); + return changed(oldKind, oldInt, oldFloat); + } + kind = F_BOTTOM; + return changed(oldKind, oldInt, oldFloat); + } + + private boolean changed(byte oldKind, long oldInt, double oldFloat) { + return kind != oldKind || intValue != oldInt || Double.compare(floatValue, oldFloat) != 0; + } + + private byte meetReference(LatticeElement other) { + if (kind == F_REF_TOP) return other.kind; + if (other.kind == F_REF_TOP) return kind; + return switch (kind) { + case F_REF_NULL -> other.kind == F_REF_NULL ? F_REF_NULL : F_REF_BOTTOM; + case F_REF_NOT_NULL -> other.kind == F_REF_NOT_NULL ? F_REF_NOT_NULL : F_REF_BOTTOM; + case F_REF_BOTTOM -> F_REF_BOTTOM; + default -> F_BOTTOM; + }; + } + + private void meetInteger(LatticeElement other) { + if (kind == F_INT_TOP) { + copyFrom(other); + return; + } + if (other.kind == F_INT_TOP) + return; + switch (kind) { + case F_INT_ZERO -> { + if (other.kind != F_INT_ZERO) + kind = F_INT_BOTTOM; + } + case F_INT_NONZERO_CONST -> { + switch (other.kind) { + case F_INT_NONZERO_CONST -> { + if (intValue != other.intValue) + kind = F_INT_NONZERO_VARYING; + } + case F_INT_NONZERO_VARYING -> kind = F_INT_NONZERO_VARYING; + case F_INT_ZERO, F_INT_BOTTOM -> kind = F_INT_BOTTOM; + } + } + case F_INT_NONZERO_VARYING -> { + if (other.kind == F_INT_ZERO || other.kind == F_INT_BOTTOM) + kind = F_INT_BOTTOM; + } + case F_INT_BOTTOM -> { + } + } + } + + private void meetFloat(LatticeElement other) { + if (kind == F_FLT_TOP) { + copyFrom(other); + return; + } + if (other.kind == F_FLT_TOP) + return; + if (kind == F_FLT_CONST && + (other.kind == F_FLT_BOTTOM || + (other.kind == F_FLT_CONST && Double.compare(floatValue, other.floatValue) != 0))) { + kind = F_FLT_BOTTOM; + } + } + + public boolean isTop() { + return kind == F_TOP; + } + + public boolean isBottom() { + return kind == F_BOTTOM; + } + + public boolean isReference() { + return isReference(kind); + } + + public boolean isInteger() { + return isInteger(kind); + } + + public boolean isFloat() { + return isFloat(kind); + } + + public boolean isNull() { + return kind == F_REF_NULL; + } + + public boolean isNotNull() { + return kind == F_REF_NOT_NULL; + } + + public boolean isMaybeNull() { + return kind == F_REF_BOTTOM; + } + + public boolean isZero() { + return kind == F_INT_ZERO; + } + + public boolean isNonZero() { + return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; + } + + public static boolean isReference(byte kind) { + return kind >= F_REF_TOP && kind <= F_REF_BOTTOM; + } + + public static boolean isInteger(byte kind) { + return kind >= F_INT_TOP && kind <= F_INT_BOTTOM; + } + + public static boolean isFloat(byte kind) { + return kind >= F_FLT_TOP && kind <= F_FLT_BOTTOM; + } + + public static LatticeElement factTopFromType(EZType type) { + if (type instanceof EZType.EZTypeInteger) + return new LatticeElement(F_INT_TOP); + if (type instanceof EZType.EZTypeFloat) + return new LatticeElement(F_FLT_TOP); + if (isReferenceType(type)) + return new LatticeElement(F_REF_TOP); + return new LatticeElement(F_TOP); + } + + public static LatticeElement factBottomFromType(EZType type) { + if (type instanceof EZType.EZTypeInteger) + return new LatticeElement(F_INT_BOTTOM); + if (type instanceof EZType.EZTypeFloat) + return new LatticeElement(F_FLT_BOTTOM); + if (type instanceof EZType.EZTypeNull) + return new LatticeElement(F_REF_NULL); + if (isReferenceType(type)) + return new LatticeElement(F_REF_BOTTOM); + return new LatticeElement(F_BOTTOM); + } + + public static LatticeElement factFromType(EZType type) { + if (type != null) { + if (type instanceof EZType.EZTypeNullable) + return new LatticeElement(F_REF_BOTTOM); + if (type instanceof EZType.EZTypeNull) + return new LatticeElement(F_REF_NULL); + if (type instanceof EZType.EZTypeArray || type instanceof EZType.EZTypeStruct) + return new LatticeElement(F_REF_NOT_NULL); + if (type instanceof EZType.EZTypeInteger) + return new LatticeElement(F_INT_BOTTOM); + if (type instanceof EZType.EZTypeFloat) + return new LatticeElement(F_FLT_BOTTOM); + } + return new LatticeElement(F_BOTTOM); + } + + public static boolean isReferenceType(EZType type) { + return type instanceof EZType.EZTypeNullable || + type instanceof EZType.EZTypeArray || + type instanceof EZType.EZTypeStruct || + type instanceof EZType.EZTypeNull; + } + + @Override + public String toString() { + return switch (kind) { + case F_TOP -> "T"; + case F_REF_TOP -> "ref"; + case F_INT_TOP -> "int"; + case F_FLT_TOP -> "flt"; + case F_REF_NOT_NULL -> "not-null"; + case F_REF_NULL -> "null"; + case F_REF_BOTTOM -> "maybe-null"; + case F_INT_BOTTOM -> "int*"; + case F_FLT_BOTTOM -> "flt*"; + case F_INT_ZERO -> "0"; + case F_INT_NONZERO_CONST -> Long.toString(intValue); + case F_INT_NONZERO_VARYING -> "non-zero"; + case F_FLT_CONST -> Double.toString(floatValue); + case F_BOTTOM -> "⊥"; + default -> throw new CompilerException("Unknown lattice kind: " + kind); + }; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + LatticeElement that = (LatticeElement) o; + return kind == that.kind && intValue == that.intValue && Double.compare(floatValue, that.floatValue) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(kind, intValue, floatValue); + } +} \ No newline at end of file From 7c9da3cf491323c6957294ef7d317759b6d229b2 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 19:48:18 +0100 Subject: [PATCH 13/15] Updated README --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a47f23..d9b099c 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ The EeZee programming language is designed to allow us to learn various compiler The EeZee programming language is a tiny statically typed language with syntax inspired by Swift. The language has the following features: -* Integer, Struct and 1-Dimensional Array types +* Integer, Float, Struct and 1-Dimensional Array types * If and While statements * Functions -The EeZee language specification is [available](https://compilerprogramming.github.io/ez-lang.html). +The EeZee language specification is [available](./docs/ez-lang.rst). The language is intentionally very simple and is meant to have just enough functionality to experiment with compiler implementation techniques. ## Modules @@ -31,6 +31,10 @@ The project is under development and subject to change. At this point in time, w * [seaofnodes](./seaofnodes/README.md) - WIP compiler that generates Sea of Nodes IR, using SoN backend from [Simple Chapter 21](https://github.com/SeaOfNodes/Simple). Generates native code for X86-64, AArch64 and RISC-V. +## Documentation + +See [docs](./docs) + ## How can you contribute? The project is educational, with a focus on exploring various compiler algorithms and data structures. From ae8c008e46759479f510016850aa483f3fdb923c Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 19:50:43 +0100 Subject: [PATCH 14/15] Updated README --- docs/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6a48118 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# Documentation + +This folder contains language and compiler design notes for ez-lang. + +## Language + +- [EeZee Programming Language](ez-lang.rst) - Source language overview, including syntax, types, control flow, expressions, and grammar. + +## Intermediate Representations + +- [Intermediate Representations](ir-overview.rst) - Overview of the compiler IRs used in the project, including stack-based, register-based, and sea-of-nodes forms. +- [IR Design Instructions](ir-design-instructions.md) - Notes on designing linear IRs, including basic blocks, control flow graphs, and instruction organization. +- [Virtual Registers](ir-virtual-registers.md) - Design discussion for virtual register representation, source variable slots, temporaries, and register identity. + +## Data-Flow And SSA + +- [Liveness Analysis](liveness.md) - Description of the liveness sets and algorithm used by the optimizing VM. +- [SSA Construction](ssa-construction.md) - Explanation of SSA conversion, dominance-frontier phi placement, and register renaming. +- [SSA Destruction Using Briggs](ssa-destruction-briggs.md) - Explanation of the Briggs SSA destruction algorithm and how phi nodes are lowered to copies. From d3ab43ce9a73cd50e802b57366207f8e9f0cdcc3 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Wed, 8 Jul 2026 20:14:11 +0100 Subject: [PATCH 15/15] Update grammar and description --- .../compilerprogramming/ezlang/antlr/EZLanguage.g4 | 2 +- docs/ez-lang.rst | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 b/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 index cce44c5..cb06926 100644 --- a/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 +++ b/antlr-parser/src/main/antlr4/com/compilerprogramming/ezlang/antlr/EZLanguage.g4 @@ -158,7 +158,7 @@ NON_DIGIT INTEGER_LITERAL : DEC_LITERAL ; -DEC_LITERAL: DEC_DIGIT (DEC_DIGIT | '_')*; +DEC_LITERAL: DEC_DIGIT+; FLOAT_LITERAL: DEC_DIGIT+ '.' DEC_DIGIT*; diff --git a/docs/ez-lang.rst b/docs/ez-lang.rst index 82af72f..46e0756 100644 --- a/docs/ez-lang.rst +++ b/docs/ez-lang.rst @@ -20,7 +20,7 @@ Keywords -------- Following are keywords in the language:: - func var Int Float struct if else while break continue return null + func var new Int Float struct if else while break continue return null Source Unit ----------- @@ -231,6 +231,16 @@ Following table describes the available operators by their precedence (low to hi +Floating point operands support the arithmetic operators ``+``, ``-``, ``*``, ``/``, +the relational operators ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, and unary +negation ``-``. The modulo operator ``%``, the logical operators ``&&`` and ``||``, and +the unary ``!`` operator are not supported for floating point operands. A relational +operator applied to floating point operands produces an ``Int`` result (``1`` or ``0``). + +There is no implicit conversion between ``Int`` and ``Float``. The two operands of a +binary arithmetic or relational operator must be of the same type; mixing ``Int`` and +``Float`` in a single operation is a type error. + Grammar -------