From c693cd8be03f4f34b7cf122df647c1253a33eaec Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Sat, 13 Jun 2026 23:06:12 +0100 Subject: [PATCH 1/9] experiment with flow type analysis --- .../ezlang/semantic/FlowCFG.java | 340 ++++++++++++++++++ .../ezlang/semantic/TestSemaAssignTypes.java | 43 +++ .../ezlang/semantic/TestSemaFlowGraph.java | 116 ++++++ 3 files changed, 499 insertions(+) create mode 100644 semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java create mode 100644 semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java diff --git a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java new file mode 100644 index 0000000..0881913 --- /dev/null +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java @@ -0,0 +1,340 @@ +package com.compilerprogramming.ezlang.semantic; + +import com.compilerprogramming.ezlang.parser.AST; + +import java.util.*; + +/** + * Builds a CFG on top of AST, focusing on control flow. + */ +public final class FlowCFG { + public enum EdgeKind { + NORMAL, + TRUE, + FALSE + } + + public static final class FlowEdge { + public final FlowBlock from; + public final FlowBlock to; + public final EdgeKind kind; + public final AST.Expr condition; // non-null for TRUE/FALSE edges + + FlowEdge(FlowBlock from, FlowBlock to, EdgeKind kind, AST.Expr condition) { + this.from = from; + this.to = to; + this.kind = kind; + this.condition = condition; + } + + @Override + public String toString() { + if (condition != null) { + return "B" + from.id + " -" + kind + "(" + condition + ")-> B" + to.id; + } + return "B" + from.id + " -" + kind + "-> B" + to.id; + } + } + + public static final class FlowBlock { + public final int id; + + /* + * Ordinary sequential statements. + * No if/while/break/continue/return should usually be stored here, + * except return/break/continue as final block statements if desired. + */ + public final List statements = new ArrayList<>(); + + /* + * If this block branches, condition is non-null. + * For short-circuit && / ||, each split condition gets its own block. + */ + public AST.Expr condition; + + public final List preds = new ArrayList<>(); + public final List succs = new ArrayList<>(); + + FlowBlock(int id) { + this.id = id; + } + + public boolean isTerminated() { + return !succs.isEmpty(); + } + + @Override + public String toString() { + return "B" + id; + } + } + + public static final class FlowGraph { + public final AST.FuncDecl function; + public final FlowBlock entry; + public final FlowBlock exit; + + public final List blocks = new ArrayList<>(); + public final List edges = new ArrayList<>(); + + FlowGraph(AST.FuncDecl function, FlowBlock entry, FlowBlock exit) { + this.function = function; + this.entry = entry; + this.blocks.add(entry); + this.exit = exit; + this.blocks.add(exit); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (FlowBlock b : blocks) { + sb.append("B" + b.id + ":\n"); + + if (b == entry) { + sb.append(" \n"); + } + + if (b == exit) { + sb.append(" \n"); + } + + for (AST.Stmt s : b.statements) { + sb.append(" " + oneLine(s)).append('\n'); + } + + if (b.condition != null) { + sb.append(" condition: " + oneLine(b.condition)).append('\n'); + } + + for (FlowEdge e : b.succs) { + sb.append(" " + e).append('\n'); + } + } + return sb.toString(); + } + + public String toDot() { + StringBuilder sb = new StringBuilder(); + sb.append("digraph CFG {\n"); + + for (FlowBlock b : blocks) { + sb.append(" B").append(b.id).append(" [shape=box,label=\""); + sb.append("B").append(b.id); + + if (b == entry) sb.append("\\n"); + if (b == exit) sb.append("\\n"); + + for (AST.Stmt s : b.statements) { + sb.append("\\n").append(escape(oneLine(s))); + } + + if (b.condition != null) { + sb.append("\\n? ").append(escape(oneLine(b.condition))); + } + + sb.append("\"];\n"); + } + + for (FlowEdge e : edges) { + sb.append(" B").append(e.from.id) + .append(" -> B").append(e.to.id); + + if (e.kind != EdgeKind.NORMAL) { + sb.append(" [label=\"").append(e.kind).append("\"]"); + } + + sb.append(";\n"); + } + + sb.append("}\n"); + return sb.toString(); + } + + private static String oneLine(AST ast) { + if (ast == null) return ""; + return ast.toString().replace("\n", " ").trim(); + } + + private static String escape(String s) { + return s.replace("\\", "\\\\").replace("\"", "\\\""); + } + } + + private static final class LoopTargets { + final FlowBlock breakTarget; + final FlowBlock continueTarget; + + LoopTargets(FlowBlock breakTarget, FlowBlock continueTarget) { + this.breakTarget = breakTarget; + this.continueTarget = continueTarget; + } + } + + private final FlowGraph graph; + private int nextId = 0; + private final Deque loopStack = new ArrayDeque<>(); + + private FlowCFG(AST.FuncDecl fn) { + FlowBlock entry = newBlock(); + FlowBlock exit = newBlock(); + this.graph = new FlowGraph(fn, entry, exit); + } + + public static FlowGraph build(AST.FuncDecl fn) { + FlowCFG builder = new FlowCFG(fn); + + FlowBlock body = builder.newBlock(); + builder.addEdge(builder.graph.entry, body, EdgeKind.NORMAL, null); + + FlowBlock fallthrough = builder.buildStmt(fn.block, body); + + if (!fallthrough.isTerminated()) { + builder.addEdge(fallthrough, builder.graph.exit, EdgeKind.NORMAL, null); + } + + return builder.graph; + } + + private FlowBlock newBlock() { + FlowBlock b = new FlowBlock(nextId++); + if (graph != null) + // Chicken and egg problem with entry/exit blocks + graph.blocks.add(b); + return b; + } + + private void addEdge(FlowBlock from, FlowBlock to, EdgeKind kind, AST.Expr condition) { + FlowEdge e = new FlowEdge(from, to, kind, condition); + from.succs.add(e); + to.preds.add(e); + graph.edges.add(e); + } + + private FlowBlock buildStmt(AST.Stmt stmt, FlowBlock cur) { + if (stmt instanceof AST.BlockStmt block) { + return buildBlock(block, cur); + } + + if (stmt instanceof AST.IfElseStmt ifs) { + return buildIf(ifs, cur); + } + + if (stmt instanceof AST.WhileStmt wh) { + return buildWhile(wh, cur); + } + + if (stmt instanceof AST.ReturnStmt ret) { + cur.statements.add(ret); + addEdge(cur, graph.exit, EdgeKind.NORMAL, null); + return newBlock(); + } + + if (stmt instanceof AST.BreakStmt br) { + cur.statements.add(br); + addEdge(cur, loopStack.peek().breakTarget, EdgeKind.NORMAL, null); + return newBlock(); + } + + if (stmt instanceof AST.ContinueStmt cont) { + cur.statements.add(cont); + addEdge(cur, loopStack.peek().continueTarget, EdgeKind.NORMAL, null); + return newBlock(); + } + + /* + * Ordinary non-branching statements are accumulated into the current block: + * AssignStmt, VarStmt, ExprStmt, VarDeclStmt, etc. + */ + cur.statements.add(stmt); + return cur; + } + + private FlowBlock buildBlock(AST.BlockStmt block, FlowBlock cur) { + for (AST.Stmt stmt : block.stmtList) { + cur = buildStmt(stmt, cur); + } + return cur; + } + + private FlowBlock buildIf(AST.IfElseStmt ifs, FlowBlock cur) { + FlowBlock thenBlock = newBlock(); + FlowBlock elseBlock = newBlock(); + FlowBlock afterIf = newBlock(); + + buildCondition(ifs.condition, cur, thenBlock, elseBlock); + + FlowBlock thenExit = buildStmt(ifs.ifStmt, thenBlock); + if (!thenExit.isTerminated()) { + addEdge(thenExit, afterIf, EdgeKind.NORMAL, null); + } + + if (ifs.elseStmt != null) { + FlowBlock elseExit = buildStmt(ifs.elseStmt, elseBlock); + if (!elseExit.isTerminated()) { + addEdge(elseExit, afterIf, EdgeKind.NORMAL, null); + } + } else { + addEdge(elseBlock, afterIf, EdgeKind.NORMAL, null); + } + + return afterIf; + } + + private FlowBlock buildWhile(AST.WhileStmt wh, FlowBlock cur) { + FlowBlock condBlock = newBlock(); + FlowBlock bodyBlock = newBlock(); + FlowBlock afterLoop = newBlock(); + + addEdge(cur, condBlock, EdgeKind.NORMAL, null); + + loopStack.push(new LoopTargets(afterLoop, condBlock)); + + buildCondition(wh.condition, condBlock, bodyBlock, afterLoop); + + FlowBlock bodyExit = buildStmt(wh.stmt, bodyBlock); + if (!bodyExit.isTerminated()) { + addEdge(bodyExit, condBlock, EdgeKind.NORMAL, null); + } + + loopStack.pop(); + + return afterLoop; + } + + private void buildCondition( + AST.Expr expr, + FlowBlock from, + FlowBlock trueTarget, + FlowBlock falseTarget + ) { + if (expr instanceof AST.BinaryExpr bin) { + String op = bin.op.str; + + if (op.equals("&&")) { + FlowBlock rhsBlock = newBlock(); + + buildCondition(bin.expr1, from, rhsBlock, falseTarget); + buildCondition(bin.expr2, rhsBlock, trueTarget, falseTarget); + return; + } + + if (op.equals("||")) { + FlowBlock rhsBlock = newBlock(); + + buildCondition(bin.expr1, from, trueTarget, rhsBlock); + buildCondition(bin.expr2, rhsBlock, trueTarget, falseTarget); + return; + } + } + + if (expr instanceof AST.UnaryExpr un && un.op.str.equals("!")) { + buildCondition(un.expr, from, falseTarget, trueTarget); + return; + } + + from.condition = expr; + addEdge(from, trueTarget, EdgeKind.TRUE, expr); + addEdge(from, falseTarget, EdgeKind.FALSE, expr); + } +} \ No newline at end of file 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 a317c12..e0f29af 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaAssignTypes.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaAssignTypes.java @@ -437,4 +437,47 @@ func foo()->Int { """; analyze(src, "foo", "func foo()->Int"); } + + // Nullable Bug call should fail + @Test + public void test24() { + String src = """ + struct Foo { var bar: Int } + func bar(arg: Foo) { + } + func foo(arg: Foo?) { + bar(arg); + } +"""; + analyze(src, "foo", "func foo(arg: Foo?)"); + } + + // Nullable Bug call should fail + @Test + public void test25() { + String src = """ + struct Foo { var bar: Int } + func bar(arg: Foo) { + } + func foo() { + bar(null); + } +"""; + analyze(src, "foo", "func foo()"); + } + + // Nullable Bug assignment should fail + @Test + public void test26() { + String src = """ + struct Foo { var bar: Int } + func bar()->Foo? { + } + func foo() { + var f: Foo + f = bar(); + } +"""; + analyze(src, "foo", "func foo()"); + } } diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java new file mode 100644 index 0000000..59dd83a --- /dev/null +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java @@ -0,0 +1,116 @@ +package com.compilerprogramming.ezlang.semantic; + +import com.compilerprogramming.ezlang.lexer.Lexer; +import com.compilerprogramming.ezlang.parser.AST; +import com.compilerprogramming.ezlang.parser.Parser; +import com.compilerprogramming.ezlang.types.Symbol; +import com.compilerprogramming.ezlang.types.TypeDictionary; +import org.junit.Assert; +import org.junit.Test; + +public class TestSemaFlowGraph { + + private String test(String src, String symbolName) { + Parser parser = new Parser(); + var program = parser.parse(new Lexer(src)); + var typeDict = new TypeDictionary(); + var sema = new SemaDefineTypes(typeDict); + sema.analyze(program); + var sema2 = new SemaAssignTypes(typeDict); + sema2.analyze(program); + var symbol = typeDict.lookup(symbolName); + Assert.assertNotNull(symbol); + if (symbol instanceof Symbol.FunctionTypeSymbol ftsym) { + var fnDecl = (AST.FuncDecl) ftsym.functionDecl; + var flowGraph = FlowCFG.build(fnDecl); + var dot = flowGraph.toDot(); + System.out.println(dot); + return flowGraph.toString(); + } + return null; + } + + @Test + public void test1() { + String src = """ + func bar(i: Int) {} + func foo(v: Int) + { + if (v == 0) + bar(v+1); + else if (v == 1) + bar(v-20); + else + bar(v*30); + } +"""; + var result = test(src, "foo"); + System.out.println(result); + } + + @Test + public void test2() { + String src = """ + func bar(i: Int) {} + func foo(a: Int, b: Int) + { + if (a && b) + bar(a+b); + else + bar(0); + } +"""; + var result = test(src, "foo"); + System.out.println(result); + } + + @Test + public void test3() { + String src = """ + func bar(i: Int) {} + func foo(a: Int, b: Int, c: Int) + { + if (a && b || c) + bar(a+b); + else + bar(0); + } +"""; + var result = test(src, "foo"); + System.out.println(result); + } + + @Test + public void test4() { + String src = """ + func bar(i: Int) {} + func foo(a: Int, b: Int, c: Int) + { + if (a && b || a && c) + bar(a+b); + else + bar(0); + } +"""; + var result = test(src, "foo"); + System.out.println(result); + } + + @Test + public void test5() { + String src = """ + func bar(i: Int) {} + func foo(a: Int, b: Int) + { + while (a && b) { + bar(a+b); + b = a; + a = 0; + } + } +"""; + var result = test(src, "foo"); + System.out.println(result); + } + +} From df00ff7aa6cead7e824ca7e38e03ca48fce3a1bd Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Mon, 15 Jun 2026 00:27:54 +0100 Subject: [PATCH 2/9] experiment with flow type analysis --- .../ezlang/semantic/NullableAnalysis.java | 558 ++++++++++++++++++ .../ezlang/semantic/TestNullAnalysis.java | 161 +++++ 2 files changed, 719 insertions(+) create mode 100644 semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java create mode 100644 semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java diff --git a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java new file mode 100644 index 0000000..1d7fd67 --- /dev/null +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -0,0 +1,558 @@ +package com.compilerprogramming.ezlang.semantic; + +import com.compilerprogramming.ezlang.exceptions.CompilerException; +import com.compilerprogramming.ezlang.lexer.Token; +import com.compilerprogramming.ezlang.parser.AST; +import com.compilerprogramming.ezlang.types.EZType; +import com.compilerprogramming.ezlang.types.Scope; +import com.compilerprogramming.ezlang.types.Symbol; +import com.compilerprogramming.ezlang.types.TypeDictionary; + +import java.util.*; + +/** + * Input is a CFG over AST. the CFG should capture control flow that is + * relevant for null analysis. + * This performs an iterative forward data flow analysis to fixed point. + * At the end of this each basic block (FlowBlock) in the CFG has + * an associated set of facts that inform which variables are null + * and which are not null or maybe null. + */ +public class NullableAnalysis { + + int reg = 0; + ArrayList latticeElements = new ArrayList<>(); + + public NullableAnalysis(Symbol.FunctionTypeSymbol functionSymbol, TypeDictionary typeDictionary) { + AST.FuncDecl funcDecl = (AST.FuncDecl) functionSymbol.functionDecl; + setVirtualRegisters(funcDecl.scope); + } + private void setVirtualRegisters(Scope scope) { + for (Symbol symbol: scope.getLocalSymbols()) { + if (symbol instanceof Symbol.VarSymbol varSymbol) { + varSymbol.regNumber = reg++; + LatticeElement elem = null; + if (varSymbol instanceof Symbol.ParameterSymbol) { + var type = varSymbol.type; + if (type.isPrimitive()) + // Some int value, as our only primitive type is int + elem = new LatticeElement(F_INT); + else if (type instanceof EZType.EZTypeNullable) + elem = new LatticeElement(F_MAYBE_NULL); + else + elem = new LatticeElement(F_NOT_NULL); + } + else { + elem = new LatticeElement(F_UNKNOWN); + } + latticeElements.add(elem); + } + } + for (Scope childScope: scope.children) { + setVirtualRegisters(childScope); + } + } + + static final byte F_UNKNOWN = 0; + static final byte F_NOT_NULL = 1; + static final byte F_NULL = 2; + static final byte F_MAYBE_NULL = 3; // BOTTOM + static final byte F_ZERO = 4; + static final byte F_NONZERO_CONST = 5; + static final byte F_NONZERO_VARYING = 6; + static final byte F_INT = 7; + + // Associated with each register + static final class LatticeElement { + public byte kind; + private long intValue; + + public LatticeElement() { + this.kind = F_UNKNOWN; + } + public LatticeElement(byte kind) { + this.kind = kind; + } + public LatticeElement(byte kind,long value) { + this.kind = kind; + this.intValue = value; + } + public LatticeElement(long value) { + setIntValue(value); + } + + boolean nullable() { + return kind == F_NULL || kind == F_NOT_NULL || kind == F_MAYBE_NULL; + } + boolean someInt() { + return kind == F_INT || kind == F_ZERO || kind == F_NONZERO_CONST || kind == F_NONZERO_VARYING; + } + LatticeElement copy() { + return new LatticeElement(kind,intValue); + } + boolean isTrue() { + return kind == F_NONZERO_CONST || kind == F_NONZERO_VARYING; + } + boolean isFalse() { + return kind == F_ZERO; + } + boolean isIntegerConstant() { + return kind == F_ZERO || kind == F_NONZERO_CONST; + } + boolean setIntValue(long value) { + byte prevKind = kind; + if (kind == F_UNKNOWN) { + this.intValue = value; + this.kind = value == 0 ? F_ZERO : F_NONZERO_CONST; + } + else if (kind == F_ZERO && value != 0) { + this.kind = F_INT; + } + else if (kind == F_NONZERO_CONST && (value == 0 || value != intValue)) { + this.kind = F_INT; + } + else if (nullable()) { + throw new CompilerException("Cannot assign integer value to lattice cell that is nullable"); + } + return (kind != prevKind); + } + + boolean setNull() { + byte prevKind = kind; + if (kind == F_UNKNOWN) + kind = F_NULL; + else if (kind == F_NOT_NULL) + kind = F_MAYBE_NULL; + else if (someInt()) { + throw new CompilerException("Cannot assign null to lattice cell that is int type"); + } + return (kind != prevKind); + } + + boolean setNotNull() { + byte prevKind = kind; + if (kind == F_UNKNOWN) + kind = F_NOT_NULL; + else if (kind == F_NULL) + kind = F_MAYBE_NULL; + else if (someInt()) { + throw new CompilerException("Cannot assign not null to lattice cell that is int type"); + } + return (kind != prevKind); + } + + boolean setMaybeNull() { + byte prevKind = kind; + if (kind == F_UNKNOWN) + kind = F_MAYBE_NULL; + else if (kind == F_NULL || kind == F_NOT_NULL) + kind = F_MAYBE_NULL; + else if (someInt()) { + throw new CompilerException("Cannot assign maybe null to lattice cell that is int type"); + } + return (kind != prevKind); + } + + boolean meet(LatticeElement other) { + byte oldKind = this.kind; + if (kind == F_UNKNOWN) { + kind = other.kind; + } + else if (nullable() && other.nullable()) { + if (kind == F_NULL && other.kind == F_NOT_NULL || + kind == F_NOT_NULL && other.kind == F_NULL) + kind = F_MAYBE_NULL; + } + else if (someInt() && other.someInt()) { + if (kind == F_ZERO) { + if (other.kind == F_NONZERO_CONST || other.kind == F_NONZERO_VARYING) { + kind = F_INT; + } + } + else if (kind == F_NONZERO_CONST) { + if (other.kind == F_NONZERO_CONST && intValue != other.intValue) { + kind = F_INT; + } + else if (other.kind == F_ZERO) { + kind = F_INT; + } + } + else if (kind == F_NONZERO_VARYING && other.kind == F_ZERO) { + kind = F_INT; + } + } + return kind != oldKind; + } + + @Override + public String toString() { + switch (kind) { + case F_UNKNOWN -> { + return "unknown"; + } + case F_NULL -> { return "null"; } + case F_NOT_NULL -> { return "not null"; } + case F_MAYBE_NULL -> { return "maybe null"; } + case F_ZERO -> { return "zero"; } + case F_NONZERO_CONST -> { return "non-zero const"; } + case F_NONZERO_VARYING -> { return "non-zero varying"; } + case F_INT -> { return "int"; } + default -> throw new CompilerException("Unknown type in lattice"); + } + } + + @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; + } + + @Override + public int hashCode() { + return Objects.hash(kind, intValue); + } + } + static final class Facts { + final LatticeElement[] vars; + + Facts(List elementList) { + vars = elementList.toArray(new LatticeElement[0]); + } + Facts(LatticeElement[] elements) { + vars = elements; + } + + Facts copy() { + LatticeElement[] copyvars = new LatticeElement[vars.length]; + for (int i = 0; i < copyvars.length; i++) + copyvars[i] = vars[i].copy(); + return new Facts(copyvars); + } + + LatticeElement get(int reg) { + return vars[reg]; + } + + static Facts merge(Facts a, Facts b) { + Facts out = a.copy(); + + for (int i = 0; i < out.vars.length; i++) { + out.vars[i].meet(b.vars[i]); + } + + return out; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Facts facts = (Facts) o; + return Objects.deepEquals(vars, facts.vars); + } + } + + LatticeElement analyzeExpr(AST.Expr e, Facts facts) { + if (e instanceof AST.NewExpr || e instanceof AST.InitExpr) { + return new LatticeElement(F_NOT_NULL); + } + + if (e instanceof AST.NameExpr name && + name.symbol instanceof Symbol.VarSymbol varSymbol) { + return facts.get(varSymbol.regNumber).copy(); + } + + if (e instanceof AST.CallExpr call) { + EZType.EZTypeFunction tf = (EZType.EZTypeFunction)call.callee.type; + for (int i = 0; i < tf.args.size(); i++) { + EZType type = tf.args.get(i).type; + AST.Expr expr = call.args.get(i); + var lattice = analyzeExpr(expr,facts); + checkAssignment(type,lattice); + } + + // Use function return type: + // Foo -> NON_NULL + // Foo? -> UNKNOWN + return factFromType(call.type); + } + + if (e instanceof AST.GetFieldExpr field) { + // First check object is non-null. + // Then use field type. + return factFromType(field.type); + } + + if (e instanceof AST.ArrayLoadExpr arrayLoad) { + // First check array is non-null. + // Then use element type. + return factFromType(arrayLoad.type); + } + + if (e instanceof AST.LiteralExpr lit) { + if (lit.value.str.equals("null")) { + return new LatticeElement(F_NULL); + } + if (lit.value.kind == Token.Kind.NUM) { + return new LatticeElement(Long.parseLong(lit.value.str)); + } + } + + if (e instanceof AST.UnaryExpr un) { + LatticeElement v = analyzeExpr(un.expr, facts); + + if (un.op.str.equals("-")) { + if (v.someInt()) + v.setIntValue(-v.intValue); + else + throw new CompilerException("Cannot apply - to non integer"); + } + + if (un.op.str.equals("!")) { + if (v.isTrue()) return new LatticeElement(0L); + if (v.isFalse()) return new LatticeElement(1L); + return new LatticeElement(F_INT); + } + } + + if (e instanceof AST.BinaryExpr bin) { + LatticeElement result = null; + LatticeElement a = analyzeExpr(bin.expr1, facts); + LatticeElement b = analyzeExpr(bin.expr2, facts); + + if (a.isIntegerConstant() && b.isIntegerConstant()) { + boolean isArith = false; + long value = 0; + switch (bin.op.str) { + case "+" -> { + value = a.intValue + b.intValue; + isArith = true; + } + case "-" -> { + value = a.intValue - b.intValue; + isArith = true; + } + case "*" -> { + value = a.intValue * b.intValue; + isArith = true; + } + case "/" -> { + if (b.kind == F_ZERO) + throw new CompilerException("Division by zero"); + value = a.intValue / b.intValue; + } + case "%" -> { + if (b.kind == F_ZERO) + throw new CompilerException("Division by zero"); + value = a.intValue % b.intValue; + } + + case "==" -> { + value = a.intValue == b.intValue ? 1 : 0; + } + case "!=" -> { + value = a.intValue != b.intValue ? 1 : 0; + } + case "<" -> { + value = a.intValue < b.intValue ? 1 : 0; + } + case "<=" -> { + value = a.intValue <= b.intValue ? 1 : 0; + } + case ">" -> { + value = a.intValue > b.intValue ? 1 : 0; + } + case ">=" -> { + value = a.intValue >= b.intValue ? 1 : 0; + } + + default -> throw new CompilerException("Unknown binary operator " + bin.op.str); + } + if (isArith) + result = new LatticeElement(value); + else { + if (value == 1) + result = new LatticeElement(F_NONZERO_CONST,1); + else + result = new LatticeElement(F_ZERO); + } + return result; + } + } + + return new LatticeElement(); + } + + LatticeElement factFromType(EZType type) { + if (type != null) { + if (type instanceof EZType.EZTypeNullable) + return new LatticeElement(F_MAYBE_NULL); + else if (type instanceof EZType.EZTypeNull) + return new LatticeElement(F_NULL); + else if (!type.isPrimitive()) + return new LatticeElement(F_NOT_NULL); + } + return new LatticeElement(); + } + + public void doAnalysis(FlowCFG.FlowGraph cfg) { + Queue worklist = new ArrayDeque<>(); + + Map in = new HashMap<>(); + Map out = new HashMap<>(); + + var initialFacts = new Facts(latticeElements); + in.put(cfg.entry, initialFacts); + worklist.add(cfg.entry); + + while (!worklist.isEmpty()) { + FlowCFG.FlowBlock b = worklist.remove(); + + Facts inFacts = in.get(b); + if (inFacts == null) + throw new CompilerException("Missing facts"); + Facts outFacts = transferBlock(b, inFacts); + + if (!outFacts.equals(out.get(b))) { + out.put(b, outFacts); + + for (FlowCFG.FlowEdge e : b.succs) { + Facts edgeFacts = applyEdgeFacts(outFacts, e); + + Facts oldIn = in.get(e.to); + Facts newIn = oldIn == null ? edgeFacts : Facts.merge(oldIn, edgeFacts); + + if (!newIn.equals(oldIn)) { + in.put(e.to, newIn); + worklist.add(e.to); + } + } + } + } + } + + Facts transferBlock(FlowCFG.FlowBlock block, Facts in) { + Facts facts = in.copy(); + + for (AST.Stmt stmt : block.statements) { + transferStmt(stmt, facts); + } + + return facts; + } + + private void checkAssignment(EZType type, LatticeElement lattice) { + if (type.isPrimitive()) { + if (!lattice.someInt() && lattice.kind != F_UNKNOWN) + throw new CompilerException("Cannot assign reference/null to int"); + return; + } + + if (type instanceof EZType.EZTypeNullable) { + return; // null allowed + } + + // non-null reference + if (lattice.kind == F_NULL || lattice.kind == F_MAYBE_NULL) { + throw new CompilerException("Cannot assign null or potentially null value"); + } + } + + void transferStmt(AST.Stmt stmt, Facts facts) { + if (stmt instanceof AST.AssignStmt assign) { + Symbol.VarSymbol sym = (Symbol.VarSymbol) assign.nameExpr.symbol; + var lattice = analyzeExpr(assign.rhs, facts); + checkAssignment(sym.type,lattice); + facts.vars[sym.regNumber] = lattice; + return; + } + + if (stmt instanceof AST.VarStmt varStmt) { + Symbol.VarSymbol sym = varStmt.symbol; + var lattice = analyzeExpr(varStmt.expr, facts); + checkAssignment(sym.type,lattice); + facts.vars[sym.regNumber] = lattice; + return; + } + + if (stmt instanceof AST.ExprStmt exprStmt && + exprStmt.expr instanceof AST.SetFieldExpr setFieldExpr) { + EZType.EZTypeStruct structType = (EZType.EZTypeStruct) setFieldExpr.object.type; + EZType fieldType = structType.getField(setFieldExpr.fieldName); + var lattice = analyzeExpr(setFieldExpr.value,facts); + checkAssignment(fieldType,lattice); + return; + } + + if (stmt instanceof AST.ExprStmt exprStmt && + exprStmt.expr instanceof AST.ArrayStoreExpr arrayStoreExpr) { + EZType.EZTypeArray arrayType = null; + EZType elementType = null; + if (arrayStoreExpr.array.type instanceof EZType.EZTypeArray ta) { + arrayType = ta; + } + else if (arrayStoreExpr.array.type instanceof EZType.EZTypeNullable ptr && + ptr.baseType instanceof EZType.EZTypeArray ta) { + arrayType = ta; + } + if (arrayType == null) + return; + elementType = arrayType.getElementType(); + var lattice = analyzeExpr(arrayStoreExpr.value,facts); + checkAssignment(elementType,lattice); + } + + if (stmt instanceof AST.ExprStmt exprStmt && + exprStmt.expr instanceof AST.CallExpr callExpr) { + analyzeExpr(callExpr,facts); + } + } + + + Facts applyEdgeFacts(Facts in, FlowCFG.FlowEdge edge) { + Facts out = in.copy(); + + if (edge.kind == FlowCFG.EdgeKind.TRUE) { + applyConditionFacts(out, edge.condition, true); + } else if (edge.kind == FlowCFG.EdgeKind.FALSE) { + applyConditionFacts(out, edge.condition, false); + } + + return out; + } + void applyConditionFacts(Facts facts, AST.Expr expr, boolean branchIsTrue) { + if (!(expr instanceof AST.BinaryExpr bin)) { + return; + } + + boolean leftNameRightNull = + isName(bin.expr1) && analyzeExpr(bin.expr2, facts).kind == F_NULL; + + boolean rightNameLeftNull = + isName(bin.expr2) && analyzeExpr(bin.expr1, facts).kind == F_NULL; + + if (!leftNameRightNull && !rightNameLeftNull) { + return; + } + + AST.NameExpr name = leftNameRightNull ? (AST.NameExpr) bin.expr1 : (AST.NameExpr) bin.expr2; + Symbol.VarSymbol varSymbol = (Symbol.VarSymbol) name.symbol; + String op = bin.op.str; + LatticeElement lattice; + if (branchIsTrue) { + lattice = op.equals("!=") + ? new LatticeElement(F_NOT_NULL) + : new LatticeElement(F_NULL); + } else { + lattice = op.equals("!=") + ? new LatticeElement(F_NULL) + : new LatticeElement(F_NOT_NULL); + } + facts.vars[varSymbol.regNumber] = lattice; + } + + boolean isName(AST.Expr e) { + return e instanceof AST.NameExpr; + } + +} diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java new file mode 100644 index 0000000..aea774b --- /dev/null +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java @@ -0,0 +1,161 @@ +package com.compilerprogramming.ezlang.semantic; + +import com.compilerprogramming.ezlang.exceptions.CompilerException; +import com.compilerprogramming.ezlang.lexer.Lexer; +import com.compilerprogramming.ezlang.parser.AST; +import com.compilerprogramming.ezlang.parser.Parser; +import com.compilerprogramming.ezlang.types.Symbol; +import com.compilerprogramming.ezlang.types.TypeDictionary; +import org.junit.Assert; +import org.junit.Test; + +public class TestNullAnalysis { + private void analyze(String src, String symbolName) { + Parser parser = new Parser(); + var program = parser.parse(new Lexer(src)); + var typeDict = new TypeDictionary(); + var sema = new SemaDefineTypes(typeDict); + sema.analyze(program); + var sema2 = new SemaAssignTypes(typeDict); + sema2.analyze(program); + var symbol = typeDict.lookup(symbolName); + Assert.assertNotNull(symbol); + if (symbol instanceof Symbol.FunctionTypeSymbol ftsym) { + var fnDecl = (AST.FuncDecl) ftsym.functionDecl; + var flowGraph = FlowCFG.build(fnDecl); + var dot = flowGraph.toDot(); + System.out.println(dot); + new NullableAnalysis(ftsym,typeDict).doAnalysis(flowGraph); + } + } + + @Test(expected = CompilerException.class) + public void test25() { + String src = """ + struct Foo { var bar: Int } + func takesFoo(arg: Foo) { + } + func test() { + takesFoo(null); + } +"""; + analyze(src, "test"); + } + + @Test + public void test26() { + String src = """ + struct Foo { var bar: Int } + func takesOptionalFoo(arg: Foo?) { + } + func test() { + takesOptionalFoo(null); + } +"""; + analyze(src, "test"); + } + + @Test + public void test27() { + String src = """ + struct Foo { var bar: Int } + func takesFoo(arg: Foo) { + } + func test(arg: Foo?) { + if (arg != null) { + takesFoo(arg); + } + } +"""; + analyze(src, "test"); + } + + @Test + public void test28() { + String src = """ + struct Foo { + var bar: Int + } + func takesFoo(arg: Foo) { + } + func test(arg: Foo?) { + if (arg == null) { + return; + } + takesFoo(arg); + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void test29() { + String src = """ + struct Foo { + var bar: Int + } + func returnsOptionalFoo()->Foo? { + return null; + } + func test() { + var foo: Foo + foo = returnsOptionalFoo(); + + } +"""; + analyze(src, "test"); + } + + @Test + public void test30() { + String src = """ + struct Foo { + var bar: Int + } + func returnsFoo()->Foo { + return new Foo{}; + } + func test() { + var foo: Foo + foo = returnsFoo(); + } +"""; + analyze(src, "test"); + } + + @Test + public void test31() { + String src = """ + struct Foo { + var bar: Int + } + func returnsFoo()->Foo { + return new Foo{}; + } + func test() { + var foo: Foo? + foo = null; + foo = returnsFoo(); + } +"""; + analyze(src, "test"); + } + + @Test + public void test32() { + String src = """ + struct Foo { + var bar: Int + } + func returnsFoo()->Foo? { + return new Foo{}; + } + func test() { + var foo: Foo? + foo = null; + foo = returnsFoo(); + } +"""; + analyze(src, "test"); + } +} From eccdc855803c21f6554f3a27e24b3d6848fe397f Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Sat, 4 Jul 2026 21:09:02 +0100 Subject: [PATCH 3/9] experiment with flow type analysis --- .../ezlang/semantic/FlowCFG.java | 220 ++++++++++++++++-- .../ezlang/semantic/NullableAnalysis.java | 81 ++++--- .../ezlang/semantic/TestSemaFlowGraph.java | 75 +++++- 3 files changed, 324 insertions(+), 52 deletions(-) diff --git a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java index 0881913..9c5d563 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java @@ -1,6 +1,7 @@ package com.compilerprogramming.ezlang.semantic; import com.compilerprogramming.ezlang.parser.AST; +import com.compilerprogramming.ezlang.parser.ASTVisitor; import java.util.*; @@ -30,7 +31,7 @@ public static final class FlowEdge { @Override public String toString() { if (condition != null) { - return "B" + from.id + " -" + kind + "(" + condition + ")-> B" + to.id; + return "B" + from.id + " -" + kind + "(" + FlowGraph.render(condition) + ")-> B" + to.id; } return "B" + from.id + " -" + kind + "-> B" + to.id; } @@ -100,11 +101,11 @@ public String toString() { } for (AST.Stmt s : b.statements) { - sb.append(" " + oneLine(s)).append('\n'); + sb.append(" " + render(s)).append('\n'); } if (b.condition != null) { - sb.append(" condition: " + oneLine(b.condition)).append('\n'); + sb.append(" condition: " + render(b.condition)).append('\n'); } for (FlowEdge e : b.succs) { @@ -126,11 +127,11 @@ public String toDot() { if (b == exit) sb.append("\\n"); for (AST.Stmt s : b.statements) { - sb.append("\\n").append(escape(oneLine(s))); + sb.append("\\n").append(escape(render(s))); } if (b.condition != null) { - sb.append("\\n? ").append(escape(oneLine(b.condition))); + sb.append("\\n? ").append(escape(render(b.condition))); } sb.append("\"];\n"); @@ -156,6 +157,39 @@ private static String oneLine(AST ast) { return ast.toString().replace("\n", " ").trim(); } + /* + * Logical descendants have their own CFG blocks. Avoid recursively + * printing them again as part of the enclosing AST node. + */ + private static String render(AST ast) { + String result = oneLine(ast); + List shortCircuitExpressions = new ArrayList<>(); + ast.accept(new ASTVisitor() { + @Override + public ASTVisitor enter(AST.BinaryExpr expr) { + if (expr.op.str.equals("&&") || expr.op.str.equals("||")) { + shortCircuitExpressions.add(oneLine(expr)); + return null; + } + return this; + } + + @Override + public ASTVisitor enter(AST.UnaryExpr expr) { + if (expr.op.str.equals("!")) { + shortCircuitExpressions.add(oneLine(expr)); + return null; + } + return this; + } + }); + + for (String expression : shortCircuitExpressions) { + result = result.replace(expression, ""); + } + return result; + } + private static String escape(String s) { return s.replace("\\", "\\\\").replace("\"", "\\\""); } @@ -225,6 +259,9 @@ private FlowBlock buildStmt(AST.Stmt stmt, FlowBlock cur) { } if (stmt instanceof AST.ReturnStmt ret) { + if (ret.expr != null) { + cur = buildExpr(ret.expr, cur); + } cur.statements.add(ret); addEdge(cur, graph.exit, EdgeKind.NORMAL, null); return newBlock(); @@ -242,10 +279,15 @@ private FlowBlock buildStmt(AST.Stmt stmt, FlowBlock cur) { return newBlock(); } - /* - * Ordinary non-branching statements are accumulated into the current block: - * AssignStmt, VarStmt, ExprStmt, VarDeclStmt, etc. - */ + if (stmt instanceof AST.AssignStmt assign) { + cur = buildExpr(assign.rhs, cur); + } else if (stmt instanceof AST.VarStmt var) { + cur = buildExpr(var.expr, cur); + } else if (stmt instanceof AST.ExprStmt exprStmt) { + cur = buildExpr(exprStmt.expr, cur); + } + + // Store the statement after the blocks needed to evaluate its expression. cur.statements.add(stmt); return cur; } @@ -257,6 +299,21 @@ private FlowBlock buildBlock(AST.BlockStmt block, FlowBlock cur) { return cur; } + /** + * Builds an if/else diamond and returns its join block. + * {@link #buildCondition} may insert additional blocks between {@code cur} + * and the two branch-entry blocks when the condition short-circuits. + * A branch that terminates (for example with return) has no edge to the join. + * + *
+     *                  +--TRUE--> thenBlock -> thenExit --NORMAL--+
+     * cur -> condition                                           +--> afterIf
+     *                  +--FALSE-> elseBlock -> elseExit --NORMAL--+
+     * 
+ * + * When there is no else statement, {@code elseBlock} is an empty block + * connected directly to {@code afterIf}. + */ private FlowBlock buildIf(AST.IfElseStmt ifs, FlowBlock cur) { FlowBlock thenBlock = newBlock(); FlowBlock elseBlock = newBlock(); @@ -281,6 +338,24 @@ private FlowBlock buildIf(AST.IfElseStmt ifs, FlowBlock cur) { return afterIf; } + /** + * Builds a loop with a dedicated condition entry and returns the block + * reached when the condition is false or a break is executed. + * {@link #buildCondition} may expand {@code condBlock} into several blocks. + * + *
+     *                                      +--------------------+
+     *                                      |                    |
+     * cur --NORMAL--> condBlock --TRUE--> bodyBlock -> bodyExit-+
+     *                       |
+     *                       +--FALSE--> afterLoop
+     *
+     * continue ---------------------> condBlock
+     * break    ---------------------> afterLoop
+     * 
+ * + * The loop-back edge is omitted when the body terminates. + */ private FlowBlock buildWhile(AST.WhileStmt wh, FlowBlock cur) { FlowBlock condBlock = newBlock(); FlowBlock bodyBlock = newBlock(); @@ -302,12 +377,30 @@ private FlowBlock buildWhile(AST.WhileStmt wh, FlowBlock cur) { return afterLoop; } - private void buildCondition( - AST.Expr expr, - FlowBlock from, - FlowBlock trueTarget, - FlowBlock falseTarget - ) { + /** + * Routes evaluation of {@code expr} to caller-supplied true and false + * targets. Logical operators are expanded recursively to preserve + * short-circuit evaluation: + * + *
+     * lhs && rhs:
+     *   from --lhs TRUE--> rhsBlock --rhs TRUE--> trueTarget
+     *     |                   +------rhs FALSE--> falseTarget
+     *     +------lhs FALSE----------------------> falseTarget
+     *
+     * lhs || rhs:
+     *   from --lhs TRUE-------------------------> trueTarget
+     *     +------lhs FALSE--> rhsBlock --rhs TRUE-> trueTarget
+     *                           +------rhs FALSE-> falseTarget
+     *
+     * !value: build value with trueTarget and falseTarget exchanged
+     * 
+ * + * For a non-logical root, expression children are evaluated first so any + * nested logical expressions get their own blocks. The resulting block is + * then marked with {@code expr} and receives TRUE and FALSE outgoing edges. + */ + private void buildCondition(AST.Expr expr,FlowBlock from,FlowBlock trueTarget,FlowBlock falseTarget) { if (expr instanceof AST.BinaryExpr bin) { String op = bin.op.str; @@ -333,8 +426,97 @@ private void buildCondition( return; } - from.condition = expr; - addEdge(from, trueTarget, EdgeKind.TRUE, expr); - addEdge(from, falseTarget, EdgeKind.FALSE, expr); + FlowBlock conditionBlock = buildExprChildren(expr, from); + conditionBlock.condition = expr; + addEdge(conditionBlock, trueTarget, EdgeKind.TRUE, expr); + addEdge(conditionBlock, falseTarget, EdgeKind.FALSE, expr); + } + + /** + * Builds the control flow needed to evaluate an expression used as a value. + * A logical expression has two short-circuit paths which rejoin once its + * boolean value has been determined. + */ + private FlowBlock buildExpr(AST.Expr expr, FlowBlock from) { + if (isLogical(expr)) { + FlowBlock trueBlock = newBlock(); + FlowBlock falseBlock = newBlock(); + FlowBlock afterExpr = newBlock(); + + buildCondition(expr, from, trueBlock, falseBlock); + addEdge(trueBlock, afterExpr, EdgeKind.NORMAL, null); + addEdge(falseBlock, afterExpr, EdgeKind.NORMAL, null); + return afterExpr; + } + + return buildExprChildren(expr, from); + } + + /** + * Recurses through immediate expression children in evaluation order. + */ + private FlowBlock buildExprChildren(AST.Expr expr, FlowBlock cur) { + if (expr instanceof AST.BinaryExpr binary) { + cur = buildExpr(binary.expr1, cur); + return buildExpr(binary.expr2, cur); + } + + if (expr instanceof AST.UnaryExpr unary) { + return buildExpr(unary.expr, cur); + } + + if (expr instanceof AST.ArrayStoreExpr store) { + cur = buildExpr(store.array, cur); + cur = buildExpr(store.expr, cur); + return buildExpr(store.value, cur); + } + + if (expr instanceof AST.ArrayLoadExpr load) { + cur = buildExpr(load.array, cur); + return buildExpr(load.expr, cur); + } + + if (expr instanceof AST.SetFieldExpr set) { + cur = buildExpr(set.object, cur); + return buildExpr(set.value, cur); + } + + if (expr instanceof AST.GetFieldExpr get) { + return buildExpr(get.object, cur); + } + + if (expr instanceof AST.CallExpr call) { + cur = buildExpr(call.callee, cur); + for (AST.Expr arg : call.args) { + cur = buildExpr(arg, cur); + } + return cur; + } + + if (expr instanceof AST.NewExpr newExpr) { + if (newExpr.len != null) { + cur = buildExpr(newExpr.len, cur); + } + if (newExpr.initValue != null) { + cur = buildExpr(newExpr.initValue, cur); + } + return cur; + } + + if (expr instanceof AST.InitExpr init) { + cur = buildExpr(init.newExpr, cur); + for (AST.Expr initializer : init.initExprList) { + cur = buildExpr(initializer, cur); + } + } + + return cur; + } + + private boolean isLogical(AST.Expr expr) { + if (expr instanceof AST.BinaryExpr binary) { + return binary.op.str.equals("&&") || binary.op.str.equals("||"); + } + return expr instanceof AST.UnaryExpr unary && unary.op.str.equals("!"); } -} \ No newline at end of file +} 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 1d7fd67..b60aa7d 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -213,29 +213,29 @@ public int hashCode() { return Objects.hash(kind, intValue); } } - static final class Facts { + static final class Lattice { final LatticeElement[] vars; - Facts(List elementList) { + Lattice(List elementList) { vars = elementList.toArray(new LatticeElement[0]); } - Facts(LatticeElement[] elements) { + Lattice(LatticeElement[] elements) { vars = elements; } - Facts copy() { + Lattice copy() { LatticeElement[] copyvars = new LatticeElement[vars.length]; for (int i = 0; i < copyvars.length; i++) copyvars[i] = vars[i].copy(); - return new Facts(copyvars); + return new Lattice(copyvars); } LatticeElement get(int reg) { return vars[reg]; } - static Facts merge(Facts a, Facts b) { - Facts out = a.copy(); + static Lattice merge(Lattice a, Lattice b) { + Lattice out = a.copy(); for (int i = 0; i < out.vars.length; i++) { out.vars[i].meet(b.vars[i]); @@ -247,13 +247,33 @@ static Facts merge(Facts a, Facts b) { @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; - Facts facts = (Facts) o; - return Objects.deepEquals(vars, facts.vars); + Lattice lattice = (Lattice) o; + return Objects.deepEquals(vars, lattice.vars); } } - LatticeElement analyzeExpr(AST.Expr e, Facts facts) { - if (e instanceof AST.NewExpr || e instanceof AST.InitExpr) { + LatticeElement analyzeExpr(AST.Expr e, Lattice facts) { + if (e instanceof AST.NewExpr) { + return new LatticeElement(F_NOT_NULL); + } + + if (e instanceof AST.InitExpr initExpr) { + if (initExpr.newExpr.type instanceof EZType.EZTypeStruct typeStruct) { + for (AST.Expr expr: initExpr.initExprList) { + if (expr instanceof AST.SetFieldExpr setFieldExpr) { + var fieldType = typeStruct.getField(setFieldExpr.fieldName); + var latticeElement = analyzeExpr(setFieldExpr.value,facts); + checkAssignment(fieldType,latticeElement); + } + } + } + else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { + var elemType = arrayType.getElementType(); + for (AST.Expr expr: initExpr.initExprList) { + var latticeElement = analyzeExpr(expr,facts); + checkAssignment(elemType,latticeElement); + } + } return new LatticeElement(F_NOT_NULL); } @@ -398,29 +418,29 @@ else if (!type.isPrimitive()) public void doAnalysis(FlowCFG.FlowGraph cfg) { Queue worklist = new ArrayDeque<>(); - Map in = new HashMap<>(); - Map out = new HashMap<>(); + Map in = new HashMap<>(); + Map out = new HashMap<>(); - var initialFacts = new Facts(latticeElements); + var initialFacts = new Lattice(latticeElements); in.put(cfg.entry, initialFacts); worklist.add(cfg.entry); while (!worklist.isEmpty()) { FlowCFG.FlowBlock b = worklist.remove(); - Facts inFacts = in.get(b); - if (inFacts == null) + Lattice inLattice = in.get(b); + if (inLattice == null) throw new CompilerException("Missing facts"); - Facts outFacts = transferBlock(b, inFacts); + Lattice outLattice = transferBlock(b, inLattice); - if (!outFacts.equals(out.get(b))) { - out.put(b, outFacts); + if (!outLattice.equals(out.get(b))) { + out.put(b, outLattice); for (FlowCFG.FlowEdge e : b.succs) { - Facts edgeFacts = applyEdgeFacts(outFacts, e); + Lattice edgeLattice = applyEdgeFacts(outLattice, e); - Facts oldIn = in.get(e.to); - Facts newIn = oldIn == null ? edgeFacts : Facts.merge(oldIn, edgeFacts); + Lattice oldIn = in.get(e.to); + Lattice newIn = oldIn == null ? edgeLattice : Lattice.merge(oldIn, edgeLattice); if (!newIn.equals(oldIn)) { in.put(e.to, newIn); @@ -431,14 +451,14 @@ public void doAnalysis(FlowCFG.FlowGraph cfg) { } } - Facts transferBlock(FlowCFG.FlowBlock block, Facts in) { - Facts facts = in.copy(); + Lattice transferBlock(FlowCFG.FlowBlock block, Lattice in) { + Lattice lattice = in.copy(); for (AST.Stmt stmt : block.statements) { - transferStmt(stmt, facts); + transferStmt(stmt, lattice); } - return facts; + return lattice; } private void checkAssignment(EZType type, LatticeElement lattice) { @@ -458,7 +478,7 @@ private void checkAssignment(EZType type, LatticeElement lattice) { } } - void transferStmt(AST.Stmt stmt, Facts facts) { + void transferStmt(AST.Stmt stmt, Lattice facts) { if (stmt instanceof AST.AssignStmt assign) { Symbol.VarSymbol sym = (Symbol.VarSymbol) assign.nameExpr.symbol; var lattice = analyzeExpr(assign.rhs, facts); @@ -508,9 +528,8 @@ else if (arrayStoreExpr.array.type instanceof EZType.EZTypeNullable ptr && } } - - Facts applyEdgeFacts(Facts in, FlowCFG.FlowEdge edge) { - Facts out = in.copy(); + Lattice applyEdgeFacts(Lattice in, FlowCFG.FlowEdge edge) { + Lattice out = in.copy(); if (edge.kind == FlowCFG.EdgeKind.TRUE) { applyConditionFacts(out, edge.condition, true); @@ -520,7 +539,7 @@ Facts applyEdgeFacts(Facts in, FlowCFG.FlowEdge edge) { return out; } - void applyConditionFacts(Facts facts, AST.Expr expr, boolean branchIsTrue) { + void applyConditionFacts(Lattice facts, AST.Expr expr, boolean branchIsTrue) { if (!(expr instanceof AST.BinaryExpr bin)) { return; } diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java index 59dd83a..1559eb5 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java @@ -10,7 +10,7 @@ public class TestSemaFlowGraph { - private String test(String src, String symbolName) { + private FlowCFG.FlowGraph test(String src, String symbolName) { Parser parser = new Parser(); var program = parser.parse(new Lexer(src)); var typeDict = new TypeDictionary(); @@ -25,7 +25,7 @@ private String test(String src, String symbolName) { var flowGraph = FlowCFG.build(fnDecl); var dot = flowGraph.toDot(); System.out.println(dot); - return flowGraph.toString(); + return flowGraph; } return null; } @@ -113,4 +113,75 @@ func foo(a: Int, b: Int) System.out.println(result); } + @Test + public void test6() { + String src = """ + func bar(i: Int)->Int { return 0; } + func foo(a: Int, b: Int) + { + while (a && b) { + bar(a+b + (bar(a) || bar(b))); + b = a; + a = 0; + } + } +"""; + var result = test(src, "foo"); + var text = result.toString(); + var dot = result.toDot(); + Assert.assertTrue(text.contains("")); + Assert.assertTrue(dot.contains("")); + Assert.assertFalse(text.contains("(bar(a)||bar(b))")); + Assert.assertFalse(dot.contains("(bar(a)||bar(b))")); + System.out.println(result); + } + + + @Test + public void testLogicalOperatorsNestedInConditionExpressions() { + String src = """ + func bar(i: Int) {} + func foo(a: Int, b: Int, c: Int, d: Int) + { + if ((a && b) == (c || d)) + bar(1); + } +"""; + var graph = test(src, "foo"); + + var conditions = graph.blocks.stream() + .map(block -> block.condition) + .filter(java.util.Objects::nonNull) + .map(Object::toString) + .toList(); + + Assert.assertEquals(5, conditions.size()); + Assert.assertTrue(conditions.contains("((a&&b)==(c||d))")); + Assert.assertTrue(conditions.containsAll( + java.util.List.of("a", "b", "c", "d"))); + } + + @Test + public void testLogicalOperatorsNestedInStatementExpressions() { + String src = """ + func bar(i: Int)->Int { return i; } + func foo(a: Int, b: Int)->Int + { + var value = bar(a && b); + value = a || b; + return value + (a && b); + } +"""; + var graph = test(src, "foo"); + + long conditionCount = graph.blocks.stream() + .filter(block -> block.condition != null) + .count(); + Assert.assertEquals(6, conditionCount); + + long statementCount = graph.blocks.stream() + .flatMap(block -> block.statements.stream()) + .count(); + Assert.assertEquals(3, statementCount); + } } From 3e15ffed7f3a8a0f5f1a292bb2a29e579b9c24d5 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Sat, 4 Jul 2026 23:37:30 +0100 Subject: [PATCH 4/9] experiment with flow type analysis --- .../ezlang/semantic/NullableAnalysis.java | 385 ++++++++++++------ 1 file changed, 253 insertions(+), 132 deletions(-) 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 b60aa7d..cfe5d74 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -31,20 +31,10 @@ private void setVirtualRegisters(Scope scope) { for (Symbol symbol: scope.getLocalSymbols()) { if (symbol instanceof Symbol.VarSymbol varSymbol) { varSymbol.regNumber = reg++; - LatticeElement elem = null; - if (varSymbol instanceof Symbol.ParameterSymbol) { - var type = varSymbol.type; - if (type.isPrimitive()) - // Some int value, as our only primitive type is int - elem = new LatticeElement(F_INT); - else if (type instanceof EZType.EZTypeNullable) - elem = new LatticeElement(F_MAYBE_NULL); - else - elem = new LatticeElement(F_NOT_NULL); - } - else { - elem = new LatticeElement(F_UNKNOWN); - } + var type = varSymbol.type; + LatticeElement elem = type.isPrimitive() + ? new LatticeElement(F_INT_TOP) + : new LatticeElement(F_REF_TOP); latticeElements.add(elem); } } @@ -53,23 +43,36 @@ else if (type instanceof EZType.EZTypeNullable) } } - static final byte F_UNKNOWN = 0; - static final byte F_NOT_NULL = 1; - static final byte F_NULL = 2; - static final byte F_MAYBE_NULL = 3; // BOTTOM - static final byte F_ZERO = 4; - static final byte F_NONZERO_CONST = 5; - static final byte F_NONZERO_VARYING = 6; - static final byte F_INT = 7; + // 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 + 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_BOTTOM = 10; // impossible / contradiction // Associated with each register static final class LatticeElement { public byte kind; private long intValue; - public LatticeElement() { - this.kind = F_UNKNOWN; - } public LatticeElement(byte kind) { this.kind = kind; } @@ -81,124 +84,237 @@ public LatticeElement(long value) { setIntValue(value); } - boolean nullable() { - return kind == F_NULL || kind == F_NOT_NULL || kind == F_MAYBE_NULL; - } - boolean someInt() { - return kind == F_INT || kind == F_ZERO || kind == F_NONZERO_CONST || kind == F_NONZERO_VARYING; - } +// boolean nullable() { +// return kind == F_NULL || kind == F_NOT_NULL || kind == F_MAYBE_NULL; +// } +// boolean someInt() { +// return kind == F_INT || kind == F_ZERO || kind == F_NONZERO_CONST || kind == F_NONZERO_VARYING; +// } LatticeElement copy() { return new LatticeElement(kind,intValue); } boolean isTrue() { - return kind == F_NONZERO_CONST || kind == F_NONZERO_VARYING; + return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; } boolean isFalse() { - return kind == F_ZERO; + return kind == F_INT_ZERO; } boolean isIntegerConstant() { - return kind == F_ZERO || kind == F_NONZERO_CONST; + return kind == F_INT_ZERO || kind == F_INT_NONZERO_CONST; } boolean setIntValue(long value) { byte prevKind = kind; - if (kind == F_UNKNOWN) { + if (kind == F_INT_TOP) { this.intValue = value; - this.kind = value == 0 ? F_ZERO : F_NONZERO_CONST; + this.kind = value == 0 ? F_INT_ZERO : F_INT_NONZERO_CONST; } - else if (kind == F_ZERO && value != 0) { - this.kind = F_INT; + else if (kind == F_INT_ZERO && value != 0) { + this.kind = F_INT_BOTTOM; } - else if (kind == F_NONZERO_CONST && (value == 0 || value != intValue)) { - this.kind = F_INT; + else if (kind == F_INT_NONZERO_CONST && (value == 0 || value != intValue)) { + this.kind = F_INT_BOTTOM; } - else if (nullable()) { - throw new CompilerException("Cannot assign integer value to lattice cell that is nullable"); + else if (!isInteger()) { + throw new CompilerException("Cannot assign integer value to reference type"); } return (kind != prevKind); } +// +// boolean setNull() { +// byte prevKind = kind; +// if (kind == F_UNKNOWN) +// kind = F_NULL; +// else if (kind == F_NOT_NULL) +// kind = F_MAYBE_NULL; +// else if (someInt()) { +// throw new CompilerException("Cannot assign null to lattice cell that is int type"); +// } +// return (kind != prevKind); +// } - boolean setNull() { - byte prevKind = kind; - if (kind == F_UNKNOWN) - kind = F_NULL; - else if (kind == F_NOT_NULL) - kind = F_MAYBE_NULL; - else if (someInt()) { - throw new CompilerException("Cannot assign null to lattice cell that is int type"); + + boolean meet(LatticeElement other) { + byte old = kind; + + // universal top/bottom + if (kind == F_TOP) { + copyFrom(other); + return true; } - return (kind != prevKind); - } - boolean setNotNull() { - byte prevKind = kind; - if (kind == F_UNKNOWN) - kind = F_NOT_NULL; - else if (kind == F_NULL) - kind = F_MAYBE_NULL; - else if (someInt()) { - throw new CompilerException("Cannot assign not null to lattice cell that is int type"); + if (kind == F_BOTTOM || other.kind == F_TOP) + return false; + + if (other.kind == F_BOTTOM) { + kind = F_BOTTOM; + return kind != old; } - return (kind != prevKind); - } - boolean setMaybeNull() { - byte prevKind = kind; - if (kind == F_UNKNOWN) - kind = F_MAYBE_NULL; - else if (kind == F_NULL || kind == F_NOT_NULL) - kind = F_MAYBE_NULL; - else if (someInt()) { - throw new CompilerException("Cannot assign maybe null to lattice cell that is int type"); + // same value + if (kind == other.kind) { + if (kind == F_INT_NONZERO_CONST && + intValue != other.intValue) { + kind = F_INT_NONZERO_VARYING; + } + return kind != old; } - return (kind != prevKind); - } - boolean meet(LatticeElement other) { - byte oldKind = this.kind; - if (kind == F_UNKNOWN) { - kind = other.kind; + // reference lattice + if (isReference(kind) && isReference(other.kind)) { + kind = meetReference(other); + return kind != old; } - else if (nullable() && other.nullable()) { - if (kind == F_NULL && other.kind == F_NOT_NULL || - kind == F_NOT_NULL && other.kind == F_NULL) - kind = F_MAYBE_NULL; + + // integer lattice + if (isInteger(kind) && isInteger(other.kind)) { + meetInteger(other); + return kind != old; } - else if (someInt() && other.someInt()) { - if (kind == F_ZERO) { - if (other.kind == F_NONZERO_CONST || other.kind == F_NONZERO_VARYING) { - kind = F_INT; - } + + // 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; } - else if (kind == F_NONZERO_CONST) { - if (other.kind == F_NONZERO_CONST && intValue != other.intValue) { - kind = F_INT; - } - else if (other.kind == F_ZERO) { - kind = F_INT; + + 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 (kind == F_NONZERO_VARYING && other.kind == F_ZERO) { - kind = F_INT; + + case F_INT_NONZERO_VARYING -> { + if (other.kind == F_INT_ZERO || + other.kind == F_INT_BOTTOM) + kind = F_INT_BOTTOM; + } + + case F_INT_BOTTOM -> { } } - return kind != oldKind; + } + 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; + } + void copyFrom(LatticeElement other) { + this.kind = other.kind; + this.intValue = other.intValue; + } + boolean isTop() { + return kind == F_TOP; + } + + boolean isBottom() { + return kind == F_BOTTOM; + } + + boolean isReference() { + return isReference(kind); + } + + boolean isInteger() { + return isInteger(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() { - switch (kind) { - case F_UNKNOWN -> { - return "unknown"; - } - case F_NULL -> { return "null"; } - case F_NOT_NULL -> { return "not null"; } - case F_MAYBE_NULL -> { return "maybe null"; } - case F_ZERO -> { return "zero"; } - case F_NONZERO_CONST -> { return "non-zero const"; } - case F_NONZERO_VARYING -> { return "non-zero varying"; } - case F_INT -> { return "int"; } - default -> throw new CompilerException("Unknown type in lattice"); - } + 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_BOTTOM -> "⊥"; + + default -> throw new CompilerException("Unknown lattice kind: " + kind); + }; } @Override @@ -254,7 +370,7 @@ public boolean equals(Object o) { LatticeElement analyzeExpr(AST.Expr e, Lattice facts) { if (e instanceof AST.NewExpr) { - return new LatticeElement(F_NOT_NULL); + return new LatticeElement(F_REF_NOT_NULL); } if (e instanceof AST.InitExpr initExpr) { @@ -274,7 +390,7 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { checkAssignment(elemType,latticeElement); } } - return new LatticeElement(F_NOT_NULL); + return new LatticeElement(F_REF_NOT_NULL); } if (e instanceof AST.NameExpr name && @@ -311,7 +427,7 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { if (e instanceof AST.LiteralExpr lit) { if (lit.value.str.equals("null")) { - return new LatticeElement(F_NULL); + return new LatticeElement(F_REF_NULL); } if (lit.value.kind == Token.Kind.NUM) { return new LatticeElement(Long.parseLong(lit.value.str)); @@ -322,7 +438,7 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { LatticeElement v = analyzeExpr(un.expr, facts); if (un.op.str.equals("-")) { - if (v.someInt()) + if (v.isInteger()) v.setIntValue(-v.intValue); else throw new CompilerException("Cannot apply - to non integer"); @@ -331,8 +447,8 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { if (un.op.str.equals("!")) { if (v.isTrue()) return new LatticeElement(0L); if (v.isFalse()) return new LatticeElement(1L); - return new LatticeElement(F_INT); } + return new LatticeElement(F_INT_BOTTOM); } if (e instanceof AST.BinaryExpr bin) { @@ -357,12 +473,12 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { isArith = true; } case "/" -> { - if (b.kind == F_ZERO) + if (b.kind == F_INT_ZERO) throw new CompilerException("Division by zero"); value = a.intValue / b.intValue; } case "%" -> { - if (b.kind == F_ZERO) + if (b.kind == F_INT_ZERO) throw new CompilerException("Division by zero"); value = a.intValue % b.intValue; } @@ -392,27 +508,30 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { result = new LatticeElement(value); else { if (value == 1) - result = new LatticeElement(F_NONZERO_CONST,1); + result = new LatticeElement(F_INT_NONZERO_CONST,1); else - result = new LatticeElement(F_ZERO); + result = new LatticeElement(F_INT_ZERO); } return result; } } - return new LatticeElement(); + return new LatticeElement(F_BOTTOM); } LatticeElement factFromType(EZType type) { if (type != null) { if (type instanceof EZType.EZTypeNullable) - return new LatticeElement(F_MAYBE_NULL); + return new LatticeElement(F_REF_BOTTOM); else if (type instanceof EZType.EZTypeNull) - return new LatticeElement(F_NULL); - else if (!type.isPrimitive()) - return new LatticeElement(F_NOT_NULL); - } - return new LatticeElement(); + 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.isPrimitive()) + return new LatticeElement(F_INT_BOTTOM); + } + return new LatticeElement(F_BOTTOM); } public void doAnalysis(FlowCFG.FlowGraph cfg) { @@ -463,17 +582,19 @@ Lattice transferBlock(FlowCFG.FlowBlock block, Lattice in) { private void checkAssignment(EZType type, LatticeElement lattice) { if (type.isPrimitive()) { - if (!lattice.someInt() && lattice.kind != F_UNKNOWN) + if (!lattice.isInteger()) throw new CompilerException("Cannot assign reference/null to int"); return; } if (type instanceof EZType.EZTypeNullable) { - return; // null allowed + if (!lattice.isReference()) + throw new CompilerException("Cannot assign non reference value to reference type"); + return; } // non-null reference - if (lattice.kind == F_NULL || lattice.kind == F_MAYBE_NULL) { + if (lattice.isNull() || lattice.isMaybeNull()) { throw new CompilerException("Cannot assign null or potentially null value"); } } @@ -545,10 +666,10 @@ void applyConditionFacts(Lattice facts, AST.Expr expr, boolean branchIsTrue) { } boolean leftNameRightNull = - isName(bin.expr1) && analyzeExpr(bin.expr2, facts).kind == F_NULL; + isName(bin.expr1) && analyzeExpr(bin.expr2, facts).isNull(); boolean rightNameLeftNull = - isName(bin.expr2) && analyzeExpr(bin.expr1, facts).kind == F_NULL; + isName(bin.expr2) && analyzeExpr(bin.expr1, facts).isNull(); if (!leftNameRightNull && !rightNameLeftNull) { return; @@ -560,12 +681,12 @@ void applyConditionFacts(Lattice facts, AST.Expr expr, boolean branchIsTrue) { LatticeElement lattice; if (branchIsTrue) { lattice = op.equals("!=") - ? new LatticeElement(F_NOT_NULL) - : new LatticeElement(F_NULL); + ? new LatticeElement(F_REF_NOT_NULL) + : new LatticeElement(F_REF_NULL); } else { lattice = op.equals("!=") - ? new LatticeElement(F_NULL) - : new LatticeElement(F_NOT_NULL); + ? new LatticeElement(F_REF_NULL) + : new LatticeElement(F_REF_NOT_NULL); } facts.vars[varSymbol.regNumber] = lattice; } From fe5e90da2f54a927744c83c32401c7cf7fa2473c Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Sat, 4 Jul 2026 23:55:27 +0100 Subject: [PATCH 5/9] experiment with flow type analysis --- .../ezlang/semantic/NullableAnalysis.java | 88 ++++++++++++------- .../ezlang/semantic/TestNullAnalysis.java | 79 +++++++++++++++++ 2 files changed, 133 insertions(+), 34 deletions(-) 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 cfe5d74..53f22dd 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -22,9 +22,11 @@ public class NullableAnalysis { int reg = 0; ArrayList latticeElements = new ArrayList<>(); + private final EZType returnType; public NullableAnalysis(Symbol.FunctionTypeSymbol functionSymbol, TypeDictionary typeDictionary) { AST.FuncDecl funcDecl = (AST.FuncDecl) functionSymbol.functionDecl; + returnType = ((EZType.EZTypeFunction) functionSymbol.type).returnType; setVirtualRegisters(funcDecl.scope); } private void setVirtualRegisters(Scope scope) { @@ -32,9 +34,20 @@ private void setVirtualRegisters(Scope scope) { if (symbol instanceof Symbol.VarSymbol varSymbol) { varSymbol.regNumber = reg++; var type = varSymbol.type; - LatticeElement elem = type.isPrimitive() - ? new LatticeElement(F_INT_TOP) - : new LatticeElement(F_REF_TOP); + LatticeElement elem; + if (varSymbol instanceof Symbol.ParameterSymbol) { + if (type.isPrimitive()) + elem = new LatticeElement(F_INT_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() + ? new LatticeElement(F_INT_TOP) + : new LatticeElement(F_REF_TOP); + } latticeElements.add(elem); } } @@ -81,15 +94,9 @@ public LatticeElement(byte kind,long value) { this.intValue = value; } public LatticeElement(long value) { + kind = F_INT_TOP; setIntValue(value); } - -// boolean nullable() { -// return kind == F_NULL || kind == F_NOT_NULL || kind == F_MAYBE_NULL; -// } -// boolean someInt() { -// return kind == F_INT || kind == F_ZERO || kind == F_NONZERO_CONST || kind == F_NONZERO_VARYING; -// } LatticeElement copy() { return new LatticeElement(kind,intValue); } @@ -119,20 +126,6 @@ else if (!isInteger()) { } return (kind != prevKind); } -// -// boolean setNull() { -// byte prevKind = kind; -// if (kind == F_UNKNOWN) -// kind = F_NULL; -// else if (kind == F_NOT_NULL) -// kind = F_MAYBE_NULL; -// else if (someInt()) { -// throw new CompilerException("Cannot assign null to lattice cell that is int type"); -// } -// return (kind != prevKind); -// } - - boolean meet(LatticeElement other) { byte old = kind; @@ -414,14 +407,13 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { } if (e instanceof AST.GetFieldExpr field) { - // First check object is non-null. - // Then use field type. + checkDereference(field.object, facts); return factFromType(field.type); } if (e instanceof AST.ArrayLoadExpr arrayLoad) { - // First check array is non-null. - // Then use element type. + checkDereference(arrayLoad.array, facts); + analyzeExpr(arrayLoad.expr, facts); return factFromType(arrayLoad.type); } @@ -438,17 +430,19 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { LatticeElement v = analyzeExpr(un.expr, facts); if (un.op.str.equals("-")) { + if (v.isIntegerConstant()) + return new LatticeElement(-v.intValue); if (v.isInteger()) - v.setIntValue(-v.intValue); - else - throw new CompilerException("Cannot apply - to non integer"); + return new LatticeElement(F_INT_BOTTOM); + throw new CompilerException("Cannot apply - to non integer"); } if (un.op.str.equals("!")) { if (v.isTrue()) return new LatticeElement(0L); if (v.isFalse()) return new LatticeElement(1L); + return new LatticeElement(F_INT_BOTTOM); } - return new LatticeElement(F_INT_BOTTOM); + return factFromType(un.type); } if (e instanceof AST.BinaryExpr bin) { @@ -514,6 +508,7 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { } return result; } + return factFromType(bin.type); } return new LatticeElement(F_BOTTOM); @@ -594,11 +589,26 @@ private void checkAssignment(EZType type, LatticeElement lattice) { } // non-null reference - if (lattice.isNull() || lattice.isMaybeNull()) { + if (!lattice.isReference() || lattice.isNull() || lattice.isMaybeNull()) { throw new CompilerException("Cannot assign null or potentially null value"); } } + private void checkDereference(AST.Expr receiver, Lattice facts) { + LatticeElement lattice = analyzeExpr(receiver, facts); + if (!lattice.isNotNull()) + throw new CompilerException("Cannot dereference null or potentially null value", receiver.lineNumber); + } + + private EZType.EZTypeStruct structType(EZType type) { + if (type instanceof EZType.EZTypeStruct structType) + return structType; + if (type instanceof EZType.EZTypeNullable nullable && + nullable.baseType instanceof EZType.EZTypeStruct structType) + return structType; + throw new CompilerException("Expected struct type"); + } + void transferStmt(AST.Stmt stmt, Lattice facts) { if (stmt instanceof AST.AssignStmt assign) { Symbol.VarSymbol sym = (Symbol.VarSymbol) assign.nameExpr.symbol; @@ -618,7 +628,8 @@ void transferStmt(AST.Stmt stmt, Lattice facts) { if (stmt instanceof AST.ExprStmt exprStmt && exprStmt.expr instanceof AST.SetFieldExpr setFieldExpr) { - EZType.EZTypeStruct structType = (EZType.EZTypeStruct) setFieldExpr.object.type; + checkDereference(setFieldExpr.object, facts); + EZType.EZTypeStruct structType = structType(setFieldExpr.object.type); EZType fieldType = structType.getField(setFieldExpr.fieldName); var lattice = analyzeExpr(setFieldExpr.value,facts); checkAssignment(fieldType,lattice); @@ -627,6 +638,8 @@ void transferStmt(AST.Stmt stmt, Lattice facts) { if (stmt instanceof AST.ExprStmt exprStmt && exprStmt.expr instanceof AST.ArrayStoreExpr arrayStoreExpr) { + checkDereference(arrayStoreExpr.array, facts); + analyzeExpr(arrayStoreExpr.expr, facts); EZType.EZTypeArray arrayType = null; EZType elementType = null; if (arrayStoreExpr.array.type instanceof EZType.EZTypeArray ta) { @@ -641,11 +654,18 @@ else if (arrayStoreExpr.array.type instanceof EZType.EZTypeNullable ptr && elementType = arrayType.getElementType(); var lattice = analyzeExpr(arrayStoreExpr.value,facts); checkAssignment(elementType,lattice); + return; } if (stmt instanceof AST.ExprStmt exprStmt && exprStmt.expr instanceof AST.CallExpr callExpr) { analyzeExpr(callExpr,facts); + return; + } + + if (stmt instanceof AST.ReturnStmt returnStmt && returnStmt.expr != null) { + LatticeElement lattice = analyzeExpr(returnStmt.expr, facts); + checkAssignment(returnType, lattice); } } diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java index aea774b..ec9806d 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java @@ -155,6 +155,85 @@ func test() { foo = null; foo = returnsFoo(); } +"""; + analyze(src, "test"); + } + + @Test + public void numericLiteralAndNonConstantArithmetic() { + String src = """ + func takesInt(arg: Int) { + } + func test(a: Int, b: Int) { + takesInt(1); + takesInt(a + b); + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void nullableReturnToNonNullType() { + String src = """ + struct Foo { var bar: Int } + func test(arg: Foo?)->Foo { + return arg; + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void nullableFieldDereference() { + String src = """ + struct Foo { var bar: Int } + func test(arg: Foo?)->Int { + return arg.bar; + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void nullableArrayDereference() { + String src = """ + func test(arg: [Int]?)->Int { + return arg[0]; + } +"""; + analyze(src, "test"); + } + + @Test + public void guardedNullableFieldStore() { + String src = """ + struct Foo { var bar: Int } + func test(arg: Foo?) { + if (arg != null) { + arg.bar = 1; + } + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void nullableFieldStore() { + String src = """ + struct Foo { var bar: Int } + func test(arg: Foo?) { + arg.bar = 1; + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void nullableArrayStore() { + String src = """ + func test(arg: [Int]?) { + arg[0] = 1; + } """; analyze(src, "test"); } From 9a64f57f353a19059631861e0d82f3923fe3c928 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Sun, 5 Jul 2026 00:25:27 +0100 Subject: [PATCH 6/9] experiment with flow type analysis --- .../ezlang/compiler/Compiler.java | 2 + .../ezlang/compiler/Compiler.java | 2 + .../ezlang/compiler/Compiler.java | 2 + .../ezlang/semantic/NullableAnalysis.java | 105 +++++++++++++++--- .../ezlang/semantic/TestNullAnalysis.java | 66 +++++++++-- .../ezlang/compiler/Compiler.java | 2 + 6 files changed, 159 insertions(+), 20 deletions(-) diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java index 7f33025..bebe519 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java @@ -2,6 +2,7 @@ import com.compilerprogramming.ezlang.lexer.Lexer; import com.compilerprogramming.ezlang.parser.Parser; +import com.compilerprogramming.ezlang.semantic.NullableAnalysis; import com.compilerprogramming.ezlang.semantic.SemaAssignTypes; import com.compilerprogramming.ezlang.semantic.SemaDefineTypes; import com.compilerprogramming.ezlang.types.Symbol; @@ -35,6 +36,7 @@ public TypeDictionary compileSrc(String src, EnumSet options) { sema.analyze(program); var sema2 = new SemaAssignTypes(typeDict); sema2.analyze(program); + NullableAnalysis.analyze(typeDict); compile(typeDict, options); return typeDict; } diff --git a/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java b/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java index d76569b..f4b02b8 100644 --- a/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java +++ b/registervm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java @@ -2,6 +2,7 @@ import com.compilerprogramming.ezlang.lexer.Lexer; import com.compilerprogramming.ezlang.parser.Parser; +import com.compilerprogramming.ezlang.semantic.NullableAnalysis; import com.compilerprogramming.ezlang.semantic.SemaAssignTypes; import com.compilerprogramming.ezlang.semantic.SemaDefineTypes; import com.compilerprogramming.ezlang.types.Symbol; @@ -28,6 +29,7 @@ public TypeDictionary compileSrc(String src) { sema.analyze(program); var sema2 = new SemaAssignTypes(typeDict); sema2.analyze(program); + NullableAnalysis.analyze(typeDict); compile(typeDict); return typeDict; } 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 8d0b880..a93e5cb 100644 --- a/seaofnodes/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java +++ b/seaofnodes/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java @@ -9,6 +9,7 @@ import com.compilerprogramming.ezlang.lexer.Lexer; import com.compilerprogramming.ezlang.parser.AST; import com.compilerprogramming.ezlang.parser.Parser; +import com.compilerprogramming.ezlang.semantic.NullableAnalysis; import com.compilerprogramming.ezlang.semantic.SemaAssignTypes; import com.compilerprogramming.ezlang.semantic.SemaDefineTypes; import com.compilerprogramming.ezlang.types.Scope; @@ -86,6 +87,7 @@ public TypeDictionary createAST(String src) { sema.analyze(program); var sema2 = new SemaAssignTypes(typeDict); sema2.analyze(program); + NullableAnalysis.analyze(typeDict); return typeDict; } 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 53f22dd..1c4e0bd 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -24,10 +24,21 @@ public class NullableAnalysis { ArrayList latticeElements = new ArrayList<>(); private final EZType returnType; - public NullableAnalysis(Symbol.FunctionTypeSymbol functionSymbol, TypeDictionary typeDictionary) { + public static void analyze(TypeDictionary typeDictionary) { + for (Symbol symbol: typeDictionary.getLocalSymbols()) { + if (symbol instanceof Symbol.FunctionTypeSymbol functionSymbol) { + new NullableAnalysis(functionSymbol); + } + } + } + public NullableAnalysis(Symbol.FunctionTypeSymbol functionSymbol) { AST.FuncDecl funcDecl = (AST.FuncDecl) functionSymbol.functionDecl; + var flowGraph = FlowCFG.build(funcDecl); +// var dot = flowGraph.toDot(); +// System.out.println(dot); returnType = ((EZType.EZTypeFunction) functionSymbol.type).returnType; setVirtualRegisters(funcDecl.scope); + doAnalysis(flowGraph); } private void setVirtualRegisters(Scope scope) { for (Symbol symbol: scope.getLocalSymbols()) { @@ -85,6 +96,7 @@ else if (type instanceof EZType.EZTypeNullable) static final class LatticeElement { public byte kind; private long intValue; + private Map arrayElements; public LatticeElement(byte kind) { this.kind = kind; @@ -98,7 +110,28 @@ public LatticeElement(long value) { setIntValue(value); } LatticeElement copy() { - return new LatticeElement(kind,intValue); + LatticeElement copy = new LatticeElement(kind, intValue); + if (arrayElements != null) { + copy.arrayElements = new HashMap<>(); + for (var entry : arrayElements.entrySet()) + copy.arrayElements.put(entry.getKey(), entry.getValue().copy()); + } + return copy; + } + + void setArrayElement(long index, LatticeElement element) { + if (arrayElements == null) + arrayElements = new HashMap<>(); + arrayElements.put(index, element.copy()); + } + + LatticeElement getArrayElement(long index) { + LatticeElement element = arrayElements == null ? null : arrayElements.get(index); + return element == null ? null : element.copy(); + } + + void clearArrayElements() { + arrayElements = null; } boolean isTrue() { return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; @@ -128,6 +161,8 @@ else if (!isInteger()) { } boolean meet(LatticeElement other) { byte old = kind; + if (!Objects.equals(arrayElements, other.arrayElements)) + arrayElements = null; // universal top/bottom if (kind == F_TOP) { @@ -249,8 +284,10 @@ private static boolean isInteger(byte kind) { return kind >= F_INT_TOP && kind <= F_INT_BOTTOM; } void copyFrom(LatticeElement other) { - this.kind = other.kind; - this.intValue = other.intValue; + LatticeElement copy = other.copy(); + this.kind = copy.kind; + this.intValue = copy.intValue; + this.arrayElements = copy.arrayElements; } boolean isTop() { return kind == F_TOP; @@ -314,12 +351,13 @@ 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 && + Objects.equals(arrayElements, that.arrayElements); } @Override public int hashCode() { - return Objects.hash(kind, intValue); + return Objects.hash(kind, intValue, arrayElements); } } static final class Lattice { @@ -362,11 +400,19 @@ public boolean equals(Object o) { } LatticeElement analyzeExpr(AST.Expr e, Lattice facts) { - if (e instanceof AST.NewExpr) { + if (e instanceof AST.NewExpr newExpr) { + if (newExpr.type instanceof EZType.EZTypeArray arrayType) { + if (newExpr.len != null) + analyzeExpr(newExpr.len, facts); + if (newExpr.initValue != null) + checkAssignment(arrayType.getElementType(), analyzeExpr(newExpr.initValue, facts)); + } return new LatticeElement(F_REF_NOT_NULL); } if (e instanceof AST.InitExpr initExpr) { + analyzeExpr(initExpr.newExpr, facts); + LatticeElement result = new LatticeElement(F_REF_NOT_NULL); if (initExpr.newExpr.type instanceof EZType.EZTypeStruct typeStruct) { for (AST.Expr expr: initExpr.initExprList) { if (expr instanceof AST.SetFieldExpr setFieldExpr) { @@ -379,11 +425,19 @@ LatticeElement analyzeExpr(AST.Expr e, Lattice facts) { else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { var elemType = arrayType.getElementType(); for (AST.Expr expr: initExpr.initExprList) { - var latticeElement = analyzeExpr(expr,facts); + AST.Expr value = expr instanceof AST.ArrayInitExpr arrayInitExpr + ? arrayInitExpr.value + : expr; + var latticeElement = analyzeExpr(value,facts); checkAssignment(elemType,latticeElement); + if (expr instanceof AST.ArrayInitExpr arrayInitExpr) { + LatticeElement index = analyzeExpr(arrayInitExpr.expr, facts); + if (index.isIntegerConstant()) + result.setArrayElement(index.intValue, latticeElement); + } } } - return new LatticeElement(F_REF_NOT_NULL); + return result; } if (e instanceof AST.NameExpr name && @@ -400,6 +454,8 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { checkAssignment(type,lattice); } + invalidateArrayElements(facts); + // Use function return type: // Foo -> NON_NULL // Foo? -> UNKNOWN @@ -412,8 +468,13 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { } if (e instanceof AST.ArrayLoadExpr arrayLoad) { - checkDereference(arrayLoad.array, facts); - analyzeExpr(arrayLoad.expr, facts); + LatticeElement array = checkDereference(arrayLoad.array, facts); + LatticeElement index = analyzeExpr(arrayLoad.expr, facts); + if (index.isIntegerConstant()) { + LatticeElement element = array.getArrayElement(index.intValue); + if (element != null) + return element; + } return factFromType(arrayLoad.type); } @@ -495,6 +556,12 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { case ">=" -> { value = a.intValue >= b.intValue ? 1 : 0; } + case "&&" -> { + value = a.intValue != 0 && b.intValue != 0 ? 1 : 0; + } + case "||" -> { + value = a.intValue != 0 || b.intValue != 0 ? 1 : 0; + } default -> throw new CompilerException("Unknown binary operator " + bin.op.str); } @@ -594,10 +661,16 @@ private void checkAssignment(EZType type, LatticeElement lattice) { } } - private void checkDereference(AST.Expr receiver, Lattice facts) { + private LatticeElement checkDereference(AST.Expr receiver, Lattice facts) { LatticeElement lattice = analyzeExpr(receiver, facts); if (!lattice.isNotNull()) throw new CompilerException("Cannot dereference null or potentially null value", receiver.lineNumber); + return lattice; + } + + private void invalidateArrayElements(Lattice facts) { + for (LatticeElement element : facts.vars) + element.clearArrayElements(); } private EZType.EZTypeStruct structType(EZType type) { @@ -639,7 +712,7 @@ void transferStmt(AST.Stmt stmt, Lattice facts) { if (stmt instanceof AST.ExprStmt exprStmt && exprStmt.expr instanceof AST.ArrayStoreExpr arrayStoreExpr) { checkDereference(arrayStoreExpr.array, facts); - analyzeExpr(arrayStoreExpr.expr, facts); + LatticeElement index = analyzeExpr(arrayStoreExpr.expr, facts); EZType.EZTypeArray arrayType = null; EZType elementType = null; if (arrayStoreExpr.array.type instanceof EZType.EZTypeArray ta) { @@ -654,6 +727,12 @@ else if (arrayStoreExpr.array.type instanceof EZType.EZTypeNullable ptr && elementType = arrayType.getElementType(); var lattice = analyzeExpr(arrayStoreExpr.value,facts); checkAssignment(elementType,lattice); + invalidateArrayElements(facts); + if (arrayStoreExpr.array instanceof AST.NameExpr name && + name.symbol instanceof Symbol.VarSymbol varSymbol && + index.isIntegerConstant()) { + facts.get(varSymbol.regNumber).setArrayElement(index.intValue, lattice); + } return; } diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java index ec9806d..3aaabc8 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java @@ -20,13 +20,7 @@ private void analyze(String src, String symbolName) { sema2.analyze(program); var symbol = typeDict.lookup(symbolName); Assert.assertNotNull(symbol); - if (symbol instanceof Symbol.FunctionTypeSymbol ftsym) { - var fnDecl = (AST.FuncDecl) ftsym.functionDecl; - var flowGraph = FlowCFG.build(fnDecl); - var dot = flowGraph.toDot(); - System.out.println(dot); - new NullableAnalysis(ftsym,typeDict).doAnalysis(flowGraph); - } + NullableAnalysis.analyze(typeDict); } @Test(expected = CompilerException.class) @@ -234,6 +228,64 @@ public void nullableArrayStore() { func test(arg: [Int]?) { arg[0] = 1; } +"""; + analyze(src, "test"); + } + + @Test + public void testArrayInit() { + String src = """ + func test()->[Int] { + return new [Int] { 1, 2, 3 }; + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void nullableArrayFillValue() { + String src = """ + struct Foo {} + func test(arg: Foo?)->[Foo] { + return new [Foo] { len=1, value=arg }; + } +"""; + analyze(src, "test"); + } + + @Test + public void knownNonNullArrayElement() { + String src = """ + struct Foo { var value: Int } + func test()->Int { + var array = new [Foo?] { new Foo { value=1 }, null }; + return array[0].value; + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void knownNullArrayElement() { + String src = """ + struct Foo { var value: Int } + func test()->Int { + var array = new [Foo?] { new Foo { value=1 }, null }; + return array[1].value; + } +"""; + analyze(src, "test"); + } + + @Test(expected = CompilerException.class) + public void arrayStoreInvalidatesKnownElement() { + String src = """ + struct Foo { var value: Int } + func test()->Int { + var array = new [Foo?] { new Foo { value=1 } }; + array[0] = null; + return array[0].value; + } """; analyze(src, "test"); } diff --git a/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java b/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java index a4f5c4d..90c61e3 100644 --- a/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java +++ b/stackvm/src/main/java/com/compilerprogramming/ezlang/compiler/Compiler.java @@ -2,6 +2,7 @@ import com.compilerprogramming.ezlang.lexer.Lexer; import com.compilerprogramming.ezlang.parser.Parser; +import com.compilerprogramming.ezlang.semantic.NullableAnalysis; import com.compilerprogramming.ezlang.semantic.SemaAssignTypes; import com.compilerprogramming.ezlang.semantic.SemaDefineTypes; import com.compilerprogramming.ezlang.types.Symbol; @@ -28,6 +29,7 @@ public TypeDictionary compileSrc(String src) { sema.analyze(program); var sema2 = new SemaAssignTypes(typeDict); sema2.analyze(program); + NullableAnalysis.analyze(typeDict); compile(typeDict); return typeDict; } From 88ba9b4b215495c3d456d0dae340ad34ebcec4cb Mon Sep 17 00:00:00 2001 From: Dibyendu Majumdar Date: Sun, 5 Jul 2026 10:11:42 +0100 Subject: [PATCH 7/9] Remove per element tracking of array from lattice --- .../ezlang/semantic/NullableAnalysis.java | 57 ++----------------- .../ezlang/semantic/TestNullAnalysis.java | 4 ++ 2 files changed, 8 insertions(+), 53 deletions(-) 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 1c4e0bd..4e54371 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -96,7 +96,6 @@ else if (type instanceof EZType.EZTypeNullable) static final class LatticeElement { public byte kind; private long intValue; - private Map arrayElements; public LatticeElement(byte kind) { this.kind = kind; @@ -110,29 +109,9 @@ public LatticeElement(long value) { setIntValue(value); } LatticeElement copy() { - LatticeElement copy = new LatticeElement(kind, intValue); - if (arrayElements != null) { - copy.arrayElements = new HashMap<>(); - for (var entry : arrayElements.entrySet()) - copy.arrayElements.put(entry.getKey(), entry.getValue().copy()); - } - return copy; - } - - void setArrayElement(long index, LatticeElement element) { - if (arrayElements == null) - arrayElements = new HashMap<>(); - arrayElements.put(index, element.copy()); + return new LatticeElement(kind, intValue); } - LatticeElement getArrayElement(long index) { - LatticeElement element = arrayElements == null ? null : arrayElements.get(index); - return element == null ? null : element.copy(); - } - - void clearArrayElements() { - arrayElements = null; - } boolean isTrue() { return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; } @@ -161,8 +140,6 @@ else if (!isInteger()) { } boolean meet(LatticeElement other) { byte old = kind; - if (!Objects.equals(arrayElements, other.arrayElements)) - arrayElements = null; // universal top/bottom if (kind == F_TOP) { @@ -287,7 +264,6 @@ void copyFrom(LatticeElement other) { LatticeElement copy = other.copy(); this.kind = copy.kind; this.intValue = copy.intValue; - this.arrayElements = copy.arrayElements; } boolean isTop() { return kind == F_TOP; @@ -351,13 +327,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 && - Objects.equals(arrayElements, that.arrayElements); + return kind == that.kind && intValue == that.intValue; } @Override public int hashCode() { - return Objects.hash(kind, intValue, arrayElements); + return Objects.hash(kind, intValue); } } static final class Lattice { @@ -430,11 +405,6 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { : expr; var latticeElement = analyzeExpr(value,facts); checkAssignment(elemType,latticeElement); - if (expr instanceof AST.ArrayInitExpr arrayInitExpr) { - LatticeElement index = analyzeExpr(arrayInitExpr.expr, facts); - if (index.isIntegerConstant()) - result.setArrayElement(index.intValue, latticeElement); - } } } return result; @@ -453,9 +423,6 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { var lattice = analyzeExpr(expr,facts); checkAssignment(type,lattice); } - - invalidateArrayElements(facts); - // Use function return type: // Foo -> NON_NULL // Foo? -> UNKNOWN @@ -463,18 +430,13 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { } if (e instanceof AST.GetFieldExpr field) { - checkDereference(field.object, facts); + //checkDereference(field.object, facts); return factFromType(field.type); } if (e instanceof AST.ArrayLoadExpr arrayLoad) { LatticeElement array = checkDereference(arrayLoad.array, facts); LatticeElement index = analyzeExpr(arrayLoad.expr, facts); - if (index.isIntegerConstant()) { - LatticeElement element = array.getArrayElement(index.intValue); - if (element != null) - return element; - } return factFromType(arrayLoad.type); } @@ -668,11 +630,6 @@ private LatticeElement checkDereference(AST.Expr receiver, Lattice facts) { return lattice; } - private void invalidateArrayElements(Lattice facts) { - for (LatticeElement element : facts.vars) - element.clearArrayElements(); - } - private EZType.EZTypeStruct structType(EZType type) { if (type instanceof EZType.EZTypeStruct structType) return structType; @@ -727,12 +684,6 @@ else if (arrayStoreExpr.array.type instanceof EZType.EZTypeNullable ptr && elementType = arrayType.getElementType(); var lattice = analyzeExpr(arrayStoreExpr.value,facts); checkAssignment(elementType,lattice); - invalidateArrayElements(facts); - if (arrayStoreExpr.array instanceof AST.NameExpr name && - name.symbol instanceof Symbol.VarSymbol varSymbol && - index.isIntegerConstant()) { - facts.get(varSymbol.regNumber).setArrayElement(index.intValue, lattice); - } return; } diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java index 3aaabc8..9db9ad9 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java @@ -7,6 +7,7 @@ import com.compilerprogramming.ezlang.types.Symbol; import com.compilerprogramming.ezlang.types.TypeDictionary; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; public class TestNullAnalysis { @@ -177,6 +178,7 @@ func test(arg: Foo?)->Foo { analyze(src, "test"); } + @Ignore("Array index based value is not tracked during semantic analysis") @Test(expected = CompilerException.class) public void nullableFieldDereference() { String src = """ @@ -265,6 +267,7 @@ func test()->Int { analyze(src, "test"); } + @Ignore("Array index based value is not tracked during semantic analysis") @Test(expected = CompilerException.class) public void knownNullArrayElement() { String src = """ @@ -277,6 +280,7 @@ func test()->Int { analyze(src, "test"); } + @Ignore("Array index based value is not tracked during semantic analysis") @Test(expected = CompilerException.class) public void arrayStoreInvalidatesKnownElement() { String src = """ From 251431f7e3916df266e93f5eab13075738d9388d Mon Sep 17 00:00:00 2001 From: Dibyendu Majumdar Date: Sun, 5 Jul 2026 10:31:20 +0100 Subject: [PATCH 8/9] Add test case --- .../ezlang/semantic/NullableAnalysis.java | 2 +- .../ezlang/semantic/TestNullAnalysis.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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 4e54371..c485a6c 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -430,7 +430,7 @@ else if (initExpr.newExpr.type instanceof EZType.EZTypeArray arrayType) { } if (e instanceof AST.GetFieldExpr field) { - //checkDereference(field.object, facts); + checkDereference(field.object, facts); return factFromType(field.type); } diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java index 9db9ad9..3d6e085 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java @@ -293,4 +293,23 @@ func test()->Int { """; analyze(src, "test"); } + + @Test + public void testGuardedLogicalAnd() + { + String src = """ + struct Foo + { + var i: Int + } + func foo()->Int + { + var f = new [Foo?] { new Foo{i = 1}, null } + var f0 = f[0] + return null == f[1] && f0 != null && 1 == f0.i + } + + """; + analyze(src, "foo"); + } } From 10529a0255ad10a7c81c4816a4c847eda5a42e61 Mon Sep 17 00:00:00 2001 From: dibyendumajumdar Date: Sun, 5 Jul 2026 11:50:11 +0100 Subject: [PATCH 9/9] experiment with flow type analysis --- .../ezlang/compiler/TestSSADestructB.java | 233 +++++++++++------- .../ezlang/compiler/TestSSATransform.java | 121 +++++---- .../ezlang/interpreter/TestInterpreter.java | 3 +- .../ezlang/interpreter/TestInterpreter.java | 5 +- .../ezlang/semantic/NullableAnalysis.java | 17 ++ .../ezlang/semantic/TestNullAnalysis.java | 24 +- 6 files changed, 268 insertions(+), 135 deletions(-) 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 9142a7c..38e4eaf 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSADestructB.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSADestructB.java @@ -4578,7 +4578,8 @@ public void testSSA15() { func foo()->Int { var f = new [Foo?] { new Foo{i = 1}, null } - return null == f[1] && 1 == f[0].i + var f0 = f[0] + return null == f[1] && f0 != null && 1 == f0.i } """; String result = compileSrc(src); @@ -4587,136 +4588,196 @@ func foo()->Int Before SSA ========== L0: - %t1 = New([Foo?], len=2) - %t2 = New(Foo) - %t2.i = 1 - %t1[0] = %t2 - %t1[1] = null - f = %t1 - %t3 = f[1] - %t4 = null==%t3 - if %t4 goto L2 else goto L3 + %t2 = New([Foo?], len=2) + %t3 = New(Foo) + %t3.i = 1 + %t2[0] = %t3 + %t2[1] = null + f = %t2 + %t4 = f[0] + f0 = %t4 + %t5 = f[1] + %t6 = null==%t5 + if %t6 goto L5 else goto L6 +L5: + %t7 = f0!=null + goto L7 +L7: + if %t7 goto L2 else goto L3 L2: - %t5 = f[0] - %t6 = %t5.i - %t7 = 1==%t6 + %t8 = f0.i + %t9 = 1==%t8 goto L4 L4: - ret %t7 + ret %t9 goto L1 L1: L3: - %t7 = 0 + %t9 = 0 goto L4 +L6: + %t7 = 0 + goto L7 After SSA ========= L0: - %t1_0 = New([Foo?], len=2) - %t2_0 = New(Foo) - %t2_0.i = 1 - %t1_0[0] = %t2_0 - %t1_0[1] = null - f_0 = %t1_0 - %t3_0 = f_0[1] - %t4_0 = null==%t3_0 - if %t4_0 goto L2 else goto L3 + %t2_0 = New([Foo?], len=2) + %t3_0 = New(Foo) + %t3_0.i = 1 + %t2_0[0] = %t3_0 + %t2_0[1] = null + f_0 = %t2_0 + %t4_0 = f_0[0] + f0_0 = %t4_0 + %t5_0 = f_0[1] + %t6_0 = null==%t5_0 + if %t6_0 goto L5 else goto L6 +L5: + %t7_1 = f0_0!=null + goto L7 +L7: + %t7_2 = phi(%t7_1, %t7_0) + if %t7_2 goto L2 else goto L3 L2: - %t5_0 = f_0[0] - %t6_0 = %t5_0.i - %t7_1 = 1==%t6_0 + %t8_0 = f0_0.i + %t9_1 = 1==%t8_0 goto L4 L4: - %t7_2 = phi(%t7_1, %t7_0) - ret %t7_2 + %t9_2 = phi(%t9_1, %t9_0) + ret %t9_2 goto L1 L1: L3: - %t7_0 = 0 + %t9_0 = 0 goto L4 +L6: + %t7_0 = 0 + goto L7 After converting from SSA to CSSA L0: - %t1_0 = New([Foo?], len=2) - %t2_0 = New(Foo) - %t2_0.i = 1 - %t1_0[0] = %t2_0 - %t1_0[1] = null - f_0 = %t1_0 - %t3_0 = f_0[1] - %t4_0 = null==%t3_0 - if %t4_0 goto L2 else goto L3 + %t2_0 = New([Foo?], len=2) + %t3_0 = New(Foo) + %t3_0.i = 1 + %t2_0[0] = %t3_0 + %t2_0[1] = null + f_0 = %t2_0 + %t4_0 = f_0[0] + f0_0 = %t4_0 + %t5_0 = f_0[1] + %t6_0 = null==%t5_0 + if %t6_0 goto L5 else goto L6 +L5: + %t7_1 = f0_0!=null + (%t7_1_27) = (%t7_1) + goto L7 +L7: + %t7_2_29 = phi(%t7_1_27, %t7_0_28) + (%t7_2) = (%t7_2_29) + if %t7_2 goto L2 else goto L3 L2: - %t5_0 = f_0[0] - %t6_0 = %t5_0.i - %t7_1 = 1==%t6_0 - (%t7_1_18) = (%t7_1) + %t8_0 = f0_0.i + %t9_1 = 1==%t8_0 + (%t9_1_24) = (%t9_1) goto L4 L4: - %t7_2_20 = phi(%t7_1_18, %t7_0_19) - (%t7_2) = (%t7_2_20) - ret %t7_2 + %t9_2_26 = phi(%t9_1_24, %t9_0_25) + (%t9_2) = (%t9_2_26) + ret %t9_2 goto L1 L1: L3: - %t7_0 = 0 - (%t7_0_19) = (%t7_0) + %t9_0 = 0 + (%t9_0_25) = (%t9_0) goto L4 +L6: + %t7_0 = 0 + (%t7_0_28) = (%t7_0) + goto L7 After removing phis from CSSA L0: - %t1_0 = New([Foo?], len=2) - %t2_0 = New(Foo) - %t2_0.i = 1 - %t1_0[0] = %t2_0 - %t1_0[1] = null - f_0 = %t1_0 - %t3_0 = f_0[1] - %t4_0 = null==%t3_0 - if %t4_0 goto L2 else goto L3 + %t2_0 = New([Foo?], len=2) + %t3_0 = New(Foo) + %t3_0.i = 1 + %t2_0[0] = %t3_0 + %t2_0[1] = null + f_0 = %t2_0 + %t4_0 = f_0[0] + f0_0 = %t4_0 + %t5_0 = f_0[1] + %t6_0 = null==%t5_0 + if %t6_0 goto L5 else goto L6 +L5: + %t7_1 = f0_0!=null + (%t7_1_27) = (%t7_1) + %t7_2_29 = %t7_1_27 + goto L7 +L7: + (%t7_2) = (%t7_2_29) + if %t7_2 goto L2 else goto L3 L2: - %t5_0 = f_0[0] - %t6_0 = %t5_0.i - %t7_1 = 1==%t6_0 - (%t7_1_18) = (%t7_1) - %t7_2_20 = %t7_1_18 + %t8_0 = f0_0.i + %t9_1 = 1==%t8_0 + (%t9_1_24) = (%t9_1) + %t9_2_26 = %t9_1_24 goto L4 L4: - (%t7_2) = (%t7_2_20) - ret %t7_2 + (%t9_2) = (%t9_2_26) + ret %t9_2 goto L1 L1: L3: - %t7_0 = 0 - (%t7_0_19) = (%t7_0) - %t7_2_20 = %t7_0_19 + %t9_0 = 0 + (%t9_0_25) = (%t9_0) + %t9_2_26 = %t9_0_25 goto L4 +L6: + %t7_0 = 0 + (%t7_0_28) = (%t7_0) + %t7_2_29 = %t7_0_28 + goto L7 After exiting SSA ================= L0: - %t1_0 = New([Foo?], len=2) - %t2_0 = New(Foo) - %t2_0.i = 1 - %t1_0[0] = %t2_0 - %t1_0[1] = null - f_0 = %t1_0 - %t3_0 = f_0[1] - %t4_0 = null==%t3_0 - if %t4_0 goto L2 else goto L3 + %t2_0 = New([Foo?], len=2) + %t3_0 = New(Foo) + %t3_0.i = 1 + %t2_0[0] = %t3_0 + %t2_0[1] = null + f_0 = %t2_0 + %t4_0 = f_0[0] + f0_0 = %t4_0 + %t5_0 = f_0[1] + %t6_0 = null==%t5_0 + if %t6_0 goto L5 else goto L6 +L5: + %t7_1 = f0_0!=null + %t7_1_27 = %t7_1 + %t7_2_29 = %t7_1_27 + goto L7 +L7: + %t7_2 = %t7_2_29 + if %t7_2 goto L2 else goto L3 L2: - %t5_0 = f_0[0] - %t6_0 = %t5_0.i - %t7_1 = 1==%t6_0 - %t7_1_18 = %t7_1 - %t7_2_20 = %t7_1_18 + %t8_0 = f0_0.i + %t9_1 = 1==%t8_0 + %t9_1_24 = %t9_1 + %t9_2_26 = %t9_1_24 goto L4 L4: - %t7_2 = %t7_2_20 - ret %t7_2 + %t9_2 = %t9_2_26 + ret %t9_2 goto L1 L1: L3: - %t7_0 = 0 - %t7_0_19 = %t7_0 - %t7_2_20 = %t7_0_19 + %t9_0 = 0 + %t9_0_25 = %t9_0 + %t9_2_26 = %t9_0_25 goto L4 +L6: + %t7_0 = 0 + %t7_0_28 = %t7_0 + %t7_2_29 = %t7_0_28 + goto L7 """, result); } 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 e652b89..65fb48d 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSATransform.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/compiler/TestSSATransform.java @@ -2774,7 +2774,8 @@ public void testSSA15() { func foo()->Int { var f = new [Foo?] { new Foo{i = 1}, null } - return null == f[1] && 1 == f[0].i + var f0 = f[0] + return null == f[1] && f0 != null && 1 == f0.i } """; String result = compileSrc(src); @@ -2783,78 +2784,108 @@ func foo()->Int Before SSA ========== L0: - %t1 = New([Foo?], len=2) - %t2 = New(Foo) - %t2.i = 1 - %t1[0] = %t2 - %t1[1] = null - f = %t1 - %t3 = f[1] - %t4 = null==%t3 - if %t4 goto L2 else goto L3 + %t2 = New([Foo?], len=2) + %t3 = New(Foo) + %t3.i = 1 + %t2[0] = %t3 + %t2[1] = null + f = %t2 + %t4 = f[0] + f0 = %t4 + %t5 = f[1] + %t6 = null==%t5 + if %t6 goto L5 else goto L6 +L5: + %t7 = f0!=null + goto L7 +L7: + if %t7 goto L2 else goto L3 L2: - %t5 = f[0] - %t6 = %t5.i - %t7 = 1==%t6 + %t8 = f0.i + %t9 = 1==%t8 goto L4 L4: - ret %t7 + ret %t9 goto L1 L1: L3: - %t7 = 0 + %t9 = 0 goto L4 +L6: + %t7 = 0 + goto L7 After SSA ========= L0: - %t1_0 = New([Foo?], len=2) - %t2_0 = New(Foo) - %t2_0.i = 1 - %t1_0[0] = %t2_0 - %t1_0[1] = null - f_0 = %t1_0 - %t3_0 = f_0[1] - %t4_0 = null==%t3_0 - if %t4_0 goto L2 else goto L3 + %t2_0 = New([Foo?], len=2) + %t3_0 = New(Foo) + %t3_0.i = 1 + %t2_0[0] = %t3_0 + %t2_0[1] = null + f_0 = %t2_0 + %t4_0 = f_0[0] + f0_0 = %t4_0 + %t5_0 = f_0[1] + %t6_0 = null==%t5_0 + if %t6_0 goto L5 else goto L6 +L5: + %t7_1 = f0_0!=null + goto L7 +L7: + %t7_2 = phi(%t7_1, %t7_0) + if %t7_2 goto L2 else goto L3 L2: - %t5_0 = f_0[0] - %t6_0 = %t5_0.i - %t7_1 = 1==%t6_0 + %t8_0 = f0_0.i + %t9_1 = 1==%t8_0 goto L4 L4: - %t7_2 = phi(%t7_1, %t7_0) - ret %t7_2 + %t9_2 = phi(%t9_1, %t9_0) + ret %t9_2 goto L1 L1: L3: - %t7_0 = 0 + %t9_0 = 0 goto L4 +L6: + %t7_0 = 0 + goto L7 After exiting SSA ================= L0: - %t1_0 = New([Foo?], len=2) - %t2_0 = New(Foo) - %t2_0.i = 1 - %t1_0[0] = %t2_0 - %t1_0[1] = null - f_0 = %t1_0 - %t3_0 = f_0[1] - %t4_0 = null==%t3_0 - if %t4_0 goto L2 else goto L3 -L2: - %t5_0 = f_0[0] - %t6_0 = %t5_0.i - %t7_1 = 1==%t6_0 + %t2_0 = New([Foo?], len=2) + %t3_0 = New(Foo) + %t3_0.i = 1 + %t2_0[0] = %t3_0 + %t2_0[1] = null + f_0 = %t2_0 + %t4_0 = f_0[0] + f0_0 = %t4_0 + %t5_0 = f_0[1] + %t6_0 = null==%t5_0 + if %t6_0 goto L5 else goto L6 +L5: + %t7_1 = f0_0!=null %t7_2 = %t7_1 + goto L7 +L7: + if %t7_2 goto L2 else goto L3 +L2: + %t8_0 = f0_0.i + %t9_1 = 1==%t8_0 + %t9_2 = %t9_1 goto L4 L4: - ret %t7_2 + ret %t9_2 goto L1 L1: L3: + %t9_0 = 0 + %t9_2 = %t9_0 + goto L4 +L6: %t7_0 = 0 %t7_2 = %t7_0 - goto L4 + goto L7 """, result); } 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 136ef72..c7c0fb5 100644 --- a/optvm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java +++ b/optvm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java @@ -528,7 +528,8 @@ public void testFunction103() { func foo()->Int { var f = new [Foo?] { new Foo{i = 1}, null } - return null == f[1] && 1 == f[0].i + var f0 = f[0] + return null == f[1] && f0 != null && 1 == f0.i } """; 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 4734aff..f1bc9d0 100644 --- a/registervm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java +++ b/registervm/src/test/java/com/compilerprogramming/ezlang/interpreter/TestInterpreter.java @@ -163,10 +163,11 @@ public void testFunction103() { { var i: Int } - func foo()->Int + func foo()->Int { var f = new [Foo?] { new Foo{i = 1}, null } - return null == f[1] && 1 == f[0].i + var f0 = f[0] + return null == f[1] && f0 != null && 1 == f0.i } """; 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 c485a6c..2d82f06 100644 --- a/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -375,6 +375,12 @@ public boolean equals(Object o) { } LatticeElement analyzeExpr(AST.Expr e, Lattice facts) { + // FlowCFG lowers every &&, ||, and ! into condition blocks. Their + // operands are analyzed in those blocks; walking the original logical + // AST again here would replay guarded operands after the paths merge. + if (isLogical(e)) + return factFromType(e.type); + if (e instanceof AST.NewExpr newExpr) { if (newExpr.type instanceof EZType.EZTypeArray arrayType) { if (newExpr.len != null) @@ -600,6 +606,11 @@ Lattice transferBlock(FlowCFG.FlowBlock block, Lattice in) { for (AST.Stmt stmt : block.statements) { transferStmt(stmt, lattice); } + // A condition is an evaluation point produced by logical-expression + // lowering. Analyze it on every block transfer so fixed-point revisits + // use the current incoming facts. + if (block.condition != null) + analyzeExpr(block.condition, lattice); return lattice; } @@ -745,4 +756,10 @@ boolean isName(AST.Expr e) { return e instanceof AST.NameExpr; } + boolean isLogical(AST.Expr e) { + if (e instanceof AST.BinaryExpr binary) + return binary.op.str.equals("&&") || binary.op.str.equals("||"); + return e instanceof AST.UnaryExpr unary && unary.op.str.equals("!"); + } + } diff --git a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java index 3d6e085..860b1ec 100644 --- a/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java @@ -255,7 +255,7 @@ func test(arg: Foo?)->[Foo] { analyze(src, "test"); } - @Test + @Test(expected = CompilerException.class) public void knownNonNullArrayElement() { String src = """ struct Foo { var value: Int } @@ -312,4 +312,26 @@ func foo()->Int """; analyze(src, "foo"); } + + @Test + public void testGuardedLogicalOr() { + String src = """ + struct Foo { var i: Int } + func foo(f0: Foo?)->Int { + return f0 == null || f0.i == 1 + } + """; + analyze(src, "foo"); + } + + @Test(expected = CompilerException.class) + public void testUnguardedLogicalAnd() { + String src = """ + struct Foo { var i: Int } + func foo(f0: Foo?)->Int { + return 1 == 1 && f0.i == 1 + } + """; + analyze(src, "foo"); + } }