diff --git a/examples/basics/classes.own b/examples/basics/classes.own new file mode 100644 index 0000000..b46c12f --- /dev/null +++ b/examples/basics/classes.own @@ -0,0 +1,27 @@ +use ["std"] + +class Point { + def Point(x = 0, y = 0) { + this.x = x + this.y = y + } + + def moveBy(p) { + this.move(p.x, p.y) + } + + def move(dx, dy) { + this.x += dx + this.y += dy + } + + def toString() = "(" + this.x + ", " + this.y + ")" +} + +p = new Point(20, 30) +p.move(10, -5) +println p.toString() + +p2 = new Point(1, 1) +p2.moveBy(p) +println p2.toString() diff --git a/src/main/java/com/annimon/ownlang/exceptions/UnknownClassException.java b/src/main/java/com/annimon/ownlang/exceptions/UnknownClassException.java new file mode 100644 index 0000000..40c29b9 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/exceptions/UnknownClassException.java @@ -0,0 +1,15 @@ +package com.annimon.ownlang.exceptions; + +public final class UnknownClassException extends RuntimeException { + + private final String className; + + public UnknownClassException(String name) { + super("Unknown class " + name); + this.className = name; + } + + public String getClassName() { + return className; + } +} diff --git a/src/main/java/com/annimon/ownlang/lib/ClassDeclarations.java b/src/main/java/com/annimon/ownlang/lib/ClassDeclarations.java new file mode 100644 index 0000000..1335b1b --- /dev/null +++ b/src/main/java/com/annimon/ownlang/lib/ClassDeclarations.java @@ -0,0 +1,37 @@ +package com.annimon.ownlang.lib; + +import com.annimon.ownlang.exceptions.UnknownFunctionException; +import com.annimon.ownlang.parser.ast.ClassDeclarationStatement; +import java.util.HashMap; +import java.util.Map; + +public final class ClassDeclarations { + + private static final Map declarations; + static { + declarations = new HashMap<>(); + } + + private ClassDeclarations() { } + + public static void clear() { + declarations.clear(); + } + + public static Map getAll() { + return declarations; + } + + public static boolean isExists(String key) { + return declarations.containsKey(key); + } + + public static ClassDeclarationStatement get(String key) { + if (!isExists(key)) throw new UnknownFunctionException(key); + return declarations.get(key); + } + + public static void set(String key, ClassDeclarationStatement classDef) { + declarations.put(key, classDef); + } +} diff --git a/src/main/java/com/annimon/ownlang/lib/ClassInstanceValue.java b/src/main/java/com/annimon/ownlang/lib/ClassInstanceValue.java new file mode 100644 index 0000000..febcfa9 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/lib/ClassInstanceValue.java @@ -0,0 +1,98 @@ +package com.annimon.ownlang.lib; + +import com.annimon.ownlang.exceptions.TypeException; +import java.util.Objects; + +public class ClassInstanceValue implements Value { + + private final String className; + private final MapValue thisMap; + private ClassMethod constructor; + + public ClassInstanceValue(String name) { + this.className = name; + thisMap = new MapValue(10); + } + + public MapValue getThisMap() { + return thisMap; + } + + public String getClassName() { + return className; + } + + public void addField(String name, Value value) { + thisMap.set(name, value); + } + + public void addMethod(String name, ClassMethod method) { + thisMap.set(name, method); + if (name.equals(className)) { + constructor = method; + } + } + + public void callConstructor(Value[] args) { + if (constructor != null) { + constructor.execute(args); + } + } + + public Value access(Value value) { + return thisMap.get(value); + } + + @Override + public Object raw() { + return null; + } + + @Override + public int asInt() { + throw new TypeException("Cannot cast class to integer"); + } + + @Override + public double asNumber() { + throw new TypeException("Cannot cast class to integer"); + } + + @Override + public String asString() { + return "class " + className + "@" + thisMap; + } + + @Override + public int type() { + return Types.CLASS; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 37 * hash + Objects.hash(className, thisMap); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) + return false; + final ClassInstanceValue other = (ClassInstanceValue) obj; + return Objects.equals(this.className, other.className) + && Objects.equals(this.thisMap, other.thisMap); + } + + @Override + public int compareTo(Value o) { + return asString().compareTo(o.asString()); + } + + @Override + public String toString() { + return asString(); + } +} diff --git a/src/main/java/com/annimon/ownlang/lib/ClassMethod.java b/src/main/java/com/annimon/ownlang/lib/ClassMethod.java new file mode 100644 index 0000000..a595039 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/lib/ClassMethod.java @@ -0,0 +1,26 @@ +package com.annimon.ownlang.lib; + +import com.annimon.ownlang.parser.ast.Arguments; +import com.annimon.ownlang.parser.ast.Statement; + +public class ClassMethod extends UserDefinedFunction { + + public final ClassInstanceValue classInstance; + + public ClassMethod(Arguments arguments, Statement body, ClassInstanceValue classInstance) { + super(arguments, body); + this.classInstance = classInstance; + } + + @Override + public Value execute(Value[] values) { + Variables.push(); + Variables.define("this", classInstance.getThisMap()); + + try { + return super.execute(values); + } finally { + Variables.pop(); + } + } +} diff --git a/src/main/java/com/annimon/ownlang/lib/Classes.java b/src/main/java/com/annimon/ownlang/lib/Classes.java new file mode 100644 index 0000000..488d546 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/lib/Classes.java @@ -0,0 +1,36 @@ +package com.annimon.ownlang.lib; + +import com.annimon.ownlang.exceptions.UnknownClassException; +import java.util.HashMap; +import java.util.Map; + +public final class Classes { + + private static final Map classes; + static { + classes = new HashMap<>(); + } + + private Classes() { } + + public static void clear() { + classes.clear(); + } + + public static Map getFunctions() { + return classes; + } + + public static boolean isExists(String key) { + return classes.containsKey(key); + } + + public static ClassInstanceValue get(String key) { + if (!isExists(key)) throw new UnknownClassException(key); + return classes.get(key); + } + + public static void set(String key, ClassInstanceValue classDef) { + classes.put(key, classDef); + } +} diff --git a/src/main/java/com/annimon/ownlang/lib/Types.java b/src/main/java/com/annimon/ownlang/lib/Types.java index 8c0b216..ab53368 100644 --- a/src/main/java/com/annimon/ownlang/lib/Types.java +++ b/src/main/java/com/annimon/ownlang/lib/Types.java @@ -8,11 +8,14 @@ public final class Types { STRING = 2, ARRAY = 3, MAP = 4, - FUNCTION = 5; + FUNCTION = 5, + CLASS = 6; private static final int FIRST = OBJECT; - private static final int LAST = FUNCTION; - private static final String[] NAMES = {"object", "number", "string", "array", "map", "function"}; + private static final int LAST = CLASS; + private static final String[] NAMES = { + "object", "number", "string", "array", "map", "function", "class" + }; public static String typeToString(int type) { if (FIRST <= type && type <= LAST) { diff --git a/src/main/java/com/annimon/ownlang/lib/UserDefinedFunction.java b/src/main/java/com/annimon/ownlang/lib/UserDefinedFunction.java index 965be46..6b0ba78 100644 --- a/src/main/java/com/annimon/ownlang/lib/UserDefinedFunction.java +++ b/src/main/java/com/annimon/ownlang/lib/UserDefinedFunction.java @@ -10,7 +10,7 @@ import com.annimon.ownlang.parser.ast.Statement; * * @author aNNiMON */ -public final class UserDefinedFunction implements Function { +public class UserDefinedFunction implements Function { public final Arguments arguments; public final Statement body; diff --git a/src/main/java/com/annimon/ownlang/parser/Lexer.java b/src/main/java/com/annimon/ownlang/parser/Lexer.java index 3bc71f5..4bed518 100644 --- a/src/main/java/com/annimon/ownlang/parser/Lexer.java +++ b/src/main/java/com/annimon/ownlang/parser/Lexer.java @@ -105,6 +105,8 @@ public final class Lexer { KEYWORDS.put("case", TokenType.CASE); KEYWORDS.put("extract", TokenType.EXTRACT); KEYWORDS.put("include", TokenType.INCLUDE); + KEYWORDS.put("class", TokenType.CLASS); + KEYWORDS.put("new", TokenType.NEW); } public static Set getKeywords() { diff --git a/src/main/java/com/annimon/ownlang/parser/Parser.java b/src/main/java/com/annimon/ownlang/parser/Parser.java index 545300f..b23851b 100644 --- a/src/main/java/com/annimon/ownlang/parser/Parser.java +++ b/src/main/java/com/annimon/ownlang/parser/Parser.java @@ -159,6 +159,9 @@ public final class Parser { if (match(TokenType.MATCH)) { return match(); } + if (match(TokenType.CLASS)) { + return classDeclaration(); + } if (lookMatch(0, TokenType.WORD) && lookMatch(1, TokenType.LPAREN)) { return new ExprStatement(functionChain(qualifiedName())); } @@ -437,6 +440,30 @@ public final class Parser { return new MatchExpression(expression, patterns); } + + private Statement classDeclaration() { + // class Name { + // x = 123 + // str = "" + // def method() = str + // } + final String name = consume(TokenType.WORD).getText(); + final ClassDeclarationStatement classDeclaration = new ClassDeclarationStatement(name); + consume(TokenType.LBRACE); + do { + if (match(TokenType.DEF)) { + classDeclaration.addMethod(functionDefine()); + } else { + final AssignmentExpression fieldDeclaration = assignmentStrict(); + if (fieldDeclaration != null) { + classDeclaration.addField(fieldDeclaration); + } else { + throw new ParseException("Class can contain only assignments and function declarations"); + } + } + } while (!match(TokenType.RBRACE)); + return classDeclaration; + } private Expression expression() { return assignment(); @@ -450,7 +477,7 @@ public final class Parser { return ternary(); } - private Expression assignmentStrict() { + private AssignmentExpression assignmentStrict() { // x[0].prop += ... final int position = pos; final Expression targetExpr = qualifiedName(); @@ -667,23 +694,23 @@ public final class Parser { } private Expression multiplicative() { - Expression result = unary(); + Expression result = objectCreation(); while (true) { if (match(TokenType.STAR)) { - result = new BinaryExpression(BinaryExpression.Operator.MULTIPLY, result, unary()); + result = new BinaryExpression(BinaryExpression.Operator.MULTIPLY, result, expression()); continue; } if (match(TokenType.SLASH)) { - result = new BinaryExpression(BinaryExpression.Operator.DIVIDE, result, unary()); + result = new BinaryExpression(BinaryExpression.Operator.DIVIDE, result, expression()); continue; } if (match(TokenType.PERCENT)) { - result = new BinaryExpression(BinaryExpression.Operator.REMAINDER, result, unary()); + result = new BinaryExpression(BinaryExpression.Operator.REMAINDER, result, expression()); continue; } if (match(TokenType.STARSTAR)) { - result = new BinaryExpression(BinaryExpression.Operator.POWER, result, unary()); + result = new BinaryExpression(BinaryExpression.Operator.POWER, result, expression()); continue; } break; @@ -691,6 +718,21 @@ public final class Parser { return result; } + + private Expression objectCreation() { + if (match(TokenType.NEW)) { + final String className = consume(TokenType.WORD).getText(); + final List args = new ArrayList<>(); + consume(TokenType.LPAREN); + while (!match(TokenType.RPAREN)) { + args.add(expression()); + match(TokenType.COMMA); + } + return new ObjectCreationExpression(className, args); + } + + return unary(); + } private Expression unary() { if (match(TokenType.PLUSPLUS)) { diff --git a/src/main/java/com/annimon/ownlang/parser/TokenType.java b/src/main/java/com/annimon/ownlang/parser/TokenType.java index 0418765..87d15eb 100644 --- a/src/main/java/com/annimon/ownlang/parser/TokenType.java +++ b/src/main/java/com/annimon/ownlang/parser/TokenType.java @@ -28,6 +28,8 @@ public enum TokenType { CASE, EXTRACT, INCLUDE, + CLASS, + NEW, PLUS, // + MINUS, // - diff --git a/src/main/java/com/annimon/ownlang/parser/ast/BinaryExpression.java b/src/main/java/com/annimon/ownlang/parser/ast/BinaryExpression.java index 999e94e..b6cdab0 100644 --- a/src/main/java/com/annimon/ownlang/parser/ast/BinaryExpression.java +++ b/src/main/java/com/annimon/ownlang/parser/ast/BinaryExpression.java @@ -308,7 +308,7 @@ public final class BinaryExpression implements Expression { "for " + Types.typeToString(value1.type())); } } - + private Value and(Value value1, Value value2) { switch (value1.type()) { case Types.NUMBER: return and((NumberValue) value1, value2); diff --git a/src/main/java/com/annimon/ownlang/parser/ast/ClassDeclarationStatement.java b/src/main/java/com/annimon/ownlang/parser/ast/ClassDeclarationStatement.java new file mode 100644 index 0000000..92a272c --- /dev/null +++ b/src/main/java/com/annimon/ownlang/parser/ast/ClassDeclarationStatement.java @@ -0,0 +1,46 @@ +package com.annimon.ownlang.parser.ast; + +import com.annimon.ownlang.lib.ClassDeclarations; +import java.util.ArrayList; +import java.util.List; + +public final class ClassDeclarationStatement implements Statement { + + public final String name; + public final List methods; + public final List fields; + + public ClassDeclarationStatement(String name) { + this.name = name; + methods = new ArrayList<>(); + fields = new ArrayList<>(); + } + + public void addField(AssignmentExpression expr) { + fields.add(expr); + } + + public void addMethod(FunctionDefineStatement statement) { + methods.add(statement); + } + + @Override + public void execute() { + ClassDeclarations.set(name, this); + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public R accept(ResultVisitor visitor, T t) { + return visitor.visit(this, t); + } + + @Override + public String toString() { + return String.format("class %s {\n %s %s}", name, fields, methods); + } +} diff --git a/src/main/java/com/annimon/ownlang/parser/ast/ContainerAccessExpression.java b/src/main/java/com/annimon/ownlang/parser/ast/ContainerAccessExpression.java index 1d5e915..d013717 100644 --- a/src/main/java/com/annimon/ownlang/parser/ast/ContainerAccessExpression.java +++ b/src/main/java/com/annimon/ownlang/parser/ast/ContainerAccessExpression.java @@ -51,6 +51,9 @@ public final class ContainerAccessExpression implements Expression, Accessible { case Types.STRING: return ((StringValue) container).access(lastIndex); + case Types.CLASS: + return ((ClassInstanceValue) container).access(lastIndex); + default: throw new TypeException("Array or map expected. Got " + Types.typeToString(container.type())); } diff --git a/src/main/java/com/annimon/ownlang/parser/ast/ObjectCreationExpression.java b/src/main/java/com/annimon/ownlang/parser/ast/ObjectCreationExpression.java new file mode 100644 index 0000000..e5edc27 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/parser/ast/ObjectCreationExpression.java @@ -0,0 +1,67 @@ +package com.annimon.ownlang.parser.ast; + +import com.annimon.ownlang.lib.*; +import java.util.Iterator; +import java.util.List; + +public final class ObjectCreationExpression implements Expression { + + public final String className; + public final List constructorArguments; + + public ObjectCreationExpression(String className, List constructorArguments) { + this.className = className; + this.constructorArguments = constructorArguments; + } + + @Override + public Value eval() { + final ClassDeclarationStatement cd = ClassDeclarations.get(className); + + // Create an instance and put evaluated fields with method declarations + final ClassInstanceValue instance = new ClassInstanceValue(className); + for (AssignmentExpression f : cd.fields) { + // TODO check only variable assignments + final String fieldName = ((VariableExpression) f.target).name; + instance.addField(fieldName, f.eval()); + } + for (FunctionDefineStatement m : cd.methods) { + instance.addMethod(m.name, new ClassMethod(m.arguments, m.body, instance)); + } + + // Call a constructor + final int argsSize = constructorArguments.size(); + final Value[] ctorArgs = new Value[argsSize]; + for (int i = 0; i < argsSize; i++) { + ctorArgs[i] = constructorArguments.get(i).eval(); + } + instance.callConstructor(ctorArgs); + + return instance; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + @Override + public R accept(ResultVisitor visitor, T t) { + return visitor.visit(this, t); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("new ").append(className).append(' '); + final Iterator it = constructorArguments.iterator(); + if (it.hasNext()) { + sb.append(it.next()); + while (it.hasNext()) { + sb.append(", ").append(it.next()); + } + } + sb.append(')'); + return sb.toString(); + } +} diff --git a/src/main/java/com/annimon/ownlang/parser/ast/ResultVisitor.java b/src/main/java/com/annimon/ownlang/parser/ast/ResultVisitor.java index a98e7ab..b512750 100644 --- a/src/main/java/com/annimon/ownlang/parser/ast/ResultVisitor.java +++ b/src/main/java/com/annimon/ownlang/parser/ast/ResultVisitor.java @@ -13,6 +13,7 @@ public interface ResultVisitor { R visit(BinaryExpression s, T t); R visit(BlockStatement s, T t); R visit(BreakStatement s, T t); + R visit(ClassDeclarationStatement s, T t); R visit(ConditionalExpression s, T t); R visit(ContainerAccessExpression s, T t); R visit(ContinueStatement s, T t); @@ -29,6 +30,7 @@ public interface ResultVisitor { R visit(IncludeStatement s, T t); R visit(MapExpression s, T t); R visit(MatchExpression s, T t); + R visit(ObjectCreationExpression s, T t); R visit(PrintStatement s, T t); R visit(PrintlnStatement s, T t); R visit(ReturnStatement s, T t); diff --git a/src/main/java/com/annimon/ownlang/parser/ast/Visitor.java b/src/main/java/com/annimon/ownlang/parser/ast/Visitor.java index 31ad18c..adee199 100644 --- a/src/main/java/com/annimon/ownlang/parser/ast/Visitor.java +++ b/src/main/java/com/annimon/ownlang/parser/ast/Visitor.java @@ -11,6 +11,7 @@ public interface Visitor { void visit(BinaryExpression s); void visit(BlockStatement s); void visit(BreakStatement s); + void visit(ClassDeclarationStatement s); void visit(ConditionalExpression s); void visit(ContainerAccessExpression s); void visit(ContinueStatement s); @@ -27,6 +28,7 @@ public interface Visitor { void visit(IncludeStatement s); void visit(MapExpression s); void visit(MatchExpression s); + void visit(ObjectCreationExpression s); void visit(PrintStatement s); void visit(PrintlnStatement s); void visit(ReturnStatement s); diff --git a/src/main/java/com/annimon/ownlang/parser/optimization/OptimizationVisitor.java b/src/main/java/com/annimon/ownlang/parser/optimization/OptimizationVisitor.java index 8db0e84..4311b41 100644 --- a/src/main/java/com/annimon/ownlang/parser/optimization/OptimizationVisitor.java +++ b/src/main/java/com/annimon/ownlang/parser/optimization/OptimizationVisitor.java @@ -75,6 +75,11 @@ public abstract class OptimizationVisitor implements ResultVisitor { return s; } + @Override + public Node visit(ClassDeclarationStatement s, T t) { + return s; + } + @Override public Node visit(ConditionalExpression s, T t) { final Node expr1 = s.expr1.accept(this, t); @@ -314,6 +319,24 @@ public abstract class OptimizationVisitor implements ResultVisitor { } return s; } + + @Override + public Node visit(ObjectCreationExpression s, T t) { + final List args = new ArrayList<>(); + boolean changed = false; + for (Expression argument : s.constructorArguments) { + final Node expr = argument.accept(this, t); + if (expr != argument) { + changed = true; + } + args.add((Expression) expr); + } + + if (changed) { + return new ObjectCreationExpression(s.className, args); + } + return s; + } @Override public Node visit(PrintStatement s, T t) { diff --git a/src/main/java/com/annimon/ownlang/parser/visitors/AbstractVisitor.java b/src/main/java/com/annimon/ownlang/parser/visitors/AbstractVisitor.java index 3dbc9c3..6455abf 100644 --- a/src/main/java/com/annimon/ownlang/parser/visitors/AbstractVisitor.java +++ b/src/main/java/com/annimon/ownlang/parser/visitors/AbstractVisitor.java @@ -39,6 +39,11 @@ public abstract class AbstractVisitor implements Visitor { public void visit(BreakStatement s) { } + @Override + public void visit(ClassDeclarationStatement s) { + + } + @Override public void visit(ConditionalExpression s) { s.expr1.accept(this); @@ -136,6 +141,13 @@ public abstract class AbstractVisitor implements Visitor { public void visit(MatchExpression s) { s.expression.accept(this); } + + @Override + public void visit(ObjectCreationExpression s) { + for (Expression argument : s.constructorArguments) { + argument.accept(this); + } + } @Override public void visit(PrintStatement s) { diff --git a/src/main/java/com/annimon/ownlang/parser/visitors/PrintVisitor.java b/src/main/java/com/annimon/ownlang/parser/visitors/PrintVisitor.java index 2e0f9ca..7d3ef2e 100644 --- a/src/main/java/com/annimon/ownlang/parser/visitors/PrintVisitor.java +++ b/src/main/java/com/annimon/ownlang/parser/visitors/PrintVisitor.java @@ -7,6 +7,7 @@ import com.annimon.ownlang.lib.Types; import com.annimon.ownlang.lib.UserDefinedFunction; import com.annimon.ownlang.parser.ast.*; import java.util.Iterator; +import java.util.List; import java.util.Map; public class PrintVisitor implements ResultVisitor { @@ -79,6 +80,23 @@ public class PrintVisitor implements ResultVisitor return t; } + @Override + public StringBuilder visit(ClassDeclarationStatement s, StringBuilder t) { + t.append("class ").append(s.name).append(" {"); + newLine(t); + + increaseIndent(); + for (AssignmentExpression field : s.fields) { + field.accept(this, t); + } + for (FunctionDefineStatement method : s.methods) { + method.accept(this, t); + } + decreaseIndent(); + t.append("}"); + return t; + } + @Override public StringBuilder visit(ConditionalExpression s, StringBuilder t) { s.expr1.accept(this, t); @@ -209,14 +227,7 @@ public class PrintVisitor implements ResultVisitor } else { s.functionExpr.accept(this, t); } - t.append("("); - boolean firstElement = true; - for (Expression expr : s.arguments) { - if (firstElement) firstElement = false; - else t.append(", "); - expr.accept(this, t); - } - t.append(")"); + printArgs(t, s.arguments); return t; } @@ -290,6 +301,13 @@ public class PrintVisitor implements ResultVisitor t.append("}"); return t; } + + @Override + public StringBuilder visit(ObjectCreationExpression s, StringBuilder t) { + t.append("new ").append(s.className); + printArgs(t, s.constructorArguments); + return t; + } @Override public StringBuilder visit(PrintStatement s, StringBuilder t) { @@ -422,6 +440,17 @@ public class PrintVisitor implements ResultVisitor } return t; } + + private void printArgs(StringBuilder t, List args) { + t.append("("); + boolean firstElement = true; + for (Expression expr : args) { + if (firstElement) firstElement = false; + else t.append(", "); + expr.accept(this, t); + } + t.append(")"); + } private void newLine(StringBuilder t) { t.append(Console.newline());