From c5c01041e424460b182ea66556700fb03259f0ff Mon Sep 17 00:00:00 2001 From: nitrix Date: Wed, 22 Mar 2023 23:22:36 +0100 Subject: [PATCH] ContextAnalysis: Track local variables and function parameters separately. This enables us to add all local variable Definitions to the DAST so that downstream visitors don't need to compute the local variables of a function again. --- .../hsrm/compiler/Klang/ContextAnalysis.java | 206 ++++++++---------- .../hsrm/compiler/Klang/GetDefinitions.java | 2 +- .../Klang/nodes/FunctionDefinition.java | 5 +- .../hsrm/compiler/Klang/types/NamedType.java | 8 +- src/test/java/DestroyStatementTest.java | 2 +- src/test/java/FieldAssignmentTest.java | 4 +- src/test/java/FunctionDefinitionTest.java | 24 ++ src/test/java/StructFieldAccessTest.java | 4 +- src/test/java/VariableAssignmentTest.java | 2 +- src/test/java/VariableTest.java | 2 +- 10 files changed, 130 insertions(+), 129 deletions(-) diff --git a/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java b/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java index f752bce..a2323a9 100644 --- a/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java +++ b/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java @@ -8,10 +8,12 @@ import de.hsrm.compiler.Klang.nodes.loops.ForLoop; import de.hsrm.compiler.Klang.nodes.loops.WhileLoop; import de.hsrm.compiler.Klang.nodes.statements.*; import de.hsrm.compiler.Klang.types.Type; +import org.antlr.v4.runtime.tree.TerminalNode; import java.util.*; public class ContextAnalysis extends KlangBaseVisitor { + Map params = new HashMap<>(); Map vars = new HashMap<>(); Map functionDefs; Map structDefs; @@ -199,8 +201,8 @@ public class ContextAnalysis extends KlangBaseVisitor { throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } - if (vars.get(variableName) != null) { - var error = "Redeclaration of variable with name \"" + variableName + "\"."; + if (vars.get(variableName) != null || params.get(variableName) != null) { + var error = "Redeclaration of variable or parameter with name \"" + variableName + "\"."; throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } @@ -236,31 +238,27 @@ public class ContextAnalysis extends KlangBaseVisitor { @Override public Node visitVariable_assignment(KlangParser.Variable_assignmentContext ctx) { - String name = ctx.IDENT().getText(); - int line = ctx.start.getLine(); - int col = ctx.start.getCharPositionInLine(); + var name = ctx.IDENT().getText(); + var line = ctx.start.getLine(); + var col = ctx.start.getCharPositionInLine(); - VariableDeclaration var = this.vars.get(name); - if (var == null) { - String error = "Variable with name \"" + name + "\" not defined."; - throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); - } + var variableOrParameter = getVariableOrParameter(name, line, col); // Evaluate the expression - Expression expression = (Expression) this.visit(ctx.expression()); + var expression = (Expression) visit(ctx.expression()); // Make sure expression can be assigned to the variable try { - expression.type.combine(var.type); + expression.type.combine(variableOrParameter.type); } catch (Exception e) { throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage()); } // Since we assigned a value to this variable, we can consider it initialized - var.initialized = true; + variableOrParameter.initialized = true; // Create a new node and add the type of the expression to it - Node result = new VariableAssignment(name, expression); + var result = new VariableAssignment(name, expression); result.line = line; result.col = col; return result; @@ -272,8 +270,7 @@ public class ContextAnalysis extends KlangBaseVisitor { ReturnStatement result = new ReturnStatement(expression); // Check if this expression is a tail recursion - if (expression instanceof FunctionCall) { - var funCall = (FunctionCall) expression; + if (expression instanceof FunctionCall funCall) { if (funCall.name.equals(this.currentFunctionDefinitionName)) { // Flag this function call funCall.isTailRecursive = true; @@ -288,46 +285,24 @@ public class ContextAnalysis extends KlangBaseVisitor { @Override public Node visitField_assignment(KlangParser.Field_assignmentContext ctx) { - String varName = ctx.IDENT(0).getText(); - int line = ctx.start.getLine(); - int col = ctx.start.getCharPositionInLine(); - String[] path = new String[ctx.IDENT().size() - 1]; + var varName = ctx.IDENT(0).getText(); + var line = ctx.start.getLine(); + var col = ctx.start.getCharPositionInLine(); + var path = createStructPath(ctx.IDENT()); - for (int i = 1; i < ctx.IDENT().size(); i++) { - path[i - 1] = ctx.IDENT(i).getText(); - } + var variableOrParameter = getVariableOrParameter(varName, line, col); + ensureReferencesStruct(variableOrParameter, line, col); + var fieldType = drillType(variableOrParameter, path, line, col); - // Get the referenced variable, make sure it is defined - var variableDef = this.vars.get(varName); - if (variableDef == null) { - String error = "Variable with name " + varName + " not defined."; - throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); - } - - // Make sure it references a struct - if (variableDef.type.isPrimitiveType()) { - String error = "Variable must reference a struct but references " + variableDef.type.getName() + "."; - throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); - } - - // Get the type of the result of this expression - String structName = variableDef.type.getName(); - Type fieldType; - try { - fieldType = Helper.drillType(this.structDefs, structName, path, 0); - } catch (Exception e) { - throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage()); - } - - // Get the expression and make sure the type combines properly - Expression expression = (Expression) this.visit(ctx.expression()); + // Visit the expression and make sure the type combines properly + var expression = (Expression) visit(ctx.expression()); try { fieldType.combine(expression.type); } catch (Exception e) { throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage()); } - Node result = new FieldAssignment(varName, structName, path, expression); + var result = new FieldAssignment(varName, variableOrParameter.type.getName(), path, expression); result.col = col; result.line = line; return result; @@ -338,23 +313,17 @@ public class ContextAnalysis extends KlangBaseVisitor { var baseName = ctx.IDENT(0).getText(); var line = ctx.start.getLine(); var col = ctx.start.getCharPositionInLine(); - - // Create a list of member names. This excludes - // the first entry as it is the base name. - var path = new ArrayList(); - for (int i = 1; i < ctx.IDENT().size(); i++) { - path.add(ctx.IDENT(i).getText()); - } + var path = createStructPath(ctx.IDENT()); // Determine if the base name points to an enum or a variable var enumDef = enumDefs.get(baseName); if (enumDef != null) { - if (path.size() != 1) { + if (path.length != 1) { var error = "Illegal access to enum " + enumDef.name + "."; throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } - var enumValueName = path.get(0); + var enumValueName = path[0]; var enumValue = Arrays.stream(enumDef.enums) .filter(e -> e.value.equals(enumValueName)) .findFirst() @@ -371,29 +340,11 @@ public class ContextAnalysis extends KlangBaseVisitor { return enumAccessExpression; } - // Get the referenced variable, make sure it is defined - var variableDef = vars.get(baseName); - if (variableDef == null) { - var error = "Variable with name " + baseName + " not defined."; - throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); - } + var variableOrParameter = getVariableOrParameter(baseName, line, col); + ensureReferencesStruct(variableOrParameter, line, col); + var resultType = drillType(variableOrParameter, path, line, col); - // Make sure it references a struct - if (variableDef.type.isPrimitiveType()) { - var error = "Variable must reference a struct but references " + variableDef.type.getName() + "."; - throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); - } - - // Get the type of the result of this expression - var structName = variableDef.type.getName(); - Type resultType; - try { - resultType = Helper.drillType(structDefs, structName, path.toArray(new String[0]), 0); - } catch (Exception e) { - throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage()); - } - - var memberAccessExpression = new MemberAccessExpression(baseName, structName, path.toArray(new String[0])); + var memberAccessExpression = new MemberAccessExpression(baseName, variableOrParameter.type.getName(), path); memberAccessExpression.type = resultType; memberAccessExpression.line = line; memberAccessExpression.col = col; @@ -711,24 +662,20 @@ public class ContextAnalysis extends KlangBaseVisitor { @Override public Node visitVariable(KlangParser.VariableContext ctx) { - String name = ctx.IDENT().getText(); - int line = ctx.start.getLine(); - int col = ctx.start.getCharPositionInLine(); + var variableName = ctx.IDENT().getText(); + var line = ctx.start.getLine(); + var col = ctx.start.getCharPositionInLine(); - VariableDeclaration var = this.vars.get(name); - if (var == null) { - String error = "Variable with name \"" + name + "\" not defined."; - throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); - } + var variableOrParameter = getVariableOrParameter(variableName, line, col); // Make sure the variable has been initialized before it can be used - if (!var.initialized) { - String error = "Variable with name \"" + name + "\" has not been initialized."; + if (!variableOrParameter.initialized) { + var error = "Variable with name \"" + variableName + "\" has not been initialized."; throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } - Variable result = new Variable(ctx.IDENT().getText()); - result.type = var.type; + var result = new Variable(ctx.IDENT().getText()); + result.type = variableOrParameter.type; result.line = line; result.col = col; return result; @@ -839,23 +786,23 @@ public class ContextAnalysis extends KlangBaseVisitor { throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } - // Create a new set for the variables of the current function + // Create a new set for the variables and parameters of the current function // this will be filled in the variable declaration visitor as well vars = new HashMap<>(); + params = new HashMap<>(); + - // Process the parameter list by visiting every parameter in it var paramCount = ctx.params.parameter().size(); - var params = new Parameter[paramCount]; + var functionParameters = new Parameter[paramCount]; // the list of parameters that get passed to FunctionDefinition for (int i = 0; i < paramCount; i++) { - // Add the parameter to the list of parameters - var param = (Parameter) visit(ctx.params.parameter(i)); - params[i] = param; + functionParameters[i] = (Parameter) visit(ctx.params.parameter(i)); - // add the param as a variable - var var = new VariableDeclaration(param.name); - var.initialized = true; // parameters can always be considered initialized - var.type = param.type; - vars.put(param.name, var); + // add the param as a variable to the global parameter map so that + // child nodes can access them. + var param = new VariableDeclaration(functionParameters[i].name); + param.initialized = true; // parameters can always be considered initialized + param.type = functionParameters[i].type; + params.put(param.name, param); } // Visit the block, make sure that a return value is guaranteed @@ -867,7 +814,7 @@ public class ContextAnalysis extends KlangBaseVisitor { throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } - var functionDef = new FunctionDefinition(name, params, (Block) block); + var functionDef = new FunctionDefinition(name, functionParameters, vars.values().toArray(new VariableDeclaration[0]), (Block) block); functionDef.type = returnType; functionDef.line = ctx.start.getLine(); functionDef.col = ctx.start.getCharPositionInLine(); @@ -988,19 +935,52 @@ public class ContextAnalysis extends KlangBaseVisitor { @Override public Node visitDestroy_statement(KlangParser.Destroy_statementContext ctx) { - String name = ctx.IDENT().getText(); - int line = ctx.start.getLine(); - int col = ctx.start.getCharPositionInLine(); - - VariableDeclaration var = this.vars.get(name); - if (var == null) { - String error = "Variable with name \"" + name + "\" not defined."; - throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); - } + var varName = ctx.IDENT().getText(); + var line = ctx.start.getLine(); + var col = ctx.start.getCharPositionInLine(); - Node result = new DestructorCall(name); + var variableOrParameter = getVariableOrParameter(varName, line, col); + ensureReferencesStruct(variableOrParameter, line, col); + + var result = new DestructorCall(varName); result.line = line; result.col = col; return result; } + + private VariableDeclaration getVariableOrParameter(String name, int line, int col) { + var variable = vars.get(name); + var parameter = params.get(name); + if (variable == null && parameter == null) { + var error = "Variable or parameter with name \"" + name + "\" not defined."; + throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); + } + + return variable != null ? variable : parameter; + } + + private void ensureReferencesStruct(VariableDeclaration variableOrParameter, int line, int col) { + if (variableOrParameter.type.isPrimitiveType() || enumDefs.containsKey(variableOrParameter.type.getName())) { + var error = "Variable or parameter must reference a struct but references " + variableOrParameter.type.getName() + "."; + throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); + } + } + + private Type drillType(VariableDeclaration variableOrParameter, String[] path, int line, int col) { + try { + var structName = variableOrParameter.type.getName(); + return Helper.drillType(structDefs, structName, path, 0); + } catch (Exception e) { + throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage()); + } + } + + private String[] createStructPath(List identifiers) { + // Create a list of member names. This excludes the first entry as it is the base name. + var path = new String[identifiers.size() - 1]; + for (int i = 1; i < identifiers.size(); i++) { + path[i - 1] = identifiers.get(i).getText(); + } + return path; + } } \ No newline at end of file diff --git a/src/main/java/de/hsrm/compiler/Klang/GetDefinitions.java b/src/main/java/de/hsrm/compiler/Klang/GetDefinitions.java index d5cf9e4..7445d23 100644 --- a/src/main/java/de/hsrm/compiler/Klang/GetDefinitions.java +++ b/src/main/java/de/hsrm/compiler/Klang/GetDefinitions.java @@ -210,7 +210,7 @@ public class GetDefinitions extends KlangBaseVisitor { parameters.put(paramName, parameter); } - var functionDef = new FunctionDefinition(funcName, parameters.values().toArray(new Parameter[0]), null); + var functionDef = new FunctionDefinition(funcName, parameters.values().toArray(new Parameter[0]), null, null); functionDef.type = Type.getByName(ctx.returnType.type().getText()); functionDef.line = line; functionDef.col = col; diff --git a/src/main/java/de/hsrm/compiler/Klang/nodes/FunctionDefinition.java b/src/main/java/de/hsrm/compiler/Klang/nodes/FunctionDefinition.java index 45c1ca8..e008174 100644 --- a/src/main/java/de/hsrm/compiler/Klang/nodes/FunctionDefinition.java +++ b/src/main/java/de/hsrm/compiler/Klang/nodes/FunctionDefinition.java @@ -1,17 +1,20 @@ package de.hsrm.compiler.Klang.nodes; +import de.hsrm.compiler.Klang.nodes.statements.VariableDeclaration; import de.hsrm.compiler.Klang.visitors.Visitor; public class FunctionDefinition extends Node { public String name; public Parameter[] parameters; + public VariableDeclaration[] localVariables; public Block block; - public FunctionDefinition(String name, Parameter[] parameters, Block block) { + public FunctionDefinition(String name, Parameter[] parameters, VariableDeclaration[] localVariables, Block block) { this.name = name; this.parameters = parameters; this.block = block; + this.localVariables = localVariables; } @Override diff --git a/src/main/java/de/hsrm/compiler/Klang/types/NamedType.java b/src/main/java/de/hsrm/compiler/Klang/types/NamedType.java index 3dd028a..888be8f 100644 --- a/src/main/java/de/hsrm/compiler/Klang/types/NamedType.java +++ b/src/main/java/de/hsrm/compiler/Klang/types/NamedType.java @@ -1,11 +1,6 @@ package de.hsrm.compiler.Klang.types; import de.hsrm.compiler.Klang.Value; -import de.hsrm.compiler.Klang.nodes.EnumDefinition; -import de.hsrm.compiler.Klang.nodes.FunctionDefinition; -import de.hsrm.compiler.Klang.nodes.StructDefinition; - -import java.util.Map; public class NamedType extends Type { public String name; @@ -49,8 +44,7 @@ public class NamedType extends Type { return true; } - if (that instanceof NamedType) { - var thatType = (NamedType) that; + if (that instanceof NamedType thatType) { return getName().equals(thatType.getName()); } diff --git a/src/test/java/DestroyStatementTest.java b/src/test/java/DestroyStatementTest.java index 523c4b5..2051bcd 100644 --- a/src/test/java/DestroyStatementTest.java +++ b/src/test/java/DestroyStatementTest.java @@ -17,6 +17,6 @@ public class DestroyStatementTest { ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); - assertEquals("Error in line 1:45 Variable with name \"x\" not defined.", e.getMessage()); + assertEquals("Error in line 1:45 Variable or parameter with name \"x\" not defined.", e.getMessage()); } } \ No newline at end of file diff --git a/src/test/java/FieldAssignmentTest.java b/src/test/java/FieldAssignmentTest.java index 32eeacb..651e290 100644 --- a/src/test/java/FieldAssignmentTest.java +++ b/src/test/java/FieldAssignmentTest.java @@ -17,7 +17,7 @@ public class FieldAssignmentTest { ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); - assertEquals("Error in line 1:46 Variable with name str not defined.", e.getMessage()); + assertEquals("Error in line 1:46 Variable or parameter with name \"str\" not defined.", e.getMessage()); } @Test @@ -29,6 +29,6 @@ public class FieldAssignmentTest { ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); - assertEquals("Error in line 1:62 Variable must reference a struct but references int.", e.getMessage()); + assertEquals("Error in line 1:62 Variable or parameter must reference a struct but references int.", e.getMessage()); } } \ No newline at end of file diff --git a/src/test/java/FunctionDefinitionTest.java b/src/test/java/FunctionDefinitionTest.java index b6086fc..a1ff3cf 100644 --- a/src/test/java/FunctionDefinitionTest.java +++ b/src/test/java/FunctionDefinitionTest.java @@ -92,4 +92,28 @@ public class FunctionDefinitionTest { var e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); assertEquals("Error in line 1:31 Parameter name Bar duplicates an enum of the same name.", e.getMessage()); } + + @Test + void shouldResetVariablesAndParameterEnvironment() { + // given + var tree = Helper.prepareParser(""" + function foo(x: int): int { + return 1; + } + function bar(): int { + let x: int = 0; + return x; + } + + bar(); + """); + var ctxAnal = new ContextAnalysis( + Helper.getFuncs(tree), + Helper.getStructs(tree), + Helper.getEnums(tree) + ); + + // when / then + assertDoesNotThrow(() -> ctxAnal.visit(tree)); + } } \ No newline at end of file diff --git a/src/test/java/StructFieldAccessTest.java b/src/test/java/StructFieldAccessTest.java index 9bda8f3..51c97c2 100644 --- a/src/test/java/StructFieldAccessTest.java +++ b/src/test/java/StructFieldAccessTest.java @@ -16,7 +16,7 @@ public class StructFieldAccessTest { ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); - assertEquals("Error in line 1:53 Variable with name str not defined.", e.getMessage()); + assertEquals("Error in line 1:53 Variable or parameter with name \"str\" not defined.", e.getMessage()); } @Test @@ -28,6 +28,6 @@ public class StructFieldAccessTest { ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); - assertEquals("Error in line 1:69 Variable must reference a struct but references int.", e.getMessage()); + assertEquals("Error in line 1:69 Variable or parameter must reference a struct but references int.", e.getMessage()); } } \ No newline at end of file diff --git a/src/test/java/VariableAssignmentTest.java b/src/test/java/VariableAssignmentTest.java index 292e5f4..1c08a33 100644 --- a/src/test/java/VariableAssignmentTest.java +++ b/src/test/java/VariableAssignmentTest.java @@ -17,6 +17,6 @@ public class VariableAssignmentTest { ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); - assertEquals("Error in line 1:22 Variable with name \"x\" not defined.", e.getMessage()); + assertEquals("Error in line 1:22 Variable or parameter with name \"x\" not defined.", e.getMessage()); } } \ No newline at end of file diff --git a/src/test/java/VariableTest.java b/src/test/java/VariableTest.java index c9d2d39..8135774 100644 --- a/src/test/java/VariableTest.java +++ b/src/test/java/VariableTest.java @@ -17,7 +17,7 @@ public class VariableTest { ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); - assertEquals("Error in line 1:29 Variable with name \"x\" not defined.", e.getMessage()); + assertEquals("Error in line 1:29 Variable or parameter with name \"x\" not defined.", e.getMessage()); } @Test