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.
This commit is contained in:
2023-03-22 23:22:36 +01:00
parent 9751a1da2f
commit c5c01041e4
10 changed files with 130 additions and 129 deletions

View File

@@ -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.loops.WhileLoop;
import de.hsrm.compiler.Klang.nodes.statements.*; import de.hsrm.compiler.Klang.nodes.statements.*;
import de.hsrm.compiler.Klang.types.Type; import de.hsrm.compiler.Klang.types.Type;
import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.*; import java.util.*;
public class ContextAnalysis extends KlangBaseVisitor<Node> { public class ContextAnalysis extends KlangBaseVisitor<Node> {
Map<String, VariableDeclaration> params = new HashMap<>();
Map<String, VariableDeclaration> vars = new HashMap<>(); Map<String, VariableDeclaration> vars = new HashMap<>();
Map<String, FunctionDefinition> functionDefs; Map<String, FunctionDefinition> functionDefs;
Map<String, StructDefinition> structDefs; Map<String, StructDefinition> structDefs;
@@ -199,8 +201,8 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); throw new RuntimeException(Helper.getErrorPrefix(line, col) + error);
} }
if (vars.get(variableName) != null) { if (vars.get(variableName) != null || params.get(variableName) != null) {
var error = "Redeclaration of variable with name \"" + variableName + "\"."; var error = "Redeclaration of variable or parameter with name \"" + variableName + "\".";
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); throw new RuntimeException(Helper.getErrorPrefix(line, col) + error);
} }
@@ -236,31 +238,27 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
@Override @Override
public Node visitVariable_assignment(KlangParser.Variable_assignmentContext ctx) { public Node visitVariable_assignment(KlangParser.Variable_assignmentContext ctx) {
String name = ctx.IDENT().getText(); var name = ctx.IDENT().getText();
int line = ctx.start.getLine(); var line = ctx.start.getLine();
int col = ctx.start.getCharPositionInLine(); var col = ctx.start.getCharPositionInLine();
VariableDeclaration var = this.vars.get(name); var variableOrParameter = getVariableOrParameter(name, line, col);
if (var == null) {
String error = "Variable with name \"" + name + "\" not defined.";
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error);
}
// Evaluate the expression // 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 // Make sure expression can be assigned to the variable
try { try {
expression.type.combine(var.type); expression.type.combine(variableOrParameter.type);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage()); throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage());
} }
// Since we assigned a value to this variable, we can consider it initialized // 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 // 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.line = line;
result.col = col; result.col = col;
return result; return result;
@@ -272,8 +270,7 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
ReturnStatement result = new ReturnStatement(expression); ReturnStatement result = new ReturnStatement(expression);
// Check if this expression is a tail recursion // Check if this expression is a tail recursion
if (expression instanceof FunctionCall) { if (expression instanceof FunctionCall funCall) {
var funCall = (FunctionCall) expression;
if (funCall.name.equals(this.currentFunctionDefinitionName)) { if (funCall.name.equals(this.currentFunctionDefinitionName)) {
// Flag this function call // Flag this function call
funCall.isTailRecursive = true; funCall.isTailRecursive = true;
@@ -288,46 +285,24 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
@Override @Override
public Node visitField_assignment(KlangParser.Field_assignmentContext ctx) { public Node visitField_assignment(KlangParser.Field_assignmentContext ctx) {
String varName = ctx.IDENT(0).getText(); var varName = ctx.IDENT(0).getText();
int line = ctx.start.getLine(); var line = ctx.start.getLine();
int col = ctx.start.getCharPositionInLine(); var col = ctx.start.getCharPositionInLine();
String[] path = new String[ctx.IDENT().size() - 1]; var path = createStructPath(ctx.IDENT());
for (int i = 1; i < ctx.IDENT().size(); i++) { var variableOrParameter = getVariableOrParameter(varName, line, col);
path[i - 1] = ctx.IDENT(i).getText(); ensureReferencesStruct(variableOrParameter, line, col);
} var fieldType = drillType(variableOrParameter, path, line, col);
// Get the referenced variable, make sure it is defined // Visit the expression and make sure the type combines properly
var variableDef = this.vars.get(varName); var expression = (Expression) visit(ctx.expression());
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());
try { try {
fieldType.combine(expression.type); fieldType.combine(expression.type);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(Helper.getErrorPrefix(line, col) + e.getMessage()); 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.col = col;
result.line = line; result.line = line;
return result; return result;
@@ -338,23 +313,17 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
var baseName = ctx.IDENT(0).getText(); var baseName = ctx.IDENT(0).getText();
var line = ctx.start.getLine(); var line = ctx.start.getLine();
var col = ctx.start.getCharPositionInLine(); var col = ctx.start.getCharPositionInLine();
var path = createStructPath(ctx.IDENT());
// Create a list of member names. This excludes
// the first entry as it is the base name.
var path = new ArrayList<String>();
for (int i = 1; i < ctx.IDENT().size(); i++) {
path.add(ctx.IDENT(i).getText());
}
// Determine if the base name points to an enum or a variable // Determine if the base name points to an enum or a variable
var enumDef = enumDefs.get(baseName); var enumDef = enumDefs.get(baseName);
if (enumDef != null) { if (enumDef != null) {
if (path.size() != 1) { if (path.length != 1) {
var error = "Illegal access to enum " + enumDef.name + "."; var error = "Illegal access to enum " + enumDef.name + ".";
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); throw new RuntimeException(Helper.getErrorPrefix(line, col) + error);
} }
var enumValueName = path.get(0); var enumValueName = path[0];
var enumValue = Arrays.stream(enumDef.enums) var enumValue = Arrays.stream(enumDef.enums)
.filter(e -> e.value.equals(enumValueName)) .filter(e -> e.value.equals(enumValueName))
.findFirst() .findFirst()
@@ -371,29 +340,11 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
return enumAccessExpression; return enumAccessExpression;
} }
// Get the referenced variable, make sure it is defined var variableOrParameter = getVariableOrParameter(baseName, line, col);
var variableDef = vars.get(baseName); ensureReferencesStruct(variableOrParameter, line, col);
if (variableDef == null) { var resultType = drillType(variableOrParameter, path, line, col);
var error = "Variable with name " + baseName + " not defined.";
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error);
}
// Make sure it references a struct var memberAccessExpression = new MemberAccessExpression(baseName, variableOrParameter.type.getName(), path);
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]));
memberAccessExpression.type = resultType; memberAccessExpression.type = resultType;
memberAccessExpression.line = line; memberAccessExpression.line = line;
memberAccessExpression.col = col; memberAccessExpression.col = col;
@@ -711,24 +662,20 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
@Override @Override
public Node visitVariable(KlangParser.VariableContext ctx) { public Node visitVariable(KlangParser.VariableContext ctx) {
String name = ctx.IDENT().getText(); var variableName = ctx.IDENT().getText();
int line = ctx.start.getLine(); var line = ctx.start.getLine();
int col = ctx.start.getCharPositionInLine(); var col = ctx.start.getCharPositionInLine();
VariableDeclaration var = this.vars.get(name); var variableOrParameter = getVariableOrParameter(variableName, line, col);
if (var == null) {
String error = "Variable with name \"" + name + "\" not defined.";
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error);
}
// Make sure the variable has been initialized before it can be used // Make sure the variable has been initialized before it can be used
if (!var.initialized) { if (!variableOrParameter.initialized) {
String error = "Variable with name \"" + name + "\" has not been initialized."; var error = "Variable with name \"" + variableName + "\" has not been initialized.";
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); throw new RuntimeException(Helper.getErrorPrefix(line, col) + error);
} }
Variable result = new Variable(ctx.IDENT().getText()); var result = new Variable(ctx.IDENT().getText());
result.type = var.type; result.type = variableOrParameter.type;
result.line = line; result.line = line;
result.col = col; result.col = col;
return result; return result;
@@ -839,23 +786,23 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); 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 // this will be filled in the variable declaration visitor as well
vars = new HashMap<>(); vars = new HashMap<>();
params = new HashMap<>();
// Process the parameter list by visiting every parameter in it
var paramCount = ctx.params.parameter().size(); 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++) { for (int i = 0; i < paramCount; i++) {
// Add the parameter to the list of parameters functionParameters[i] = (Parameter) visit(ctx.params.parameter(i));
var param = (Parameter) visit(ctx.params.parameter(i));
params[i] = param;
// add the param as a variable // add the param as a variable to the global parameter map so that
var var = new VariableDeclaration(param.name); // child nodes can access them.
var.initialized = true; // parameters can always be considered initialized var param = new VariableDeclaration(functionParameters[i].name);
var.type = param.type; param.initialized = true; // parameters can always be considered initialized
vars.put(param.name, var); param.type = functionParameters[i].type;
params.put(param.name, param);
} }
// Visit the block, make sure that a return value is guaranteed // Visit the block, make sure that a return value is guaranteed
@@ -867,7 +814,7 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); 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.type = returnType;
functionDef.line = ctx.start.getLine(); functionDef.line = ctx.start.getLine();
functionDef.col = ctx.start.getCharPositionInLine(); functionDef.col = ctx.start.getCharPositionInLine();
@@ -988,19 +935,52 @@ public class ContextAnalysis extends KlangBaseVisitor<Node> {
@Override @Override
public Node visitDestroy_statement(KlangParser.Destroy_statementContext ctx) { public Node visitDestroy_statement(KlangParser.Destroy_statementContext ctx) {
String name = ctx.IDENT().getText(); var varName = ctx.IDENT().getText();
int line = ctx.start.getLine(); var line = ctx.start.getLine();
int col = ctx.start.getCharPositionInLine(); 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);
}
Node result = new DestructorCall(name); var variableOrParameter = getVariableOrParameter(varName, line, col);
ensureReferencesStruct(variableOrParameter, line, col);
var result = new DestructorCall(varName);
result.line = line; result.line = line;
result.col = col; result.col = col;
return result; 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<TerminalNode> 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;
}
} }

View File

@@ -210,7 +210,7 @@ public class GetDefinitions extends KlangBaseVisitor<Node> {
parameters.put(paramName, parameter); 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.type = Type.getByName(ctx.returnType.type().getText());
functionDef.line = line; functionDef.line = line;
functionDef.col = col; functionDef.col = col;

View File

@@ -1,17 +1,20 @@
package de.hsrm.compiler.Klang.nodes; package de.hsrm.compiler.Klang.nodes;
import de.hsrm.compiler.Klang.nodes.statements.VariableDeclaration;
import de.hsrm.compiler.Klang.visitors.Visitor; import de.hsrm.compiler.Klang.visitors.Visitor;
public class FunctionDefinition extends Node { public class FunctionDefinition extends Node {
public String name; public String name;
public Parameter[] parameters; public Parameter[] parameters;
public VariableDeclaration[] localVariables;
public Block block; 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.name = name;
this.parameters = parameters; this.parameters = parameters;
this.block = block; this.block = block;
this.localVariables = localVariables;
} }
@Override @Override

View File

@@ -1,11 +1,6 @@
package de.hsrm.compiler.Klang.types; package de.hsrm.compiler.Klang.types;
import de.hsrm.compiler.Klang.Value; 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 class NamedType extends Type {
public String name; public String name;
@@ -49,8 +44,7 @@ public class NamedType extends Type {
return true; return true;
} }
if (that instanceof NamedType) { if (that instanceof NamedType thatType) {
var thatType = (NamedType) that;
return getName().equals(thatType.getName()); return getName().equals(thatType.getName());
} }

View File

@@ -17,6 +17,6 @@ public class DestroyStatementTest {
ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums);
Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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());
} }
} }

View File

@@ -17,7 +17,7 @@ public class FieldAssignmentTest {
ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums);
Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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 @Test
@@ -29,6 +29,6 @@ public class FieldAssignmentTest {
ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums);
Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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());
} }
} }

View File

@@ -92,4 +92,28 @@ public class FunctionDefinitionTest {
var e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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()); 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));
}
} }

View File

@@ -16,7 +16,7 @@ public class StructFieldAccessTest {
ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums);
Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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 @Test
@@ -28,6 +28,6 @@ public class StructFieldAccessTest {
ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums);
Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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());
} }
} }

View File

@@ -17,6 +17,6 @@ public class VariableAssignmentTest {
ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums);
Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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());
} }
} }

View File

@@ -17,7 +17,7 @@ public class VariableTest {
ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums); ContextAnalysis ctxAnal = new ContextAnalysis(funcs, structs, enums);
Exception e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); 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 @Test