diff --git a/src/main/java/leekscript/common/EnumType.java b/src/main/java/leekscript/common/EnumType.java new file mode 100644 index 0000000..3a7e799 --- /dev/null +++ b/src/main/java/leekscript/common/EnumType.java @@ -0,0 +1,35 @@ +package leekscript.common; + +import leekscript.compiler.instruction.EnumDeclarationInstruction; + +public class EnumType extends Type { + + private EnumDeclarationInstruction enumDeclaration; + + public EnumType(EnumDeclarationInstruction enumDeclaration) { + super(enumDeclaration.getName(), "e", "Object", "Object", "null"); + this.enumDeclaration = enumDeclaration; + } + + @Override + public EnumDeclarationInstruction getEnumDeclaration() { + return enumDeclaration; + } + + @Override + public CastType accepts(Type type) { + if (type == this) return CastType.EQUALS; + if (this == ANY) return CastType.UPCAST; + if (type == ANY) return CastType.UNSAFE_DOWNCAST; + if (type instanceof EnumType et) { + if (this.enumDeclaration == et.enumDeclaration) return CastType.EQUALS; + return CastType.INCOMPATIBLE; + } + return super.accepts(type); + } + + @Override + public String getCode() { + return enumDeclaration.getName(); + } +} diff --git a/src/main/java/leekscript/common/EnumValueType.java b/src/main/java/leekscript/common/EnumValueType.java new file mode 100644 index 0000000..9676bf3 --- /dev/null +++ b/src/main/java/leekscript/common/EnumValueType.java @@ -0,0 +1,64 @@ +package leekscript.common; + +import leekscript.compiler.instruction.EnumDeclarationInstruction; + +public class EnumValueType extends Type { + + private EnumDeclarationInstruction enumDeclaration; + + public EnumValueType(EnumDeclarationInstruction enumDeclaration) { + super("Enum<" + (enumDeclaration == null ? "?" : enumDeclaration.getName()) + ">", "E", "EnumLeekValue", "EnumLeekValue", "null"); + this.enumDeclaration = enumDeclaration; + } + + public Type member(String member) { + if (member == null) return Type.VOID; + if (enumDeclaration == null) return Type.VOID; + var constant = enumDeclaration.getConstant(member); + if (constant != null) { + return constant.getType(); + } + return Type.VOID; + } + + @Override + public Type elementAccess(int version, boolean strict, String key) { + return member(key); + } + + @Override + public EnumDeclarationInstruction getEnumDeclaration() { + return enumDeclaration; + } + + @Override + public CastType accepts(Type type) { + if (type instanceof EnumValueType et) { + // Same concrete enum declaration -> exactly equal + if (this.enumDeclaration == et.enumDeclaration) { + return CastType.EQUALS; + } + // Generic Enum (null declaration) can accept any specific enum as an upcast + if (this.enumDeclaration == null && et.enumDeclaration != null) { + return CastType.UPCAST; + } + // Casting from a generic Enum to a specific enum is a downcast + if (this.enumDeclaration != null && et.enumDeclaration == null) { + return CastType.SAFE_DOWNCAST; + } + // Two different concrete enums are incompatible + return CastType.INCOMPATIBLE; + } + return super.accepts(type); + } + + @Override + public boolean isIndexable() { + return false; + } + + @Override + public String getCode() { + return enumDeclaration == null ? "Enum" : "Enum<" + enumDeclaration.getName() + ">"; + } +} diff --git a/src/main/java/leekscript/common/Error.java b/src/main/java/leekscript/common/Error.java index 760bccb..e949730 100644 --- a/src/main/java/leekscript/common/Error.java +++ b/src/main/java/leekscript/common/Error.java @@ -149,4 +149,7 @@ public enum Error { DEFAULT_ARGUMENT_NOT_END, // 145 CASE_OR_DEFAULT_EXPECTED, // 146 COLON_EXPECTED_AFTER_CASE, // 147 + INCOMPLETE_ENUM_SWITCH, // 148 + ENUM_MEMBER_DOES_NOT_EXIST, // 149 + DUPLICATED_ENUM_CONSTANT, // 150 } \ No newline at end of file diff --git a/src/main/java/leekscript/common/Type.java b/src/main/java/leekscript/common/Type.java index dcac3da..ea614c3 100644 --- a/src/main/java/leekscript/common/Type.java +++ b/src/main/java/leekscript/common/Type.java @@ -325,6 +325,10 @@ public ClassDeclarationInstruction getClassDeclaration() { return null; } + public leekscript.compiler.instruction.EnumDeclarationInstruction getEnumDeclaration() { + return null; + } + public static FunctionType add_argument(FunctionType current, Type argument) { // var entry = new AbstractMap.SimpleEntry(current, argument); // var cached = addArgumentTypes.get(entry); diff --git a/src/main/java/leekscript/compiler/LexicalParser.java b/src/main/java/leekscript/compiler/LexicalParser.java index d604404..1e133fc 100644 --- a/src/main/java/leekscript/compiler/LexicalParser.java +++ b/src/main/java/leekscript/compiler/LexicalParser.java @@ -201,7 +201,7 @@ private boolean tryParseIdentifier() { addToken(word, TokenType.CONST); } else if (version >= 3 && wordEquals(word, "char")) { addToken(word, TokenType.CHAR); - } else if (version >= 3 && wordEquals(word, "enum")) { + } else if (version >= 4 && wordEquals(word, "enum")) { addToken(word, TokenType.ENUM); } else if (version >= 3 && wordEquals(word, "eval")) { addToken(word, TokenType.EVAL); diff --git a/src/main/java/leekscript/compiler/WordCompiler.java b/src/main/java/leekscript/compiler/WordCompiler.java index 4626af5..4042e9d 100644 --- a/src/main/java/leekscript/compiler/WordCompiler.java +++ b/src/main/java/leekscript/compiler/WordCompiler.java @@ -45,6 +45,7 @@ import leekscript.compiler.expression.LeekVariable.VariableType; import leekscript.compiler.instruction.BlankInstruction; import leekscript.compiler.instruction.ClassDeclarationInstruction; +import leekscript.compiler.instruction.EnumDeclarationInstruction; import leekscript.compiler.instruction.LeekBreakInstruction; import leekscript.compiler.instruction.LeekContinueInstruction; import leekscript.compiler.instruction.LeekExpressionInstruction; @@ -218,6 +219,22 @@ public void firstPass() throws LeekCompilerException { // System.out.println("Define class " + clazz.getName()); } } + } else if (getVersion() >= 4 && mTokens.get().getType() == TokenType.ENUM) { + + mTokens.skip(); + if (mTokens.hasMoreTokens()) { + var enumName = mTokens.eat(); + + if (enumName.getType() == TokenType.STRING) { + + if (mMain.getDefinedClass(enumName.getWord()) != null || mMain.getDefinedEnum(enumName.getWord()) != null) { + throw new LeekCompilerException(enumName, Error.VARIABLE_NAME_UNAVAILABLE, new String[] { enumName.getWord() }); + } + + var enumDecl = new EnumDeclarationInstruction(enumName, mLine, mAI, getMainBlock()); + mMain.defineEnum(enumDecl); + } + } } else { mTokens.skip(); } @@ -359,6 +376,12 @@ private void compileWord() throws LeekCompilerException { classDeclaration(); return; + } else if (version >= 4 && getCurrentBlock() instanceof MainLeekBlock && word.getType() == TokenType.ENUM) { + + mTokens.skip(); + enumDeclaration(); + return; + } else if (word.getType() == TokenType.BREAK) { if (!mCurentBlock.isBreakable()) { @@ -699,6 +722,11 @@ private LeekType eatPrimaryType(boolean first, boolean mandatory) throws LeekCom return new LeekType(mTokens.eat(), clazz.getType()); } + var enumDecl = mMain.getDefinedEnum(word); + if (enumDecl != null) { + return new LeekType(mTokens.eat(), enumDecl.getType()); + } + if (mandatory) { addError(new AnalyzeError(mTokens.get(), AnalyzeErrorLevel.ERROR, Error.TYPE_EXPECTED)); } @@ -1208,6 +1236,59 @@ public void classDeclaration() throws LeekCompilerException { mCurrentClass = null; } + public void enumDeclaration() throws LeekCompilerException { + Token word = mTokens.eat(); + if (word.getType() != TokenType.STRING) { + throw new LeekCompilerException(word, Error.VAR_NAME_EXPECTED); + } + if (isKeyword(word)) { + addError(new AnalyzeError(word, AnalyzeErrorLevel.ERROR, Error.VARIABLE_NAME_UNAVAILABLE, new String[] { word.getWord() })); + } + EnumDeclarationInstruction enumDeclaration = mMain.getDefinedEnum(word.getWord()); + if (enumDeclaration == null) { + throw new LeekCompilerException(word, Error.UNKNOWN_ERROR); + } + mMain.addEnumList(enumDeclaration); + + if (mTokens.get().getType() != TokenType.ACCOLADE_LEFT) { + throw new LeekCompilerException(mTokens.get(), Error.OPENING_CURLY_BRACKET_EXPECTED); + } + mTokens.skip(); + + while (mTokens.hasMoreTokens() && mTokens.get().getType() != TokenType.ACCOLADE_RIGHT) { + if (isInterrupted()) throw new LeekCompilerException(mTokens.get(), Error.AI_TIMEOUT); + word = mTokens.get(); + if (word.getType() == TokenType.STRING) { + Token nameToken = mTokens.eat(); + + // Duplicate constant name inside the same enum + if (enumDeclaration.getConstant(nameToken.getWord()) != null) { + addError(new AnalyzeError(nameToken, AnalyzeErrorLevel.ERROR, Error.DUPLICATED_ENUM_CONSTANT, new String[] { + enumDeclaration.getName(), + nameToken.getWord() + })); + } else { + Expression value = null; + if (mTokens.hasMoreTokens() && mTokens.get().getType() == TokenType.OPERATOR && mTokens.get().getWord().equals("=")) { + mTokens.skip(); // skip '=' + var expr = readExpression(); + value = expr; + } + enumDeclaration.addConstant(nameToken, value); + } + } + if (mTokens.hasMoreTokens() && mTokens.get().getType() == TokenType.VIRG) { + mTokens.skip(); + } + } + if (mTokens.hasMoreTokens() && mTokens.get().getType() != TokenType.ACCOLADE_RIGHT) { + throw new LeekCompilerException(mTokens.get(), Error.END_OF_CLASS_EXPECTED); + } + if (mTokens.hasMoreTokens()) { + mTokens.skip(); // accolade right + } + } + public void classStaticMember(ClassDeclarationInstruction classDeclaration, AccessLevel accessLevel) throws LeekCompilerException { Token token = mTokens.get(); switch (token.getWord()) { @@ -1585,7 +1666,7 @@ public Expression readExpression(boolean inList, boolean inSet, boolean inInterv } else if (word.getType() == TokenType.DOT) { // Object access var dot = mTokens.eat(); - if (mTokens.get().getType() == TokenType.STRING || mTokens.get().getType() == TokenType.CLASS || mTokens.get().getType() == TokenType.SUPER) { + if (mTokens.get().getType() == TokenType.STRING || mTokens.get().getType() == TokenType.CLASS || mTokens.get().getType() == TokenType.ENUM || mTokens.get().getType() == TokenType.SUPER) { var name = mTokens.get(); retour.addObjectAccess(dot, name); } else { @@ -1765,6 +1846,8 @@ public Expression readExpression(boolean inList, boolean inSet, boolean inInterv } else if (word.getType() == TokenType.CLASS) { retour.addExpression(new LeekVariable(this, word, VariableType.LOCAL)); + } else if (word.getType() == TokenType.ENUM) { + retour.addExpression(new LeekVariable(this, word, VariableType.LOCAL)); } else if (word.getType() == TokenType.THIS) { retour.addExpression(new LeekVariable(this, word, VariableType.LOCAL)); } else if (word.getType() == TokenType.TRUE) { diff --git a/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java b/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java index 18cd68a..762362f 100644 --- a/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java +++ b/src/main/java/leekscript/compiler/bloc/MainLeekBlock.java @@ -23,6 +23,7 @@ import leekscript.compiler.expression.LeekNumber; import leekscript.compiler.expression.LeekVariable.VariableType; import leekscript.compiler.instruction.ClassDeclarationInstruction; +import leekscript.compiler.instruction.EnumDeclarationInstruction; import leekscript.compiler.instruction.LeekGlobalDeclarationInstruction; import leekscript.runner.LeekFunctions; import leekscript.common.Error; @@ -38,6 +39,8 @@ public class MainLeekBlock extends AbstractLeekBlock { private final Map mDefinedClasses = new TreeMap(); private final Map mUserClasses = new TreeMap(); private final List mUserClassesList = new ArrayList<>(); + private final Map mDefinedEnums = new TreeMap(); + private final List mUserEnumsList = new ArrayList<>(); private int mMinLevel = 1; private int mAnonymousId = 1; private int mFunctionId = 1; @@ -245,6 +248,9 @@ public String getCode() { if (clazz.internal) continue; str += clazz.getCode() + "\n"; } + for (var enumDecl : mUserEnumsList) { + str += enumDecl.getCode() + "\n"; + } return str + super.getCode(); } @@ -267,6 +273,11 @@ public void writeJavaCode(JavaWriter writer, String className, String AIClass, O clazz.declareJava(this, writer); } + // Enums + for (var enumDecl : mUserEnumsList) { + enumDecl.declareJava(this, writer); + } + // Constructor writer.addLine("public " + className + "() throws LeekRunException {"); writer.addLine("super(" + mInstructions.size() + ", " + mCompiler.getCurrentAI().getVersion() + ");"); @@ -275,6 +286,9 @@ public void writeJavaCode(JavaWriter writer, String className, String AIClass, O if (clazz.internal) continue; clazz.createJava(this, writer); } + for (var enumDecl : mUserEnumsList) { + enumDecl.createJava(this, writer); + } writer.addLine("}"); // Classes initialize functions @@ -419,10 +433,25 @@ public Map getDefinedClasses() { return mDefinedClasses; } + public void defineEnum(EnumDeclarationInstruction enumDeclaration) { + mDefinedEnums.put(enumDeclaration.getName(), enumDeclaration); + } + + public void addEnumList(EnumDeclarationInstruction enumDeclaration) { + mUserEnumsList.add(enumDeclaration); + } + + public EnumDeclarationInstruction getDefinedEnum(String name) { + return mDefinedEnums.get(name); + } + public void preAnalyze(WordCompiler compiler) throws LeekCompilerException { for (var clazz : mUserClassesList) { clazz.declare(compiler); } + for (var enumDecl : mUserEnumsList) { + enumDecl.declare(compiler); + } for (var function : mFunctions.values()) { function.declare(compiler); } @@ -432,6 +461,9 @@ public void preAnalyze(WordCompiler compiler) throws LeekCompilerException { for (var clazz : mUserClassesList) { clazz.preAnalyze(compiler); } + for (var enumDecl : mUserEnumsList) { + enumDecl.preAnalyze(compiler); + } for (var function : mFunctions.values()) { function.preAnalyze(compiler); } @@ -445,6 +477,9 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { for (var clazz : mUserClassesList) { clazz.analyze(compiler); } + for (var enumDecl : mUserEnumsList) { + enumDecl.analyze(compiler); + } for (var function : mFunctions.values()) { function.analyze(compiler); } diff --git a/src/main/java/leekscript/compiler/bloc/SwitchBlock.java b/src/main/java/leekscript/compiler/bloc/SwitchBlock.java index 289caf4..667ad48 100644 --- a/src/main/java/leekscript/compiler/bloc/SwitchBlock.java +++ b/src/main/java/leekscript/compiler/bloc/SwitchBlock.java @@ -2,6 +2,7 @@ import java.util.ArrayList; +import leekscript.common.EnumValueType; import leekscript.common.Type; import leekscript.compiler.JavaWriter; import leekscript.compiler.Location; @@ -9,6 +10,8 @@ import leekscript.compiler.WordCompiler; import leekscript.compiler.exceptions.LeekCompilerException; import leekscript.compiler.expression.Expression; +import leekscript.compiler.expression.LeekObjectAccess; +import leekscript.compiler.expression.LeekVariable; import leekscript.compiler.expression.LeekExpressionException; public class SwitchBlock extends AbstractLeekBlock { @@ -82,6 +85,62 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { } c.body.analyze(compiler); } + + // Enum switch exhaustiveness check (no default) + if (compiler.getVersion() < 4) { + return; + } + + // Detect if this is a switch over an enum by inspecting case values + leekscript.compiler.instruction.EnumDeclarationInstruction enumDecl = null; + boolean hasDefault = false; + var covered = new java.util.HashSet(); + + for (var c : mCases) { + if (c.isDefault) { + hasDefault = true; + continue; + } + for (var v : c.values) { + if (v instanceof LeekObjectAccess oa && oa.getObject() instanceof LeekVariable var) { + var objectType = var.getType(); + if (objectType instanceof EnumValueType evt) { + var currentEnum = evt.getEnumDeclaration(); + if (currentEnum == null) continue; + if (enumDecl == null) { + enumDecl = currentEnum; + } else if (enumDecl != currentEnum) { + // Mixed enums, abort enum-specific checks + return; + } + if (enumDecl == currentEnum) { + covered.add(oa.getField()); + } + } + } + } + } + + if (enumDecl == null) { + // Not an enum-based switch + return; + } + + if (!hasDefault && covered.size() < enumDecl.getConstantOrder().size()) { + // Collect missing constants to improve diagnostics + var missing = new java.util.ArrayList(); + for (var name : enumDecl.getConstantOrder()) { + if (!covered.contains(name)) { + missing.add(name); + } + } + compiler.addError(new leekscript.compiler.AnalyzeError( + getLocation(), + leekscript.compiler.AnalyzeError.AnalyzeErrorLevel.WARNING, + leekscript.common.Error.INCOMPLETE_ENUM_SWITCH, + new String[] { enumDecl.getName(), String.join(", ", missing) } + )); + } } @Override diff --git a/src/main/java/leekscript/compiler/expression/LeekExpression.java b/src/main/java/leekscript/compiler/expression/LeekExpression.java index 1140ef2..49278a6 100644 --- a/src/main/java/leekscript/compiler/expression/LeekExpression.java +++ b/src/main/java/leekscript/compiler/expression/LeekExpression.java @@ -924,15 +924,28 @@ public void writeJavaCode(MainLeekBlock mainblock, JavaWriter writer, boolean pa writer.addCode(")"); return; case Operators.AS: - if (mExpression2 instanceof LeekType lt && lt.getType().accepts(mExpression1.getType()) != CastType.EQUALS) { - if (parenthesis) writer.addCode("("); - writer.addCode("("); - mExpression2.writeJavaCode(mainblock, writer, false); - writer.addCode(") "); - } - mExpression1.writeJavaCode(mainblock, writer, true); - if (mExpression2 instanceof LeekType lt && lt.getType().accepts(mExpression1.getType()) != CastType.EQUALS) { - if (parenthesis) writer.addCode(")"); + if (mExpression2 instanceof LeekType lt) { + var targetType = lt.getType(); + var sourceType = mExpression1.getType(); + + // Special-case enum -> integer/real: use runtime conversion instead of Java cast + if (sourceType instanceof leekscript.common.EnumType && (targetType == Type.INT || targetType == Type.REAL)) { + writer.compileConvert(mainblock, 0, mExpression1, targetType, parenthesis); + return; + } + + if (targetType.accepts(sourceType) != CastType.EQUALS) { + if (parenthesis) writer.addCode("("); + writer.addCode("("); + mExpression2.writeJavaCode(mainblock, writer, false); + writer.addCode(") "); + } + mExpression1.writeJavaCode(mainblock, writer, true); + if (targetType.accepts(sourceType) != CastType.EQUALS) { + if (parenthesis) writer.addCode(")"); + } + } else { + mExpression1.writeJavaCode(mainblock, writer, parenthesis); } return; case Operators.IN: @@ -1106,7 +1119,13 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { // Type totalement compatible var cast = lt.getType().accepts(mExpression1.getType()); - if (cast == CastType.INCOMPATIBLE) { + // Allow explicit casts between enums and integers/reals even if the + // underlying types are different at runtime. + if (cast == CastType.INCOMPATIBLE + && !(lt.getType() == Type.INT && mExpression1.getType() instanceof leekscript.common.EnumType) + && !(lt.getType() == Type.REAL && mExpression1.getType() instanceof leekscript.common.EnumType) + && !(lt.getType() instanceof leekscript.common.EnumType && mExpression1.getType() == Type.INT) + && !(lt.getType() instanceof leekscript.common.EnumType && mExpression1.getType() == Type.REAL)) { compiler.addError(new AnalyzeError(getLocation(), AnalyzeErrorLevel.ERROR, Error.IMPOSSIBLE_CAST_VALUES, new String[] { mExpression1.toString(), mExpression1.getType().toString(), @@ -1118,13 +1137,20 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { // x == y : toujours faux si types incompatibles if ((compiler.getVersion() >= 4 && mOperator == Operators.EQUALS) || mOperator == Operators.EQUALS_EQUALS) { - if (mExpression1.getType().accepts(mExpression2.getType()) == CastType.INCOMPATIBLE) { + // Special-case enum vs integer/real comparisons + if ((mExpression1.getType() instanceof leekscript.common.EnumType && (mExpression2.getType() == Type.INT || mExpression2.getType() == Type.REAL)) + || (mExpression2.getType() instanceof leekscript.common.EnumType && (mExpression1.getType() == Type.INT || mExpression1.getType() == Type.REAL))) { + compiler.addError(new AnalyzeError(getLocation(), AnalyzeErrorLevel.WARNING, Error.COMPARISON_ALWAYS_FALSE, new String[] { mExpression1.getType().toString(), mExpression2.getType().toString() })); + } else if (mExpression1.getType().accepts(mExpression2.getType()) == CastType.INCOMPATIBLE) { compiler.addError(new AnalyzeError(getLocation(), AnalyzeErrorLevel.WARNING, Error.COMPARISON_ALWAYS_FALSE, new String[] { mExpression1.getType().toString(), mExpression2.getType().toString() })); } } // x != y : toujours vrai si types incompatibles if ((compiler.getVersion() >= 4 && mOperator == Operators.NOTEQUALS) || mOperator == Operators.NOT_EQUALS_EQUALS) { - if (mExpression1.getType().accepts(mExpression2.getType()) == CastType.INCOMPATIBLE) { + if ((mExpression1.getType() instanceof leekscript.common.EnumType && (mExpression2.getType() == Type.INT || mExpression2.getType() == Type.REAL)) + || (mExpression2.getType() instanceof leekscript.common.EnumType && (mExpression1.getType() == Type.INT || mExpression1.getType() == Type.REAL))) { + compiler.addError(new AnalyzeError(getLocation(), AnalyzeErrorLevel.WARNING, Error.COMPARISON_ALWAYS_TRUE, new String[] { mExpression1.getType().toString(), mExpression2.getType().toString() })); + } else if (mExpression1.getType().accepts(mExpression2.getType()) == CastType.INCOMPATIBLE) { compiler.addError(new AnalyzeError(getLocation(), AnalyzeErrorLevel.WARNING, Error.COMPARISON_ALWAYS_TRUE, new String[] { mExpression1.getType().toString(), mExpression2.getType().toString() })); } } diff --git a/src/main/java/leekscript/compiler/expression/LeekObjectAccess.java b/src/main/java/leekscript/compiler/expression/LeekObjectAccess.java index bb73509..add0c34 100644 --- a/src/main/java/leekscript/compiler/expression/LeekObjectAccess.java +++ b/src/main/java/leekscript/compiler/expression/LeekObjectAccess.java @@ -13,6 +13,7 @@ import leekscript.compiler.expression.LeekVariable.VariableType; import leekscript.common.ClassType; import leekscript.common.ClassValueType; +import leekscript.common.EnumValueType; import leekscript.common.Error; import leekscript.common.FunctionType; import leekscript.common.Type; @@ -104,7 +105,7 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { } if (object instanceof LeekVariable) { var v = (LeekVariable) object; - if (v.getVariableType() == VariableType.CLASS || v.getVariableType() == VariableType.THIS_CLASS) { + if (v.getVariableType() == VariableType.CLASS || v.getVariableType() == VariableType.ENUM || v.getVariableType() == VariableType.THIS_CLASS) { operations -= 1; } } @@ -132,6 +133,20 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { this.type = this.variable.getType(); } } + } else if (object.getType() instanceof EnumValueType evt) { + + var enumDecl = evt.getEnumDeclaration(); + this.variable = enumDecl != null ? enumDecl.getConstant(field.getWord()) : null; + if (this.variable != null) { + this.type = this.variable.getType(); + this.isFinal = this.variable.isFinal(); + this.isLeftValue = false; + } else { + compiler.addError(new AnalyzeError(field, AnalyzeErrorLevel.ERROR, Error.ENUM_MEMBER_DOES_NOT_EXIST, new String[] { + enumDecl == null ? "?" : enumDecl.getName(), + field.getWord() + })); + } } else if (object.getType() instanceof ClassValueType cvt) { var clazz = cvt.getClassDeclaration(); @@ -201,6 +216,9 @@ public void writeJavaCode(MainLeekBlock mainblock, JavaWriter writer, boolean pa writer.addCode(mainblock.getWordCompiler().getCurrentClassVariable() + ".getField(\"" + field.getWord() + "\")"); } else if (object instanceof LeekVariable && ((LeekVariable) object).getVariableType() == VariableType.THIS) { writer.addCode(field.getWord()); + } else if (object.getType() instanceof EnumValueType) { + object.writeJavaCode(mainblock, writer, false); + writer.addCode(".getField(\"" + field.getWord() + "\")"); } else if (object.getType() instanceof ClassType && !(type instanceof FunctionType)) { // TODO : mieux détecter les méthodes object.writeJavaCode(mainblock, writer, true); writer.addCode("."); diff --git a/src/main/java/leekscript/compiler/expression/LeekVariable.java b/src/main/java/leekscript/compiler/expression/LeekVariable.java index ecfdfd1..98b6933 100644 --- a/src/main/java/leekscript/compiler/expression/LeekVariable.java +++ b/src/main/java/leekscript/compiler/expression/LeekVariable.java @@ -11,6 +11,7 @@ import leekscript.compiler.bloc.MainLeekBlock; import leekscript.compiler.exceptions.LeekCompilerException; import leekscript.compiler.instruction.ClassDeclarationInstruction; +import leekscript.compiler.instruction.EnumDeclarationInstruction; import leekscript.compiler.instruction.LeekVariableDeclarationInstruction; import leekscript.runner.LeekConstants; import leekscript.runner.LeekFunctions; @@ -21,7 +22,7 @@ public class LeekVariable extends Expression { public static enum VariableType { - LOCAL, GLOBAL, ARGUMENT, FIELD, STATIC_FIELD, THIS, THIS_CLASS, CLASS, SUPER, METHOD, STATIC_METHOD, SYSTEM_CONSTANT, SYSTEM_FUNCTION, FUNCTION, ITERATOR + LOCAL, GLOBAL, ARGUMENT, FIELD, STATIC_FIELD, THIS, THIS_CLASS, CLASS, ENUM, SUPER, METHOD, STATIC_METHOD, SYSTEM_CONSTANT, SYSTEM_FUNCTION, FUNCTION, ITERATOR } private final Token token; @@ -29,6 +30,7 @@ public static enum VariableType { private Type variableType = Type.ANY; private LeekVariableDeclarationInstruction declaration = null; private ClassDeclarationInstruction classDeclaration = null; + private EnumDeclarationInstruction enumDeclaration = null; private FunctionBlock functionDeclaration = null; private boolean box = false; private boolean isFinal = false; @@ -81,6 +83,12 @@ public LeekVariable(Token token, VariableType type, Type variableType, ClassDecl this.variableType = variableType; } + public LeekVariable(Token token, VariableType type, Type variableType, EnumDeclarationInstruction enumDeclaration) { + this(token, type); + this.enumDeclaration = enumDeclaration; + this.variableType = variableType; + } + public LeekVariable(Token token, VariableType type, Type variableType, FunctionBlock functionDeclaration) { this(token, type); this.functionDeclaration = functionDeclaration; @@ -108,7 +116,7 @@ public boolean validExpression(WordCompiler compiler, MainLeekBlock mainblock) t } public boolean isLeftValue() { - if (type == VariableType.CLASS || type == VariableType.THIS || type == VariableType.THIS_CLASS || type == VariableType.SUPER || type == VariableType.SYSTEM_CONSTANT) { + if (type == VariableType.CLASS || type == VariableType.ENUM || type == VariableType.THIS || type == VariableType.THIS_CLASS || type == VariableType.SUPER || type == VariableType.SYSTEM_CONSTANT) { return false; } return true; @@ -144,6 +152,7 @@ public void preAnalyze(WordCompiler compiler) throws LeekCompilerException { this.variableType = v.getType(); this.declaration = v.getDeclaration(); this.classDeclaration = v.getClassDeclaration(); + this.enumDeclaration = v.getEnumDeclaration(); this.functionDeclaration = v.getFunctionDeclaration(); this.isFinal = v.isFinal(); this.box = v.box; @@ -214,6 +223,10 @@ public ClassDeclarationInstruction getClassDeclaration() { return classDeclaration; } + public EnumDeclarationInstruction getEnumDeclaration() { + return enumDeclaration; + } + public LeekVariableDeclarationInstruction getDeclaration() { return declaration; } @@ -307,6 +320,8 @@ else if (constant.getType() == Type.REAL) { } else { writer.addCode("u_" + token.getWord()); } + } else if (type == VariableType.ENUM) { + writer.addCode("u_" + token.getWord()); } else { if (isWrapper() || isBox()) { writer.addCode("u_" + token.getWord() + ".get()"); @@ -367,6 +382,8 @@ else if (constant.getType() == Type.REAL) { } else { writer.addCode("u_" + token.getWord()); } + } else if (type == VariableType.ENUM) { + writer.addCode("u_" + token.getWord()); } else { if (isWrapper()) { writer.addCode("u_" + token.getWord() + ".getVariable()"); diff --git a/src/main/java/leekscript/compiler/instruction/EnumDeclarationInstruction.java b/src/main/java/leekscript/compiler/instruction/EnumDeclarationInstruction.java new file mode 100644 index 0000000..e945046 --- /dev/null +++ b/src/main/java/leekscript/compiler/instruction/EnumDeclarationInstruction.java @@ -0,0 +1,170 @@ +package leekscript.compiler.instruction; + +import java.util.ArrayList; +import java.util.LinkedHashMap; + +import leekscript.compiler.AIFile; +import leekscript.compiler.JavaWriter; +import leekscript.compiler.Location; +import leekscript.compiler.Token; +import leekscript.compiler.WordCompiler; +import leekscript.compiler.bloc.MainLeekBlock; +import leekscript.compiler.exceptions.LeekCompilerException; +import leekscript.compiler.expression.Expression; +import leekscript.compiler.expression.LeekVariable; +import leekscript.compiler.expression.LeekVariable.VariableType; +import leekscript.common.EnumType; +import leekscript.common.EnumValueType; +import leekscript.common.Type; + +public class EnumDeclarationInstruction extends LeekInstruction { + + private final Token token; + private final MainLeekBlock mainBlock; + private final LinkedHashMap constants = new LinkedHashMap<>(); + private final LinkedHashMap values = new LinkedHashMap<>(); + private final ArrayList constantOrder = new ArrayList<>(); + public Type enumType; + public Type enumValueType; + + public EnumDeclarationInstruction(Token token, int line, AIFile ai, MainLeekBlock block) { + this.token = token; + this.mainBlock = block; + this.enumType = new EnumType(this); + this.enumValueType = new EnumValueType(this); + } + + public String getName() { + return token.getWord(); + } + + public void addConstant(Token nameToken) { + addConstant(nameToken, null); + } + + public void addConstant(Token nameToken, Expression value) { + String name = nameToken.getWord(); + // Duplicate constants are reported at parse time in WordCompiler.enumDeclaration + if (constants.containsKey(name)) return; + constantOrder.add(name); + constants.put(name, new LeekVariable(nameToken, VariableType.STATIC_FIELD, enumType, true)); + values.put(name, value); + } + + public LinkedHashMap getConstants() { + return constants; + } + + public Expression getValue(String name) { + return values.get(name); + } + + public ArrayList getConstantOrder() { + return constantOrder; + } + + public LeekVariable getConstant(String name) { + return constants.get(name); + } + + @Override + public String getCode() { + String r = "enum " + token.getWord() + " {\n"; + for (String name : constantOrder) { + r += "\t" + name; + var value = values.get(name); + if (value != null) { + r += " = " + value.toString(); + } + r += ",\n"; + } + r += "}"; + return r; + } + + @Override + public int getEndBlock() { + return 0; + } + + @Override + public boolean putCounterBefore() { + return false; + } + + public void declare(WordCompiler compiler) { + compiler.getCurrentBlock().addVariable(new LeekVariable(token, VariableType.ENUM, this.enumValueType, this)); + } + + public void preAnalyze(WordCompiler compiler) throws LeekCompilerException { + // Pre-analyze enum constant value expressions, if any + for (var entry : values.entrySet()) { + var expr = entry.getValue(); + if (expr != null) { + expr.preAnalyze(compiler); + } + } + } + + public void analyze(WordCompiler compiler) throws LeekCompilerException { + // Analyze enum constant value expressions + for (var entry : values.entrySet()) { + var expr = entry.getValue(); + if (expr != null) { + expr.analyze(compiler); + } + } + } + + @Override + public Location getLocation() { + return token.getLocation(); + } + + @Override + public int getNature() { + return 0; + } + + @Override + public Type getType() { + return this.enumType; + } + + @Override + public String toString() { + return getCode(); + } + + @Override + public boolean validExpression(WordCompiler compiler, MainLeekBlock mainblock) { + return false; + } + + public Type getEnumValueType() { + return enumValueType; + } + + public void declareJava(MainLeekBlock mainblock, JavaWriter writer) { + String enumName = "u_" + token.getWord(); + writer.addLine("public EnumLeekValue " + enumName + " = new EnumLeekValue(this, \"" + token.getWord() + "\");"); + } + + public void createJava(MainLeekBlock mainblock, JavaWriter writer) { + String enumName = "u_" + token.getWord(); + for (String name : constantOrder) { + var value = values.get(name); + writer.addCode(enumName + ".addConstant(\"" + name + "\", "); + if (value != null) { + value.writeJavaCode(mainblock, writer, false); + } else { + writer.addCode("\"" + name + "\""); + } + writer.addLine(");"); + } + } + + @Override + public void writeJavaCode(MainLeekBlock mainblock, JavaWriter writer, boolean parenthesis) { + } +} diff --git a/src/main/java/leekscript/runner/AI.java b/src/main/java/leekscript/runner/AI.java index 319099b..4c9b438 100644 --- a/src/main/java/leekscript/runner/AI.java +++ b/src/main/java/leekscript/runner/AI.java @@ -9,6 +9,7 @@ import leekscript.runner.classes.StandardClass; import leekscript.runner.values.ArrayLeekValue; import leekscript.runner.values.ClassLeekValue; +import leekscript.runner.values.EnumLeekValue; import leekscript.runner.values.FunctionLeekValue; import leekscript.runner.values.GenericArrayLeekValue; import leekscript.runner.values.GenericMapLeekValue; @@ -1046,6 +1047,8 @@ public long longint(Object value) throws LeekRunException { return (long) (double) value; } else if (value instanceof Long) { return (Long) value; + } else if (value instanceof EnumLeekValue.EnumConstant c) { + return c.value; } else if (value instanceof Boolean) { return ((Boolean) value) ? 1 : 0; } else if (value instanceof ObjectLeekValue) { @@ -1088,6 +1091,8 @@ public double real(Object value) throws LeekRunException { return (Double) value; } else if (value instanceof Long) { return (Long) value; + } else if (value instanceof EnumLeekValue.EnumConstant c) { + return (double) c.value; } else if (value instanceof Boolean) { return ((Boolean) value) ? 1 : 0; } else if (value instanceof ObjectLeekValue) { @@ -1551,6 +1556,8 @@ public String export(Object value) throws LeekRunException { return set.string(this, new HashSet()); } else if (value instanceof IntervalLeekValue interval) { return interval.string(this, new HashSet()); + } else if (value instanceof EnumLeekValue.EnumConstant c) { + return "\"" + c.name + "\""; } else if (value instanceof String) { return "\"" + value + "\""; } else if (value instanceof ClassLeekValue) { @@ -1587,6 +1594,8 @@ public String export(Object value, Set visited) throws LeekRunException return ((SetLeekValue) value).string(this, visited); } else if (value instanceof IntervalLeekValue) { return ((IntervalLeekValue) value).string(this, visited); + } else if (value instanceof EnumLeekValue.EnumConstant c) { + return "\"" + c.name + "\""; } else if (value instanceof String) { return "\"" + value + "\""; } else if (value instanceof ClassLeekValue) { @@ -1665,6 +1674,9 @@ public Object getField(Object value, String field, ClassLeekValue fromClass) thr if (value instanceof ClassLeekValue) { return ((ClassLeekValue) value).getField(field, fromClass); } + if (value instanceof EnumLeekValue) { + return ((EnumLeekValue) value).getField(field); + } if (value instanceof NativeObjectLeekValue object) { try { var f = getFieldCached(value.getClass(), field); @@ -3209,6 +3221,13 @@ public ClassLeekValue classOf(Object value) { public boolean instanceOf(Object value, Object clazz) throws LeekRunException { ops(2); + if (clazz instanceof EnumLeekValue enumType) { + var v = load(value); + if (v instanceof EnumLeekValue.EnumConstant c) { + return c.enumType == enumType; + } + return false; + } if (!(clazz instanceof ClassLeekValue)) { addSystemLog(AILog.ERROR, Error.INSTANCEOF_MUST_BE_CLASS); return false; diff --git a/src/main/java/leekscript/runner/LeekFunctions.java b/src/main/java/leekscript/runner/LeekFunctions.java index 14a7e58..a6a2d82 100644 --- a/src/main/java/leekscript/runner/LeekFunctions.java +++ b/src/main/java/leekscript/runner/LeekFunctions.java @@ -17,7 +17,7 @@ public class LeekFunctions { */ method("string", "Value", 8, true, Type.STRING, new Type[] { Type.ANY }); method("number", "Value", 10, true, Type.INT_OR_REAL, new Type[] { Type.ANY }); - method("typeOf", "Value", 8, true, Type.INT, new Type[] { Type.ANY }); + method("typeOf", "Value", 8, true, Type.ANY, new Type[] { Type.ANY }); method("clone", "Value", true, new CallableVersion[] { new CallableVersion(Type.ANY, new Type[] { Type.ANY, Type.INT }), new CallableVersion(Type.ANY, new Type[] { Type.ANY }), diff --git a/src/main/java/leekscript/runner/classes/ValueClass.java b/src/main/java/leekscript/runner/classes/ValueClass.java index 14934d9..1e69b60 100644 --- a/src/main/java/leekscript/runner/classes/ValueClass.java +++ b/src/main/java/leekscript/runner/classes/ValueClass.java @@ -4,6 +4,7 @@ import leekscript.runner.LeekOperations; import leekscript.runner.LeekRunException; import leekscript.runner.LeekValueManager; +import leekscript.runner.values.EnumLeekValue; public class ValueClass { @@ -12,6 +13,10 @@ public static Object unknown(AI ai, Object value) { } public static String string(AI ai, Object value) throws LeekRunException { + if (value instanceof EnumLeekValue.EnumConstant c) { + // Enum constants stringify to their name (without quotes) + return c.name; + } if (value instanceof String) { return (String) value; } @@ -22,6 +27,9 @@ public static String string(AI ai, Object value) throws LeekRunException { } public static Number number(AI ai, Object value) { + if (value instanceof EnumLeekValue.EnumConstant c) { + return c.value; + } if (value instanceof Number) return (Number) value; if (value instanceof String) { @@ -37,7 +45,10 @@ public static Number number(AI ai, Object value) { return 0l; } - public static long typeOf(AI ai, Object value) throws LeekRunException { + public static Object typeOf(AI ai, Object value) throws LeekRunException { + if (value instanceof EnumLeekValue.EnumConstant c) { + return c.enumType.name; + } return (long) LeekValueManager.getType(value); } diff --git a/src/main/java/leekscript/runner/values/EnumLeekValue.java b/src/main/java/leekscript/runner/values/EnumLeekValue.java new file mode 100644 index 0000000..56d1882 --- /dev/null +++ b/src/main/java/leekscript/runner/values/EnumLeekValue.java @@ -0,0 +1,47 @@ +package leekscript.runner.values; + +import java.util.HashMap; +import java.util.Map; + +import leekscript.runner.AI; +import leekscript.runner.LeekRunException; + +public class EnumLeekValue { + + public final AI ai; + public final String name; + private final Map constants = new HashMap<>(); + private long nextAutoValue = 0; + + public EnumLeekValue(AI ai, String name) { + this.ai = ai; + this.name = name; + } + + public void addConstant(String name, Object value) { + long numericValue; + if (value instanceof Number n) { + numericValue = n.longValue(); + nextAutoValue = numericValue + 1; + } else { + numericValue = nextAutoValue++; + } + constants.put(name, new EnumConstant(this, name, numericValue)); + } + + public Object getField(String field) throws LeekRunException { + return constants.get(field); + } + + public static class EnumConstant { + public final EnumLeekValue enumType; + public final String name; + public final long value; + + public EnumConstant(EnumLeekValue enumType, String name, long value) { + this.enumType = enumType; + this.name = name; + this.value = value; + } + } +} diff --git a/src/test/java/test/TestEnum.java b/src/test/java/test/TestEnum.java new file mode 100644 index 0000000..8563422 --- /dev/null +++ b/src/test/java/test/TestEnum.java @@ -0,0 +1,90 @@ +package test; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.Test; + +import leekscript.common.Error; + +@ExtendWith(SummaryExtension.class) +public class TestEnum extends TestCommon { + + @Test + public void run() throws Exception { + + section("Enum numeric defaults"); + code_v4_("enum Color { RED, GREEN, BLUE } integer r = Color.RED as integer, g = Color.GREEN as integer, b = Color.BLUE as integer return [r, g, b]").equals("[0, 1, 2]"); + + section("Enum with explicit values"); + code_v4_("enum Color { RED = 1, GREEN = 2, BLUE = 3 } return Color.RED as integer").equals("1"); + code_v4_("enum Color { RED = 1, GREEN = 2, BLUE = 3 } return Color.GREEN as integer").equals("2"); + code_v4_("enum Color { RED = 1, GREEN = 2, BLUE = 3 } return Color.BLUE as integer").equals("3"); + + section("Enum mixed default and explicit values"); + code_v4_("enum Mixed { A, B = 2, C } return Mixed.A as integer").equals("0"); + code_v4_("enum Mixed { A, B = 2, C } return Mixed.B as integer").equals("2"); + code_v4_("enum Mixed { A, B = 2, C } return Mixed.C as integer").equals("3"); + + section("Enum with non-integer values"); + code_v4_("enum Status { OK = 200, FAIL = 500 } return Status.OK as integer").equals("200"); + code_v4_("enum Status { OK = 200, FAIL = 500 } return Status.FAIL as integer").equals("500"); + + section("Enum in variable"); + code_v4_("enum Direction { UP, DOWN, LEFT, RIGHT } integer d = Direction.UP as integer return d").equals("0"); + code_v4_("enum Direction { UP, DOWN } integer d = Direction.DOWN as integer return d").equals("1"); + + section("Enum type in type annotation"); + code_v4_("enum State { ON, OFF } State x = 0 as State integer i = x as integer return i").equals("0"); + + section("Enum equality and switch"); + code_v4_("enum Color { RED, GREEN } var c = Color.RED return c == Color.RED").equals("true"); + code_v4_("enum Color { RED, GREEN } var c = Color.GREEN return c == Color.RED").equals("false"); + code_v4_("enum Color { RED = 1, GREEN = 2, BLUE = 3 } var c = Color.GREEN switch (c) { case Color.RED: return 1 case Color.GREEN: return 2 default: return 3 }").equals("2"); + + section("Enum comparison warnings"); + code_v4_("enum Color { RED, GREEN } return Color.RED == 0").warning(Error.COMPARISON_ALWAYS_FALSE); + code_v4_("enum Color { RED, GREEN } return Color.RED != 0").warning(Error.COMPARISON_ALWAYS_TRUE); + + section("Enum incomplete switch (no default)"); + code_v4_("enum Color { RED, GREEN, BLUE } var c = Color.RED switch (c) { case Color.RED: return 1 case Color.GREEN: return 2 } return 3").equals("1"); + code_v4_("enum Color { RED, GREEN, BLUE } var c = Color.BLUE switch (c) { case Color.RED: return 1 case Color.GREEN: return 2 } return 3").equals("3"); + + section("Enum switch warnings"); + code_v4_("enum Color { RED, GREEN, BLUE } var c = Color.RED switch (c) { case Color.RED: return 1 case Color.GREEN: return 2 } return 3").warning(Error.INCOMPLETE_ENUM_SWITCH); + code_v4_("enum Color { RED, GREEN, BLUE } var c = Color.GREEN switch (c) { case Color.RED: return 1 case Color.GREEN: return 2 } return 3").warning(Error.INCOMPLETE_ENUM_SWITCH); + + section("Enum in arithmetic and typeOf"); + code_v4_("enum Num { ONE = 1 } return Num.ONE + 1").equals("2"); + code_v4_("enum Num { ONE = 1 } return typeOf(Num.ONE)").equals("\"Num\""); + + section("Enum string()"); + code_v4_("enum Color { RED, GREEN } return string(Color.RED)").equals("\"RED\""); + code_v4_("enum Status { OK = 200, FAIL = 500 } return string(Status.FAIL)").equals("\"FAIL\""); + + section("Enum instanceof"); + code_v4_("enum Color { RED, GREEN } var c = Color.RED return c instanceof Color").equals("true"); + code_v4_("enum Color { RED, GREEN } var i = 0 return i instanceof Color").equals("false"); + + section("Enum and functions"); + code_v4_("enum State { ON, OFF } function isOn(State s) { return s == State.ON } return isOn(State.ON)").equals("true"); + code_v4_("enum State { ON, OFF } function isOn(State s) { return s == State.ON } return isOn(State.OFF)").equals("false"); + + section("Enum cast round-trip"); + code_v4_("enum State { ON, OFF } return (0 as State) as integer").equals("0"); + code_v4_("enum Mixed { A, B = 2, C } Mixed x = Mixed.C integer i = x as integer return i").equals("3"); + + section("Enum typeOf details"); + code_v4_("enum State { ON, OFF } return typeOf(State.ON)").equals("\"State\""); + + section("Enum string() and values"); + code_v4_("enum Color { RED = 1, GREEN = 2 } return string(Color.GREEN)").equals("\"GREEN\""); + + section("Enum invalid member access"); + code_v4_("enum Color { RED, GREEN } return Color.BLUE").error(Error.ENUM_MEMBER_DOES_NOT_EXIST); + + section("Enum duplicated constants"); + code_v4_("enum Color { RED, RED } return 0").error(Error.DUPLICATED_ENUM_CONSTANT); + + section("Enum incompatible assignment between different enums"); + code_v4_("enum A { X } enum B { Y } A a = B.Y return 0").error(Error.ASSIGNMENT_INCOMPATIBLE_TYPE); + } +}