diff --git a/src/main/java/leekscript/common/SetType.java b/src/main/java/leekscript/common/SetType.java new file mode 100644 index 00000000..59ed31b9 --- /dev/null +++ b/src/main/java/leekscript/common/SetType.java @@ -0,0 +1,53 @@ +package leekscript.common; + +public class SetType extends Type { + + private Type type; + + public SetType(Type type) { + super(type == Type.VOID ? "Set" : type == Type.ANY ? "Set" : "Set<" + type.toString() + ">", "h", "SetLeekValue", "SetLeekValue", "new SetLeekValue()"); + this.type = type; + } + + @Override + public CastType accepts(Type type) { + if (type instanceof SetType at) { + if (at.type == Type.VOID) return CastType.UPCAST; + if (this.type == Type.VOID) return CastType.UPCAST; + var cast = this.type.accepts(at.type); + if (cast.ordinal() >= CastType.SAFE_DOWNCAST.ordinal()) return CastType.UNSAFE_DOWNCAST; + return cast; + } + return super.accepts(type); + } + + @Override + public Type element() { + return type; + } + + public boolean isSet() { + return true; + } + + public boolean canBeIterable() { + return true; + } + + public boolean isIterable() { + return true; + } + + @Override + public int hashCode() { + return this.type.hashCode() * 31 + 1; + } + + @Override + public boolean equals(Object object) { + if (object instanceof SetType at) { + return this.type.equals(at.type); + } + return false; + } +} diff --git a/src/main/java/leekscript/common/Type.java b/src/main/java/leekscript/common/Type.java index fe105a7a..f8807058 100644 --- a/src/main/java/leekscript/common/Type.java +++ b/src/main/java/leekscript/common/Type.java @@ -38,6 +38,7 @@ public class Type { public static final Type VOID = new Type("void", "v", "Object", "Object", "null"); // public static final Type LEGACY_ARRAY = new LegacyArrayType(); public static final Type ARRAY = array(Type.ANY); + public static final Type SET = set(Type.ANY); public static final Type INTERVAL = new Type("interval", "t", "IntervalLeekValue", "IntervalLeekValue", "new IntervalLeekValue()"); public static final Type ARRAY_INT = array(Type.INT); public static final Type ARRAY_REAL = array(Type.REAL); @@ -242,6 +243,10 @@ public static Type map(Type key, Type value) { return map; } + public static Type set(Type type) { + return new SetType(type); + } + public Type key() { if (this == ANY) { return Type.ANY; diff --git a/src/main/java/leekscript/compiler/WordCompiler.java b/src/main/java/leekscript/compiler/WordCompiler.java index ffaa3075..e2d11b3c 100644 --- a/src/main/java/leekscript/compiler/WordCompiler.java +++ b/src/main/java/leekscript/compiler/WordCompiler.java @@ -36,6 +36,7 @@ import leekscript.compiler.expression.LeekObject; import leekscript.compiler.expression.LeekParameterType; import leekscript.compiler.expression.LeekParenthesis; +import leekscript.compiler.expression.LeekSet; import leekscript.compiler.expression.LeekString; import leekscript.compiler.expression.LeekType; import leekscript.compiler.expression.LeekVariable; @@ -547,11 +548,13 @@ private LeekType eatPrimaryType(boolean first, boolean mandatory) throws LeekCom if (word.equals("string")) return new LeekType(mTokens.eat(), Type.STRING); if (word.equals("Class")) return new LeekType(mTokens.eat(), Type.CLASS); if (word.equals("Object")) return new LeekType(mTokens.eat(), Type.OBJECT); - if (word.equals("Array")) { + if (word.equals("Array") || word.equals("Set")) { + boolean isArray = word.equals("Array"); + var array = mTokens.eat(); - LeekType arrayType; + LeekType arrayOrSetType; if (mTokens.get().getType() == TokenType.OPERATOR && mTokens.get().getWord().equals("<")) { - arrayType = new LeekParameterType(array, mTokens.eat()); + arrayOrSetType = new LeekParameterType(array, mTokens.eat()); var value = eatType(false, true); Type valueType = Type.ANY; if (value != null) valueType = value.getType(); @@ -559,12 +562,12 @@ private LeekType eatPrimaryType(boolean first, boolean mandatory) throws LeekCom if (!mTokens.get().getWord().startsWith(">")) { addError(new AnalyzeError(mTokens.get(), AnalyzeErrorLevel.ERROR, Error.CLOSING_CHEVRON_EXPECTED)); } - ((LeekParameterType) arrayType).close(mTokens.eat()); - arrayType.setType(Type.array(valueType)); + ((LeekParameterType) arrayOrSetType).close(mTokens.eat()); + arrayOrSetType.setType(isArray ? Type.array(valueType) : Type.set(valueType)); } else { - arrayType = new LeekType(array, Type.ARRAY); + arrayOrSetType = new LeekType(array, isArray ? Type.ARRAY : Type.SET); } - return arrayType; + return arrayOrSetType; } if (word.equals("Map")) { var map = mTokens.eat(); @@ -1211,10 +1214,14 @@ public ClassMethodBlock classMethod(ClassDeclarationInstruction classDeclaration } public Expression readExpression() throws LeekCompilerException { - return readExpression(false); + return readExpression(false, false); } public Expression readExpression(boolean inList) throws LeekCompilerException { + return readExpression(inList, false); + } + + public Expression readExpression(boolean inList, boolean inSet) throws LeekCompilerException { var retour = new LeekExpression(); @@ -1429,7 +1436,7 @@ public Expression readExpression(boolean inList) throws LeekCompilerException { mTokens.unskip(); } - } else if (word.getType() == TokenType.OPERATOR) { + } else if (word.getType() == TokenType.OPERATOR && (!word.getWord().equals(">") || !inSet)) { int operator = Operators.getOperator(word.getWord(), getVersion()); @@ -1520,6 +1527,9 @@ public Expression readExpression(boolean inList) throws LeekCompilerException { } else if (word.getType() == TokenType.BRACKET_LEFT) { retour.addExpression(readArrayOrMapOrInterval(mTokens.eat())); + } else if (getVersion() >= 4 && word.getType() == TokenType.OPERATOR && word.getWord().equals("<")) { + retour.addExpression(readSet(mTokens.eat())); + } else if (getVersion() >= 2 && word.getType() == TokenType.ACCOLADE_LEFT) { // Déclaration d'un objet @@ -1641,6 +1651,24 @@ private boolean wordEquals(Token word, String expected) { return word.getWord().equals(expected); } + private Expression readSet(Token openingToken) throws LeekCompilerException { + var set = new LeekSet(openingToken); + + while (mTokens.get().getType() != TokenType.OPERATOR || !mTokens.get().getWord().equals(">")) { + if (isInterrupted()) throw new LeekCompilerException(mTokens.get(), Error.AI_TIMEOUT); + + set.addValue(readExpression(true, true)); + + if (mTokens.get().getType() == TokenType.VIRG) { + mTokens.skip(); + } + } + + set.setClosingToken(mTokens.get()); + + return set; + } + private Expression readArrayOrMapOrInterval(Token openingBracket) throws LeekCompilerException { if (version < 4) { return readLegacyArray(openingBracket); diff --git a/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java b/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java index 50869687..b0eb76dd 100644 --- a/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java +++ b/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java @@ -94,6 +94,7 @@ public MainLeekBlock(IACompiler compiler, WordCompiler wordCompiler, AIFile ai) if (ai.getVersion() >= 4) { addClass(new ClassDeclarationInstruction(new Token("Map"), 0, ai, true, this, Type.MAP)); addClass(new ClassDeclarationInstruction(new Token("Interval"), 0, ai, true, this, Type.INTERVAL)); + addClass(new ClassDeclarationInstruction(new Token("Set"), 0, ai, true, this, Type.SET)); } addClass(new ClassDeclarationInstruction(new Token("String"), 0, ai, true, this, Type.STRING)); var objectClass = new ClassDeclarationInstruction(new Token("Object"), 0, ai, true, this, Type.OBJECT); diff --git a/src/main/java/leekscript/compiler/expression/LeekSet.java b/src/main/java/leekscript/compiler/expression/LeekSet.java new file mode 100644 index 00000000..0da380fe --- /dev/null +++ b/src/main/java/leekscript/compiler/expression/LeekSet.java @@ -0,0 +1,103 @@ +package leekscript.compiler.expression; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.stream.Collectors; + +import leekscript.common.Type; +import leekscript.compiler.Hover; +import leekscript.compiler.Token; +import leekscript.compiler.JavaWriter; +import leekscript.compiler.Location; +import leekscript.compiler.WordCompiler; +import leekscript.compiler.bloc.MainLeekBlock; +import leekscript.compiler.exceptions.LeekCompilerException; + +public class LeekSet extends Expression { + + private final ArrayList mValues = new ArrayList(); + private Token openingToken; + private Token closingToken; + + public Type type = Type.SET; + + public LeekSet(Token openingToken) { + this.openingToken = openingToken; + } + + public void addValue(Expression param) { + mValues.add(param); + } + + public void setClosingToken(Token closingToken) { + this.closingToken = closingToken; + closingToken.setExpression(this); + openingToken.setExpression(this); + } + + @Override + public int getNature() { + return ARRAY; + } + + @Override + public Type getType() { + return type; + } + + @Override + public String toString() { + return mValues.stream().map(value -> value.toString()).collect(Collectors.joining(", ", "<", ">")); + } + + @Override + public boolean validExpression(WordCompiler compiler, MainLeekBlock mainblock) throws LeekExpressionException { + for (Expression parameter : mValues) { + parameter.validExpression(compiler, mainblock); + } + return true; + } + + @Override + public void preAnalyze(WordCompiler compiler) throws LeekCompilerException { + for (var value : mValues) { + value.preAnalyze(compiler); + } + } + + @Override + public void analyze(WordCompiler compiler) throws LeekCompilerException { + operations = 0; + + var types = new HashSet(); + for (var value : mValues) { + value.analyze(compiler); + operations += 2 + value.getOperations(); + types.add(value.getType()); + } + + this.type = Type.set(types.size() == 0 ? Type.VOID : Type.compound(types)); + } + + @Override + public void writeJavaCode(MainLeekBlock mainblock, JavaWriter writer) { + writer.addCode("new SetLeekValue(" + writer.getAIThis() + ", new Object[] { "); + for (int i = 0; i < mValues.size(); i++) { + if (i != 0) writer.addCode(", "); + mValues.get(i).writeJavaCode(mainblock, writer); + } + writer.addCode(" })"); + } + + @Override + public Location getLocation() { + return new Location(openingToken.getLocation(), closingToken.getLocation()); + } + + @Override + public Hover hover(Token token) { + var hover = new Hover(getType(), getLocation(), toString()); + hover.setSize(mValues.size()); + return hover; + } +} diff --git a/src/main/java/leekscript/runner/AI.java b/src/main/java/leekscript/runner/AI.java index 3b538b7f..7637845f 100644 --- a/src/main/java/leekscript/runner/AI.java +++ b/src/main/java/leekscript/runner/AI.java @@ -13,6 +13,7 @@ import leekscript.runner.values.GenericArrayLeekValue; import leekscript.runner.values.GenericMapLeekValue; import leekscript.runner.values.IntervalLeekValue; +import leekscript.runner.values.SetLeekValue; import leekscript.runner.values.LeekValue; import leekscript.runner.values.ObjectLeekValue; import leekscript.runner.values.Box; @@ -89,6 +90,7 @@ public abstract class AI { public final ClassLeekValue legacyArrayClass; public final ClassLeekValue mapClass; public final ClassLeekValue intervalClass; + public final ClassLeekValue setClass; public final ClassLeekValue stringClass; public final ClassLeekValue objectClass; public final ClassLeekValue functionClass; @@ -270,6 +272,7 @@ public double getDouble() { legacyArrayClass = new ClassLeekValue(this, "Array", valueClass); mapClass = new ClassLeekValue(this, "Map", valueClass); intervalClass = new ClassLeekValue(this, "Interval", valueClass); + setClass = new ClassLeekValue(this, "Set", valueClass); stringClass = new ClassLeekValue(this, "String", valueClass); objectClass = new ClassLeekValue(this, "Object", valueClass); functionClass = new ClassLeekValue(this, "Function", valueClass); @@ -1448,6 +1451,8 @@ public String export(Object value, Set visited) throws LeekRunException return ((ArrayLeekValue) value).getString(this, visited); } else if (value instanceof MapLeekValue) { return ((MapLeekValue) value).getString(this, visited); + } else if (value instanceof SetLeekValue) { + return ((SetLeekValue) value).getString(this, visited); } else if (value instanceof IntervalLeekValue) { return ((IntervalLeekValue) value).getString(this, visited); } else if (value instanceof String) { @@ -2985,6 +2990,7 @@ public ClassLeekValue classOf(Object value) { if (value instanceof ArrayLeekValue) return arrayClass; if (value instanceof MapLeekValue) return mapClass; if (value instanceof IntervalLeekValue) return intervalClass; + if (value instanceof SetLeekValue) return setClass; if (value instanceof String) return stringClass; if (value instanceof ObjectLeekValue) return ((ObjectLeekValue) value).clazz; if (value instanceof NativeObjectLeekValue) diff --git a/src/main/java/leekscript/runner/values/ClassLeekValue.java b/src/main/java/leekscript/runner/values/ClassLeekValue.java index b142860d..58d592d4 100644 --- a/src/main/java/leekscript/runner/values/ClassLeekValue.java +++ b/src/main/java/leekscript/runner/values/ClassLeekValue.java @@ -428,6 +428,9 @@ public Object run(AI ai, Object thiz, Object... arguments) throws LeekRunExcepti if (this == ai.mapClass) { return new MapLeekValue(ai); } + if (this == ai.setClass) { + return new SetLeekValue(ai); + } if (this == ai.objectClass) return new ObjectLeekValue(ai, ai.objectClass); // Create the actual object diff --git a/src/main/java/leekscript/runner/values/SetLeekValue.java b/src/main/java/leekscript/runner/values/SetLeekValue.java new file mode 100644 index 00000000..3d6344c8 --- /dev/null +++ b/src/main/java/leekscript/runner/values/SetLeekValue.java @@ -0,0 +1,70 @@ +package leekscript.runner.values; + +import java.util.HashSet; +import java.util.Set; + +import leekscript.runner.AI; +import leekscript.runner.LeekRunException; + +public class SetLeekValue { + + private final AI ai; + public final int id; + + private final HashSet set; + + public SetLeekValue(AI ai, Object[] values) throws LeekRunException { + this.ai = ai; + this.id = ai.getNextObjectID(); + this.set = new HashSet(); + for (Object value : values) { + this.set.add(value); + } + } + + public SetLeekValue(AI ai) throws LeekRunException { + this(ai, new Object[0]); + } + + @Override + public boolean equals(Object object) { + return object == this; + } + + @Override + public int hashCode() { + return this.id; + } + + public String getString(AI ai2, Set visited) throws LeekRunException { + visited.add(this); + return toString(ai, visited); + } + + public String toString(AI ai, Set visited) throws LeekRunException { + ai.ops(1); + + StringBuilder sb = new StringBuilder("<"); + + boolean first = true; + + for (Object value : set) { + if (first) { + first = false; + } else { + sb.append(", "); + } + + if (visited.contains(value)) { + sb.append("<...>"); + } else { + if (!ai.isPrimitive(value)) { + visited.add(value); + } + sb.append(ai.export(value, visited)); + } + } + + return sb.append(">").toString(); + } +} diff --git a/src/test/java/test/TestMain.java b/src/test/java/test/TestMain.java index 54368fe7..3a23529b 100644 --- a/src/test/java/test/TestMain.java +++ b/src/test/java/test/TestMain.java @@ -28,6 +28,7 @@ public static void main(String[] args) throws Exception { new TestNumber().run(); new TestBoolean().run(); new TestString().run(); + new TestSet().run(); new TestArray().run(); new TestArrayStress().run(); new TestMap().run(); diff --git a/src/test/java/test/TestSet.java b/src/test/java/test/TestSet.java new file mode 100644 index 00000000..36a87e3c --- /dev/null +++ b/src/test/java/test/TestSet.java @@ -0,0 +1,19 @@ +package test; + +public class TestSet extends TestCommon { + + public void run() throws Exception { + + section("Set.constructor()"); + code_v4_("return <1, 2>").debug().equals("<1, 2>"); + code_v4_("return <(1 > 2), (1 < 2)>").equals(""); + code_v4_("return <\"abc\", 1>").equals("<1, \"abc\">"); + code_v4_("return <>").equals("<>"); + DISABLED_code_v4_("return <<>>").equals("<<>>"); + code_v4_("<1, 2, 3, 4>").ops(8); + + section("Set typing"); + code_strict_v4_("Set i = <1, 2>; return i instanceof Set").equals("true"); + code_strict_v4_("Set i = <1, 2>; return i instanceof Set").equals("true"); + } +}