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/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/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/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/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/FlowCFG.java b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java new file mode 100644 index 0000000..9c5d563 --- /dev/null +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/FlowCFG.java @@ -0,0 +1,522 @@ +package com.compilerprogramming.ezlang.semantic; + +import com.compilerprogramming.ezlang.parser.AST; +import com.compilerprogramming.ezlang.parser.ASTVisitor; + +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 + "(" + FlowGraph.render(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(" " + render(s)).append('\n'); + } + + if (b.condition != null) { + sb.append(" condition: " + render(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(render(s))); + } + + if (b.condition != null) { + sb.append("\\n? ").append(escape(render(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(); + } + + /* + * 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("\"", "\\\""); + } + } + + 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) { + if (ret.expr != null) { + cur = buildExpr(ret.expr, cur); + } + 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(); + } + + 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; + } + + private FlowBlock buildBlock(AST.BlockStmt block, FlowBlock cur) { + for (AST.Stmt stmt : block.stmtList) { + cur = buildStmt(stmt, 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(); + 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; + } + + /** + * 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(); + 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; + } + + /** + * 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; + + 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; + } + + 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("!"); + } +} 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..2d82f06 --- /dev/null +++ b/semantic/src/main/java/com/compilerprogramming/ezlang/semantic/NullableAnalysis.java @@ -0,0 +1,765 @@ +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<>(); + private final EZType returnType; + + 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()) { + if (symbol instanceof Symbol.VarSymbol varSymbol) { + varSymbol.regNumber = reg++; + var type = varSymbol.type; + 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); + } + } + for (Scope childScope: scope.children) { + setVirtualRegisters(childScope); + } + } + + // 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(byte kind) { + this.kind = kind; + } + public LatticeElement(byte kind,long value) { + this.kind = kind; + this.intValue = value; + } + public LatticeElement(long value) { + kind = F_INT_TOP; + setIntValue(value); + } + LatticeElement copy() { + return new LatticeElement(kind, intValue); + } + + boolean isTrue() { + return kind == F_INT_NONZERO_CONST || kind == F_INT_NONZERO_VARYING; + } + boolean isFalse() { + return kind == F_INT_ZERO; + } + boolean isIntegerConstant() { + return kind == F_INT_ZERO || kind == F_INT_NONZERO_CONST; + } + boolean setIntValue(long value) { + byte prevKind = kind; + if (kind == F_INT_TOP) { + this.intValue = value; + this.kind = value == 0 ? F_INT_ZERO : F_INT_NONZERO_CONST; + } + else if (kind == F_INT_ZERO && value != 0) { + this.kind = F_INT_BOTTOM; + } + else if (kind == F_INT_NONZERO_CONST && (value == 0 || value != intValue)) { + this.kind = F_INT_BOTTOM; + } + else if (!isInteger()) { + throw new CompilerException("Cannot assign integer value to reference type"); + } + return (kind != prevKind); + } + boolean meet(LatticeElement other) { + byte old = kind; + + // universal top/bottom + if (kind == F_TOP) { + copyFrom(other); + return true; + } + + if (kind == F_BOTTOM || other.kind == F_TOP) + return false; + + if (other.kind == F_BOTTOM) { + kind = F_BOTTOM; + return kind != old; + } + + // same value + if (kind == other.kind) { + if (kind == F_INT_NONZERO_CONST && + intValue != other.intValue) { + kind = F_INT_NONZERO_VARYING; + } + return kind != old; + } + + // reference lattice + if (isReference(kind) && isReference(other.kind)) { + kind = meetReference(other); + return kind != old; + } + + // integer lattice + if (isInteger(kind) && isInteger(other.kind)) { + meetInteger(other); + return kind != old; + } + + // impossible + kind = F_BOTTOM; + return kind != old; + } + + private byte meetReference(LatticeElement other) { + + if (kind == F_REF_TOP) + return other.kind; + + if (other.kind == F_REF_TOP) + return kind; + + return switch (kind) { + + case F_REF_NULL -> + other.kind == F_REF_NULL + ? F_REF_NULL + : F_REF_BOTTOM; + + case F_REF_NOT_NULL -> + other.kind == F_REF_NOT_NULL + ? F_REF_NOT_NULL + : F_REF_BOTTOM; + + case F_REF_BOTTOM -> + F_REF_BOTTOM; + + default -> + F_BOTTOM; + }; + } + + private void meetInteger(LatticeElement other) { + + if (kind == F_INT_TOP) { + copyFrom(other); + return; + } + + if (other.kind == F_INT_TOP) + return; + + switch (kind) { + + case F_INT_ZERO -> { + if (other.kind != F_INT_ZERO) + kind = F_INT_BOTTOM; + } + + case F_INT_NONZERO_CONST -> { + switch (other.kind) { + + case F_INT_NONZERO_CONST -> { + if (intValue != other.intValue) + kind = F_INT_NONZERO_VARYING; + } + + case F_INT_NONZERO_VARYING -> + kind = F_INT_NONZERO_VARYING; + + case F_INT_ZERO, + F_INT_BOTTOM -> + kind = F_INT_BOTTOM; + } + } + + case F_INT_NONZERO_VARYING -> { + if (other.kind == F_INT_ZERO || + other.kind == F_INT_BOTTOM) + kind = F_INT_BOTTOM; + } + + case F_INT_BOTTOM -> { + } + } + } + private static boolean isReference(byte kind) { + return kind >= F_REF_TOP && kind <= F_REF_BOTTOM; + } + + private static boolean isInteger(byte kind) { + return kind >= F_INT_TOP && kind <= F_INT_BOTTOM; + } + void copyFrom(LatticeElement other) { + LatticeElement copy = other.copy(); + this.kind = copy.kind; + this.intValue = copy.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() { + 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 + 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 Lattice { + final LatticeElement[] vars; + + Lattice(List elementList) { + vars = elementList.toArray(new LatticeElement[0]); + } + Lattice(LatticeElement[] elements) { + vars = elements; + } + + Lattice copy() { + LatticeElement[] copyvars = new LatticeElement[vars.length]; + for (int i = 0; i < copyvars.length; i++) + copyvars[i] = vars[i].copy(); + return new Lattice(copyvars); + } + + LatticeElement get(int reg) { + return vars[reg]; + } + + 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]); + } + + return out; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Lattice lattice = (Lattice) o; + return Objects.deepEquals(vars, lattice.vars); + } + } + + 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) + 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) { + 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) { + AST.Expr value = expr instanceof AST.ArrayInitExpr arrayInitExpr + ? arrayInitExpr.value + : expr; + var latticeElement = analyzeExpr(value,facts); + checkAssignment(elemType,latticeElement); + } + } + return result; + } + + 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) { + 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); + return factFromType(arrayLoad.type); + } + + if (e instanceof AST.LiteralExpr lit) { + if (lit.value.str.equals("null")) { + return new LatticeElement(F_REF_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.isIntegerConstant()) + return new LatticeElement(-v.intValue); + if (v.isInteger()) + 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 factFromType(un.type); + } + + 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_INT_ZERO) + throw new CompilerException("Division by zero"); + value = a.intValue / b.intValue; + } + case "%" -> { + if (b.kind == F_INT_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; + } + 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); + } + if (isArith) + result = new LatticeElement(value); + else { + if (value == 1) + result = new LatticeElement(F_INT_NONZERO_CONST,1); + else + result = new LatticeElement(F_INT_ZERO); + } + return result; + } + return factFromType(bin.type); + } + + return new LatticeElement(F_BOTTOM); + } + + LatticeElement factFromType(EZType type) { + if (type != null) { + if (type instanceof EZType.EZTypeNullable) + return new LatticeElement(F_REF_BOTTOM); + else if (type instanceof EZType.EZTypeNull) + return new LatticeElement(F_REF_NULL); + else if (type instanceof EZType.EZTypeArray || + type instanceof EZType.EZTypeStruct) + return new LatticeElement(F_REF_NOT_NULL); + else if (type.isPrimitive()) + return new LatticeElement(F_INT_BOTTOM); + } + return new LatticeElement(F_BOTTOM); + } + + public void doAnalysis(FlowCFG.FlowGraph cfg) { + Queue worklist = new ArrayDeque<>(); + + Map in = new HashMap<>(); + Map out = new HashMap<>(); + + var initialFacts = new Lattice(latticeElements); + in.put(cfg.entry, initialFacts); + worklist.add(cfg.entry); + + while (!worklist.isEmpty()) { + FlowCFG.FlowBlock b = worklist.remove(); + + Lattice inLattice = in.get(b); + if (inLattice == null) + throw new CompilerException("Missing facts"); + Lattice outLattice = transferBlock(b, inLattice); + + if (!outLattice.equals(out.get(b))) { + out.put(b, outLattice); + + for (FlowCFG.FlowEdge e : b.succs) { + Lattice edgeLattice = applyEdgeFacts(outLattice, e); + + Lattice oldIn = in.get(e.to); + Lattice newIn = oldIn == null ? edgeLattice : Lattice.merge(oldIn, edgeLattice); + + if (!newIn.equals(oldIn)) { + in.put(e.to, newIn); + worklist.add(e.to); + } + } + } + } + } + + Lattice transferBlock(FlowCFG.FlowBlock block, Lattice in) { + Lattice lattice = in.copy(); + + 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; + } + + private void checkAssignment(EZType type, LatticeElement lattice) { + if (type.isPrimitive()) { + if (!lattice.isInteger()) + throw new CompilerException("Cannot assign reference/null to int"); + return; + } + + if (type instanceof EZType.EZTypeNullable) { + if (!lattice.isReference()) + throw new CompilerException("Cannot assign non reference value to reference type"); + return; + } + + // non-null reference + if (!lattice.isReference() || lattice.isNull() || lattice.isMaybeNull()) { + throw new CompilerException("Cannot assign null or potentially null value"); + } + } + + 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 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; + 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) { + 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); + return; + } + + if (stmt instanceof AST.ExprStmt exprStmt && + exprStmt.expr instanceof AST.ArrayStoreExpr arrayStoreExpr) { + checkDereference(arrayStoreExpr.array, facts); + LatticeElement index = analyzeExpr(arrayStoreExpr.expr, facts); + 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); + 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); + } + } + + Lattice applyEdgeFacts(Lattice in, FlowCFG.FlowEdge edge) { + Lattice 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(Lattice facts, AST.Expr expr, boolean branchIsTrue) { + if (!(expr instanceof AST.BinaryExpr bin)) { + return; + } + + boolean leftNameRightNull = + isName(bin.expr1) && analyzeExpr(bin.expr2, facts).isNull(); + + boolean rightNameLeftNull = + isName(bin.expr2) && analyzeExpr(bin.expr1, facts).isNull(); + + 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_REF_NOT_NULL) + : new LatticeElement(F_REF_NULL); + } else { + lattice = op.equals("!=") + ? new LatticeElement(F_REF_NULL) + : new LatticeElement(F_REF_NOT_NULL); + } + facts.vars[varSymbol.regNumber] = lattice; + } + + 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 new file mode 100644 index 0000000..860b1ec --- /dev/null +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestNullAnalysis.java @@ -0,0 +1,337 @@ +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.Ignore; +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); + NullableAnalysis.analyze(typeDict); + } + + @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"); + } + + @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"); + } + + @Ignore("Array index based value is not tracked during semantic analysis") + @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"); + } + + @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(expected = CompilerException.class) + 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"); + } + + @Ignore("Array index based value is not tracked during semantic analysis") + @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"); + } + + @Ignore("Array index based value is not tracked during semantic analysis") + @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"); + } + + @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"); + } + + @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"); + } +} 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..1559eb5 --- /dev/null +++ b/semantic/src/test/java/com/compilerprogramming/ezlang/semantic/TestSemaFlowGraph.java @@ -0,0 +1,187 @@ +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 FlowCFG.FlowGraph 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; + } + 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); + } + + @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); + } +} 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; }