diff --git a/src/main/antlr4/de/hsrm/compiler/Klang/Klang.g4 b/src/main/antlr4/de/hsrm/compiler/Klang/Klang.g4 index f752ddb..5eb15fc 100644 --- a/src/main/antlr4/de/hsrm/compiler/Klang/Klang.g4 +++ b/src/main/antlr4/de/hsrm/compiler/Klang/Klang.g4 @@ -107,6 +107,7 @@ atom : INTEGER_LITERAL #intAtom | BOOLEAN_LITERAL #boolAtom | FLOAT_LITERAL #floatAtom + | CHAR_LITERAL #charAtom | NULL # nullAtom | IDENT #variable ; @@ -119,6 +120,7 @@ type : INTEGER | BOOLEAN | FLOAT + | CHAR | IDENT | VOID ; @@ -184,9 +186,12 @@ SUB: '-'; MOD: '%'; DIV: '/'; +SQUOT: '\''; + BOOLEAN: 'bool'; INTEGER: 'int'; FLOAT: 'float'; +CHAR: 'char'; VOID: 'void'; INTEGER_LITERAL @@ -202,6 +207,10 @@ BOOLEAN_LITERAL | 'false' ; +CHAR_LITERAL + : SQUOT [ -~] SQUOT + ; + IDENT : [a-zA-Z][a-zA-Z0-9]* ; diff --git a/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java b/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java index cbb923e..0fed5bb 100644 --- a/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java +++ b/src/main/java/de/hsrm/compiler/Klang/ContextAnalysis.java @@ -571,7 +571,7 @@ public class ContextAnalysis extends KlangBaseVisitor { } checkNumeric(lhs, rhs, line, col); - + result.line = line; result.col = col; return result; @@ -605,7 +605,7 @@ public class ContextAnalysis extends KlangBaseVisitor { int line = ctx.start.getLine(); int col = ctx.start.getCharPositionInLine(); DivisionExpression result = new DivisionExpression((Expression) lhs, (Expression) rhs); - + try { result.type = lhs.type.combine(rhs.type); } catch (Exception e) { @@ -613,7 +613,7 @@ public class ContextAnalysis extends KlangBaseVisitor { } checkNumeric(lhs, rhs, line, col); - + result.line = line; result.col = col; return result; @@ -639,7 +639,7 @@ public class ContextAnalysis extends KlangBaseVisitor { String error = "Only integers are allowed for modulo."; throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } - + result.line = line; result.col = col; return result; @@ -652,7 +652,7 @@ public class ContextAnalysis extends KlangBaseVisitor { result.type = expression.type; result.line = ctx.start.getLine(); result.col = ctx.start.getCharPositionInLine(); - + if (!result.type.isNumericType()) { String error = "Only numeric types are allowed for this expression."; throw new RuntimeException(Helper.getErrorPrefix(result.line, result.col) + error); @@ -730,6 +730,17 @@ public class ContextAnalysis extends KlangBaseVisitor { return n; } + @Override + public Node visitCharAtom(KlangParser.CharAtomContext ctx) { + // I had no idea how to design the grammar so that the quotes + // are separate from the char, so I cheat here... + var n = new CharExpression(ctx.CHAR_LITERAL().getText().charAt(1)); + n.type = Type.getCharType(); + n.line = ctx.start.getLine(); + n.col = ctx.start.getCharPositionInLine(); + return n; + } + @Override public Node visitNullAtom(KlangParser.NullAtomContext ctx) { Node n = new NullExpression(); @@ -929,14 +940,14 @@ public class ContextAnalysis extends KlangBaseVisitor { String name = ctx.IDENT().getText(); int line = ctx.start.getLine(); int col = ctx.start.getCharPositionInLine(); - + // Get the corresponding struct definition var struct = this.structDefs.get(name); if (struct == null) { String error = "Struct with name \"" + name + "\" not defined."; throw new RuntimeException(Helper.getErrorPrefix(line, col) + error); } - + // Make sure the number of arguments match the number of struct fields int fieldCount = struct.fields.length; int argCount = ctx.arguments().expression().size(); diff --git a/src/main/java/de/hsrm/compiler/Klang/Value.java b/src/main/java/de/hsrm/compiler/Klang/Value.java index 112d1de..dfe87a8 100644 --- a/src/main/java/de/hsrm/compiler/Klang/Value.java +++ b/src/main/java/de/hsrm/compiler/Klang/Value.java @@ -4,8 +4,8 @@ import de.hsrm.compiler.Klang.types.Type; import java.util.Map; public class Value { + private final Object value; public Type type; - private Object value; public Value(Object value) { this.value = value; @@ -17,19 +17,23 @@ public class Value { } public Object asObject() { - return this.value; + return value; } public int asInteger() { - return (int) this.value; + return (int) value; } public double asFloat() { - return (double) this.value; + return (double) value; } public boolean asBoolean() { - return (boolean) this.value; + return (boolean) value; + } + + public char asChar() { + return (char) value; } @SuppressWarnings("unchecked") diff --git a/src/main/java/de/hsrm/compiler/Klang/nodes/expressions/CharExpression.java b/src/main/java/de/hsrm/compiler/Klang/nodes/expressions/CharExpression.java new file mode 100644 index 0000000..88b656f --- /dev/null +++ b/src/main/java/de/hsrm/compiler/Klang/nodes/expressions/CharExpression.java @@ -0,0 +1,16 @@ +package de.hsrm.compiler.Klang.nodes.expressions; + +import de.hsrm.compiler.Klang.visitors.Visitor; + +public class CharExpression extends Expression { + public char c; + + public CharExpression(char c) { + this.c = c; + } + + @Override + public R welcome(Visitor v) { + return v.visit(this); + } +} diff --git a/src/main/java/de/hsrm/compiler/Klang/types/CharType.java b/src/main/java/de/hsrm/compiler/Klang/types/CharType.java new file mode 100644 index 0000000..f339fe8 --- /dev/null +++ b/src/main/java/de/hsrm/compiler/Klang/types/CharType.java @@ -0,0 +1,43 @@ +package de.hsrm.compiler.Klang.types; + +import de.hsrm.compiler.Klang.Value; + +public class CharType extends PrimitiveType { + private static CharType instance = null; + + public static CharType getType() { + if (instance != null) { + return instance; + } + instance = new CharType(); + return instance; + } + + @Override + public String getName() { + return "char"; + } + + @Override + public Type combine(Type that) { + if (!this.equals(that)) { + throw new RuntimeException("Type mismatch: cannot combine " + getName() + " and " + getName()); + } + + return this; + } + + @Override + public boolean valuesEqual(Value a, Value b) { + return ((int) a.asChar()) == ((int) b.asChar()); + } + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + + return that instanceof CharType; + } +} diff --git a/src/main/java/de/hsrm/compiler/Klang/types/Type.java b/src/main/java/de/hsrm/compiler/Klang/types/Type.java index a8261df..752d73e 100644 --- a/src/main/java/de/hsrm/compiler/Klang/types/Type.java +++ b/src/main/java/de/hsrm/compiler/Klang/types/Type.java @@ -23,6 +23,10 @@ public abstract class Type { return FloatType.getType(); } + public static CharType getCharType() { + return CharType.getType(); + } + public static NullType getNullType() { return NullType.getType(); } @@ -36,6 +40,7 @@ public abstract class Type { case "bool": return getBooleanType(); case "int": return getIntegerType(); case "float": return getFloatType(); + case "char": return getCharType(); case "null": return getNullType(); case "void": return getVoidType(); default: return new NamedType(name); diff --git a/src/main/java/de/hsrm/compiler/Klang/visitors/EvalVisitor.java b/src/main/java/de/hsrm/compiler/Klang/visitors/EvalVisitor.java index 9cf28c5..5678d2f 100644 --- a/src/main/java/de/hsrm/compiler/Klang/visitors/EvalVisitor.java +++ b/src/main/java/de/hsrm/compiler/Klang/visitors/EvalVisitor.java @@ -10,7 +10,6 @@ import de.hsrm.compiler.Klang.nodes.loops.DoWhileLoop; 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.NamedType; import de.hsrm.compiler.Klang.types.NullType; import de.hsrm.compiler.Klang.types.Type; @@ -46,6 +45,11 @@ public class EvalVisitor implements Visitor { return result; } + @Override + public Value visit(CharExpression e) { + return new Value(e.c, Type.getCharType()); + } + @Override public Value visit(EqualityExpression e) { var lhs = e.lhs.welcome(this); diff --git a/src/main/java/de/hsrm/compiler/Klang/visitors/GenASM.java b/src/main/java/de/hsrm/compiler/Klang/visitors/GenASM.java index 5fe6401..80b47dc 100644 --- a/src/main/java/de/hsrm/compiler/Klang/visitors/GenASM.java +++ b/src/main/java/de/hsrm/compiler/Klang/visitors/GenASM.java @@ -102,6 +102,12 @@ public class GenASM implements Visitor { return null; } + @Override + public Void visit(CharExpression e) { + asm.mov("q", "$" + ((int) e.c), "%rax"); + return null; + } + @Override public Void visit(Variable e) { if (e.type.equals(Type.getFloatType())) { diff --git a/src/main/java/de/hsrm/compiler/Klang/visitors/PrettyPrintVisitor.java b/src/main/java/de/hsrm/compiler/Klang/visitors/PrettyPrintVisitor.java index 470e3dd..8505a01 100644 --- a/src/main/java/de/hsrm/compiler/Klang/visitors/PrettyPrintVisitor.java +++ b/src/main/java/de/hsrm/compiler/Klang/visitors/PrettyPrintVisitor.java @@ -97,6 +97,14 @@ public class PrettyPrintVisitor implements Visitor { return null; } + @Override + public Void visit(CharExpression e) { + ex.write("'"); + ex.write(e.c); + ex.write("'"); + return null; + } + @Override public Void visit(EqualityExpression e) { ex.write("("); @@ -323,7 +331,7 @@ public class PrettyPrintVisitor implements Visitor { public Void visit(Block e) { ex.write("{"); ex.addIndent(); - for (Statement stmt : e.statements) { + for (var stmt : e.statementsOrFunctionCalls) { ex.nl(); stmt.welcome(this); if (stmt.getClass() == VariableAssignment.class || stmt.getClass() == VariableDeclaration.class) { diff --git a/src/main/java/de/hsrm/compiler/Klang/visitors/Visitor.java b/src/main/java/de/hsrm/compiler/Klang/visitors/Visitor.java index f56e8b6..1276482 100644 --- a/src/main/java/de/hsrm/compiler/Klang/visitors/Visitor.java +++ b/src/main/java/de/hsrm/compiler/Klang/visitors/Visitor.java @@ -12,6 +12,7 @@ public interface Visitor { R visit(IntegerExpression e); R visit(FloatExpression e); R visit(BooleanExpression e); + R visit(CharExpression e); R visit(Variable e); R visit(AdditionExpression e); R visit(EqualityExpression e); diff --git a/src/test/java/CharTest.java b/src/test/java/CharTest.java new file mode 100644 index 0000000..cd334f1 --- /dev/null +++ b/src/test/java/CharTest.java @@ -0,0 +1,125 @@ +import de.hsrm.compiler.Klang.ContextAnalysis; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class CharTest { + + @Test + void shouldNotThrowIfCharLiteralIsAssignedToCharVariable() { + // given + var tree = Helper.prepareParser(""" + function bar(): int { + let c: char = 'c'; + return 1; + } + bar(); + """); + var ctxAnal = new ContextAnalysis(Helper.getFuncs(tree), Helper.getStructs(tree), Helper.getEnums(tree)); + + // when / then + assertDoesNotThrow(() -> ctxAnal.visit(tree)); + } + + @Test + void shouldNotThrowIfCharLiteralIsReturnedFromFunction() { + // given + var tree = Helper.prepareParser(""" + function bar(): char { + return 'c'; + } + bar(); + """); + var ctxAnal = new ContextAnalysis(Helper.getFuncs(tree), Helper.getStructs(tree), Helper.getEnums(tree)); + + // when / then + assertDoesNotThrow(() -> ctxAnal.visit(tree)); + } + + @Test + void shouldNotThrowIfCharIsUsedInStruct() { + // given + var tree = Helper.prepareParser(""" + struct myChar { c: char; } + function bar(): myChar { + let x: myChar = create myChar('c'); + return x; + } + bar(); + """); + var ctxAnal = new ContextAnalysis(Helper.getFuncs(tree), Helper.getStructs(tree), Helper.getEnums(tree)); + + // when / then + assertDoesNotThrow(() -> ctxAnal.visit(tree)); + } + + @Test + void shouldThrowIfCharIsAssignedToOtherTypedVariable() { + // given + var tree = Helper.prepareParser(""" + function bar(): int { + let c: int = 'c'; + return 1; + } + bar(); + """); + var ctxAnal = new ContextAnalysis(Helper.getFuncs(tree), Helper.getStructs(tree), Helper.getEnums(tree)); + + // when / then + var e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); + assertEquals("Error in line 2:4 Type mismatch: cannot combine int and char", e.getMessage()); + } + + @Test + void shouldThrowWhenReturningCharFromOtherTypedFunction() { + // given + var tree = Helper.prepareParser(""" + function bar(): int { + return 'c'; + } + bar(); + """); + var ctxAnal = new ContextAnalysis(Helper.getFuncs(tree), Helper.getStructs(tree), Helper.getEnums(tree)); + + // when / then + var e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); + assertEquals("Error in line 2:4 Type mismatch: cannot combine int and char", e.getMessage()); + } + + @Test + void shouldThrowPassingCharToOtherTypedFunctionParameter() { + // given + var tree = Helper.prepareParser(""" + function foo(a: float): void { + let x: bool = false; + } + function bar(): void { + foo('c'); + } + bar(); + """); + var ctxAnal = new ContextAnalysis(Helper.getFuncs(tree), Helper.getStructs(tree), Helper.getEnums(tree)); + + // when / then + var e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); + assertEquals("Error in line 5:4 argument 0 Expected float but got: char", e.getMessage()); + } + + @Test + void shouldThrowWhenAssigningCharToOtherTypedStructField() { + // given + var tree = Helper.prepareParser(""" + struct foo { a: int; } + function bar(): void { + let x: foo = create foo(1); + x.a = 'c'; + } + bar(); + """); + var ctxAnal = new ContextAnalysis(Helper.getFuncs(tree), Helper.getStructs(tree), Helper.getEnums(tree)); + + // when / then + var e = assertThrows(RuntimeException.class, () -> ctxAnal.visit(tree)); + assertEquals("Error in line 4:4 Type mismatch: cannot combine int and char", e.getMessage()); + } +}