diff --git a/pom.xml b/pom.xml index 71290718c..046d939a1 100644 --- a/pom.xml +++ b/pom.xml @@ -1166,6 +1166,23 @@ false + + llvm-integration-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + true + + + + + + diff --git a/src/main/java/com/laytonsmith/core/asm/AsmCommonLibTemplates.java b/src/main/java/com/laytonsmith/core/asm/AsmCommonLibTemplates.java index 443a6ea01..62af288d1 100644 --- a/src/main/java/com/laytonsmith/core/asm/AsmCommonLibTemplates.java +++ b/src/main/java/com/laytonsmith/core/asm/AsmCommonLibTemplates.java @@ -63,6 +63,20 @@ private static void register(String code, Environment env) { register("declare dso_local i32 @time(...)", env); }; + /** + * C Standard + */ + public static final Generator STRCMP = (env) -> { + register("declare dso_local i32 @strcmp(i8*, i8*)", env); + }; + + /** + * C Standard + */ + public static final Generator STRLEN = (env) -> { + register("declare dso_local i64 @strlen(i8*)", env); + }; + public static final Generator LLVM_MEMCPY_P0I8_P0I8_I64 = (env) -> { register("declare void @llvm.memcpy.p0i8.p0i8.i64(i8* noalias nocapture writeonly, i8* noalias nocapture readonly, i64, i1 immarg) nounwind", env); }; diff --git a/src/main/java/com/laytonsmith/core/asm/AsmCompiler.java b/src/main/java/com/laytonsmith/core/asm/AsmCompiler.java index 891f8be1e..bfa0a2f94 100644 --- a/src/main/java/com/laytonsmith/core/asm/AsmCompiler.java +++ b/src/main/java/com/laytonsmith/core/asm/AsmCompiler.java @@ -228,7 +228,6 @@ public void compileEntryPoint(File file, File outputDirectory, String exeName) } StringBuilder program = new StringBuilder(); llvmenv.newMethodFrame("@main"); - llvmenv.pushVariableScope(); // TODO: Eventually add the argument processing for this // Pop 0..2 for the main args and entry label llvmenv.getNewLocalVariableReference(IRType.INTEGER32); diff --git a/src/main/java/com/laytonsmith/core/asm/AsmMain.java b/src/main/java/com/laytonsmith/core/asm/AsmMain.java index fab9bacbe..3dc32e360 100644 --- a/src/main/java/com/laytonsmith/core/asm/AsmMain.java +++ b/src/main/java/com/laytonsmith/core/asm/AsmMain.java @@ -19,14 +19,28 @@ */ public class AsmMain { - private static void LogErrorAndQuit(String error, int code) { - StreamUtils.GetSystemErr().println(TermColors.RED + error + TermColors.RESET); - System.exit(code); - } - @tool(value = "asm", undocumented = true) public static class AsmMainCmdlineTool extends AbstractCommandLineTool { + private boolean throwOnError = false; + + /** + * When set to true, errors that would normally call System.exit will instead throw a + * RuntimeException. This is intended for use in test environments where System.exit + * would kill the test runner. + */ + public void setThrowOnError(boolean value) { + this.throwOnError = value; + } + + private void logErrorAndQuit(String error, int code) { + StreamUtils.GetSystemErr().println(TermColors.RED + error + TermColors.RESET); + if(throwOnError) { + throw new RuntimeException("ASM compilation failed (exit code " + code + "): " + error); + } + System.exit(code); + } + @Override public ArgumentParser getArgumentParser() { // Normally we would keep this builder in here, but there are so many options that it makes @@ -41,6 +55,9 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce new AsmInstaller().install(parsedArgs.isFlagSet("install-toolchain-non-interactive")); } catch (IOException | InterruptedException e) { StreamUtils.GetSystemErr().println(e.getMessage()); + if(throwOnError) { + throw new RuntimeException("Toolchain installation failed", e); + } System.exit(1); } return; @@ -59,15 +76,15 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce try { try { if(input.isDirectory()) { - LogErrorAndQuit("Currently only single files are supported.", 1); + logErrorAndQuit("Currently only single files are supported.", 1); } else { AsmCompiler compiler = new AsmCompiler(parsedArgs); try { compiler.compileEntryPoint(input, output, exeName); } catch (IOException ex) { - LogErrorAndQuit(ex.getMessage(), 1); + logErrorAndQuit(ex.getMessage(), 1); } catch (InternalException ex) { - LogErrorAndQuit(ex.getMessage() + "\nAn internal exception occurred, which is not caused by your code. " + logErrorAndQuit(ex.getMessage() + "\nAn internal exception occurred, which is not caused by your code. " + (parsedArgs.isFlagSet("verbose") ? "Please report this with all the above information." : "Please re-run with the --verbose switch."), 2); diff --git a/src/main/java/com/laytonsmith/core/asm/AsmUtil.java b/src/main/java/com/laytonsmith/core/asm/AsmUtil.java index 5a7fc6b46..808d13d42 100644 --- a/src/main/java/com/laytonsmith/core/asm/AsmUtil.java +++ b/src/main/java/com/laytonsmith/core/asm/AsmUtil.java @@ -68,7 +68,11 @@ public static String formatLine(Target t, LLVMEnvironment llvmenv, String line, if(s.length() < commentTab) { s += StringUtils.stringMultiply(commentTab - s.length(), " "); } - s += " ; " + t.toString(); + if(t == Target.UNKNOWN) { + s += " ; <>"; + } else { + s += " ; " + t.toString(); + } } s += OSUtils.GetLineEnding(); return s; diff --git a/src/main/java/com/laytonsmith/core/asm/IRBuilder.java b/src/main/java/com/laytonsmith/core/asm/IRBuilder.java index 506932539..ca8331e62 100644 --- a/src/main/java/com/laytonsmith/core/asm/IRBuilder.java +++ b/src/main/java/com/laytonsmith/core/asm/IRBuilder.java @@ -25,10 +25,48 @@ public IRBuilder() { } + private int fillTarget = -1; + public void appendLine(Target t, String line) { // This is a great place to put a breakpoint if you aren't sure where a line of IR is coming from. - lines.add(line); + if(fillTarget >= 0) { + lines.set(fillTarget, line); + fillTarget = -1; + } else { + lines.add(line); + targets.add(t); + } + } + + /** + * Reserves a line slot at the current position in the IR output. The slot contains a null + * placeholder and must be filled later via {@link #fillReservedLine(int, Runnable)}. This is + * used when a line (such as a conditional branch) cannot be fully constructed until blocks + * that follow it have been generated (and their label numbers are known). + * @param t The target for source location tracking. + * @return The index of the reserved slot, for use with {@link #fillReservedLine(int, Runnable)}. + */ + public int reserveLine(Target t) { + int index = lines.size(); + lines.add(null); targets.add(t); + return index; + } + + /** + * Fills a previously reserved line slot by executing the given action. The action should + * call exactly one Gen method (e.g. {@code gen.br} or {@code gen.brCond}), which will + * write its output into the reserved slot instead of appending to the end. + * @param index The index returned by {@link #reserveLine(Target)}. + * @param action A runnable that calls a Gen method to produce the line. + */ + public void fillReservedLine(int index, Runnable action) { + fillTarget = index; + action.run(); + if(fillTarget >= 0) { + throw new IllegalStateException( + "fillReservedLine action did not produce any output"); + } } public void appendLines(Target t, String... lines) { @@ -37,6 +75,15 @@ public void appendLines(Target t, String... lines) { } } + /** + * Appends a label line (e.g. "42:") for the given label number. + * @param t The target for source location tracking. + * @param label The label number (as returned by {@link LLVMEnvironment#getGotoLabel()}). + */ + public void appendLabel(Target t, int label) { + appendLine(t, label + ":"); + } + public void appendLines(Target t, List lines) { for(String line : lines) { appendLine(t, line); @@ -87,6 +134,86 @@ public Gen generator(Target t, Environment env) { return this.new Gen(t, env); } + /** + * Integer comparison predicates for the icmp instruction. + */ + public enum ICmpPredicate { + EQ("eq"), + NE("ne"), + /** Unsigned greater than */ + UGT("ugt"), + /** Unsigned greater or equal */ + UGE("uge"), + /** Unsigned less than */ + ULT("ult"), + /** Unsigned less or equal */ + ULE("ule"), + /** Signed greater than */ + SGT("sgt"), + /** Signed greater or equal */ + SGE("sge"), + /** Signed less than */ + SLT("slt"), + /** Signed less or equal */ + SLE("sle"); + + private final String ir; + + ICmpPredicate(String ir) { + this.ir = ir; + } + + @Override + public String toString() { + return ir; + } + } + + /** + * Floating point comparison predicates for the fcmp instruction. + */ + public enum FCmpPredicate { + /** Ordered and equal */ + OEQ("oeq"), + /** Ordered and greater than */ + OGT("ogt"), + /** Ordered and greater or equal */ + OGE("oge"), + /** Ordered and less than */ + OLT("olt"), + /** Ordered and less or equal */ + OLE("ole"), + /** Ordered and not equal */ + ONE("one"), + /** Ordered (no NaNs) */ + ORD("ord"), + /** Unordered (either NaN) */ + UNO("uno"), + /** Unordered or equal */ + UEQ("ueq"), + /** Unordered or greater than */ + UGT("ugt"), + /** Unordered or greater or equal */ + UGE("uge"), + /** Unordered or less than */ + ULT("ult"), + /** Unordered or less or equal */ + ULE("ule"), + /** Unordered or not equal */ + UNE("une"); + + private final String ir; + + FCmpPredicate(String ir) { + this.ir = ir; + } + + @Override + public String toString() { + return ir; + } + } + public class Gen { Target t; @@ -193,5 +320,251 @@ public int allocaStoreAndLoad(int allocaId, IRType type, String storeValue, int return loadId; } + // --- Comparison instructions --- + + /** + * Integer comparison of two registers. + * @return The result variable (always i1). + */ + public int icmp(ICmpPredicate predicate, IRType operandType, int lhs, int rhs) { + int result = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + IRBuilder.this.appendLine(t, "%" + result + " = icmp " + predicate + " " + + operandType.getIRType() + " %" + lhs + ", %" + rhs); + return result; + } + + /** + * Integer comparison of a register against a constant. + * @return The result variable (always i1). + */ + public int icmp(ICmpPredicate predicate, IRType operandType, int lhs, String rhsConst) { + int result = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + IRBuilder.this.appendLine(t, "%" + result + " = icmp " + predicate + " " + + operandType.getIRType() + " %" + lhs + ", " + rhsConst); + return result; + } + + /** + * Floating point comparison of two registers. + * @return The result variable (always i1). + */ + public int fcmp(FCmpPredicate predicate, IRType operandType, int lhs, int rhs) { + int result = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + IRBuilder.this.appendLine(t, "%" + result + " = fcmp " + predicate + " " + + operandType.getIRType() + " %" + lhs + ", %" + rhs); + return result; + } + + /** + * Floating point comparison of a register against a constant. + * @return The result variable (always i1). + */ + public int fcmp(FCmpPredicate predicate, IRType operandType, int lhs, String rhsConst) { + int result = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + IRBuilder.this.appendLine(t, "%" + result + " = fcmp " + predicate + " " + + operandType.getIRType() + " %" + lhs + ", " + rhsConst); + return result; + } + + // --- Logical instructions --- + + /** + * Bitwise AND of two registers. + * @return The result variable. + */ + public int and(IRType type, int lhs, int rhs) { + int result = llvmenv.getNewLocalVariableReference(type); + IRBuilder.this.appendLine(t, "%" + result + " = and " + type.getIRType() + + " %" + lhs + ", %" + rhs); + return result; + } + + /** + * Bitwise OR of two registers. + * @return The result variable. + */ + public int or(IRType type, int lhs, int rhs) { + int result = llvmenv.getNewLocalVariableReference(type); + IRBuilder.this.appendLine(t, "%" + result + " = or " + type.getIRType() + + " %" + lhs + ", %" + rhs); + return result; + } + + // --- Select instruction --- + + /** + * Select between two register values based on an i1 condition. + * @return The result variable. + */ + public int select(int condition, IRType valueType, int trueVal, int falseVal) { + int result = llvmenv.getNewLocalVariableReference(valueType); + IRBuilder.this.appendLine(t, "%" + result + " = select i1 %" + condition + + ", " + valueType.getIRType() + " %" + trueVal + + ", " + valueType.getIRType() + " %" + falseVal); + return result; + } + + /** + * Select between two arbitrary value references based on an i1 condition. + * The references are inserted as-is after the type prefix. + * @return The result variable. + */ + public int select(int condition, IRType valueType, String trueRef, String falseRef) { + int result = llvmenv.getNewLocalVariableReference(valueType); + IRBuilder.this.appendLine(t, "%" + result + " = select i1 %" + condition + + ", " + valueType.getIRType() + " " + trueRef + + ", " + valueType.getIRType() + " " + falseRef); + return result; + } + + // --- Cast instructions --- + + // --- Branch instructions --- + + /** + * Unconditional branch to a label. + * @param label The label to branch to. + */ + public void br(int label) { + IRBuilder.this.appendLine(t, "br label %" + label); + } + + /** + * Conditional branch based on an i1 condition. + * @param condition The i1 condition variable. + * @param trueLabel The label to branch to if true. + * @param falseLabel The label to branch to if false. + */ + public void brCond(int condition, int trueLabel, int falseLabel) { + IRBuilder.this.appendLine(t, "br i1 %" + condition + + ", label %" + trueLabel + ", label %" + falseLabel); + } + + // --- Cast instructions --- + + /** + * Bitcast between types of the same bit width. + * @return The result variable. + */ + public int bitcast(IRType fromType, int value, IRType toType) { + int result = llvmenv.getNewLocalVariableReference(toType); + IRBuilder.this.appendLine(t, "%" + result + " = bitcast " + + fromType.getIRType() + " %" + value + " to " + toType.getIRType()); + return result; + } + + /** + * Signed integer to floating point conversion. + * @return The result variable. + */ + public int sitofp(IRType fromType, int value, IRType toType) { + int result = llvmenv.getNewLocalVariableReference(toType); + IRBuilder.this.appendLine(t, "%" + result + " = sitofp " + + fromType.getIRType() + " %" + value + " to " + toType.getIRType()); + return result; + } + + /** + * Floating point extension to a wider float type. + * @return The result variable. + */ + public int fpext(IRType fromType, int value, IRType toType) { + int result = llvmenv.getNewLocalVariableReference(toType); + IRBuilder.this.appendLine(t, "%" + result + " = fpext " + + fromType.getIRType() + " %" + value + " to " + toType.getIRType()); + return result; + } + + /** + * Integer to pointer conversion. + * @return The result variable. + */ + public int inttoptr(IRType fromType, int value, IRType toType) { + int result = llvmenv.getNewLocalVariableReference(toType); + IRBuilder.this.appendLine(t, "%" + result + " = inttoptr " + + fromType.getIRType() + " %" + value + " to " + toType.getIRType()); + return result; + } + + // --- Aggregate instructions --- + + /** + * Extract a field from an aggregate (struct) value. + * @return The result variable. + */ + public int extractvalue(IRType structType, int structVar, int index, IRType resultType) { + int result = llvmenv.getNewLocalVariableReference(resultType); + IRBuilder.this.appendLine(t, "%" + result + " = extractvalue " + + structType.getIRType() + " %" + structVar + ", " + index); + return result; + } + + // --- ms_value tag category checks --- + + /** + * Checks whether an ms_value tag variable belongs to the given category. + * Emits one icmp per boxable IRType in that category, ORed together. + * @param category The category to check for. + * @param tagVar The i8 tag variable to test. + * @return An i1 result variable that is true if the tag matches. + */ + private int isTagCategory(IRType.Category category, int tagVar) { + int result = -1; + for(IRType type : IRType.values()) { + if(type.isBoxable() && type.getCategory() == category) { + int cmp = icmp(ICmpPredicate.EQ, IRType.INTEGER8, + tagVar, type.getBoxTagString()); + if(result == -1) { + result = cmp; + } else { + result = or(IRType.INTEGER1, result, cmp); + } + } + } + if(result == -1) { + throw new IllegalArgumentException( + "No boxable types found for category: " + category); + } + return result; + } + + /** + * Checks whether an ms_value tag represents a float type. + * @param tagVar The i8 tag variable to test. + * @return An i1 result variable. + */ + public int isFloatTag(int tagVar) { + return isTagCategory(IRType.Category.FLOAT, tagVar); + } + + /** + * Checks whether an ms_value tag represents a string type. + * @param tagVar The i8 tag variable to test. + * @return An i1 result variable. + */ + public int isStringTag(int tagVar) { + return isTagCategory(IRType.Category.POINTER, tagVar); + } + + /** + * Checks whether an ms_value tag represents a boolean (i1) type. + * @param tagVar The i8 tag variable to test. + * @return An i1 result variable. + */ + public int isBoolTag(int tagVar) { + return icmp(ICmpPredicate.EQ, IRType.INTEGER8, + tagVar, IRType.INTEGER1.getBoxTagString()); + } + + /** + * Checks whether an ms_value tag represents null. + * @param tagVar The i8 tag variable to test. + * @return An i1 result variable. + */ + public int isNullTag(int tagVar) { + return icmp(ICmpPredicate.EQ, IRType.INTEGER8, + tagVar, IRType.MS_NULL.getBoxTagString()); + } + } } diff --git a/src/main/java/com/laytonsmith/core/asm/IRCoercion.java b/src/main/java/com/laytonsmith/core/asm/IRCoercion.java new file mode 100644 index 000000000..c47540d04 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/asm/IRCoercion.java @@ -0,0 +1,143 @@ +package com.laytonsmith.core.asm; + +import com.laytonsmith.core.asm.IRBuilder.FCmpPredicate; +import com.laytonsmith.core.asm.IRBuilder.ICmpPredicate; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; + +/** + * Shared helpers for coercing IR values between types, used by multiple LLVM function implementations. + */ +public final class IRCoercion { + + private IRCoercion() { + } + + /** + * Coerces any IR value to i1 (boolean truthiness). Dispatches to the compile-time path + * for known types, or the runtime path for ms_value. + */ + public static int toBool(IRBuilder builder, Target t, Environment env, + IRData data) { + if(data.getResultType() == IRType.MS_VALUE) { + IRBuilder.Gen gen = builder.generator(t, env); + int tag = gen.extractvalue(IRType.MS_VALUE, data.getResultVariable(), + 0, IRType.INTEGER8); + int payload = gen.extractvalue(IRType.MS_VALUE, data.getResultVariable(), + 1, IRType.INTEGER64); + return emitCoerceToBoolRuntime(builder, t, env, tag, payload); + } + return coerceToBoolean(builder, t, env, data); + } + + /** + * Coerces a compile-time known value to i1 (boolean truthiness). + * Handles INTEGER1 (passthrough), STRING (strlen != 0), other integers (ne 0), and floats (une 0.0). + */ + public static int coerceToBoolean(IRBuilder builder, Target t, Environment env, + IRData data) { + IRBuilder.Gen gen = builder.generator(t, env); + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + IRType type = data.getResultType(); + if(type == IRType.INTEGER1) { + return data.getResultVariable(); + } else if(type == IRType.STRING) { + llvmenv.addGlobalDeclaration(AsmCommonLibTemplates.STRLEN, env); + int len = llvmenv.getNewLocalVariableReference(IRType.INTEGER64); + builder.appendLine(t, "%" + len + " = call i64 @strlen(" + + data.getReference() + ")"); + return gen.icmp(ICmpPredicate.NE, IRType.INTEGER64, len, "0"); + } else if(type.getCategory() == IRType.Category.INTEGER) { + return gen.icmp(ICmpPredicate.NE, type, data.getResultVariable(), "0"); + } else if(type.getCategory() == IRType.Category.FLOAT) { + return gen.fcmp(FCmpPredicate.UNE, type, data.getResultVariable(), "0.0"); + } + throw new UnsupportedOperationException( + "Cannot coerce " + type + " to boolean"); + } + + /** + * Coerces a compile-time known value to double. + * Handles DOUBLE (passthrough), other floats (fpext), and integers (sitofp). + */ + public static int coerceToDouble(IRBuilder builder, Target t, Environment env, + IRData data) { + IRBuilder.Gen gen = builder.generator(t, env); + IRType type = data.getResultType(); + if(type == IRType.DOUBLE) { + return data.getResultVariable(); + } else if(type.getCategory() == IRType.Category.FLOAT) { + return gen.fpext(type, data.getResultVariable(), IRType.DOUBLE); + } else if(type.getCategory() == IRType.Category.INTEGER) { + return gen.sitofp(type, data.getResultVariable(), IRType.DOUBLE); + } + throw new UnsupportedOperationException( + "Cannot coerce " + type + " to double"); + } + + /** + * Emits coercion of an ms_value payload to i1 (boolean truthiness) at runtime. + * For string types: strlen != 0 (requires a branch since strlen dereferences a pointer). + * For float types: bitcast to double, fcmp une 0.0. + * For int/bool types: icmp ne i64 payload, 0. + */ + public static int emitCoerceToBoolRuntime(IRBuilder builder, Target t, + Environment env, int tagVar, int payloadVar) { + IRBuilder.Gen gen = builder.generator(t, env); + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + llvmenv.addGlobalDeclaration(AsmCommonLibTemplates.STRLEN, env); + + int resultAlloca = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + gen.alloca(resultAlloca, IRType.INTEGER1); + + int isStr = gen.isStringTag(tagVar); + int brStrSlot = builder.reserveLine(t); + + // --- String path: strlen != 0 --- + int lblStr = llvmenv.getGotoLabel(); + builder.appendLabel(t, lblStr); + int strPtr = gen.inttoptr(IRType.INTEGER64, payloadVar, IRType.STRING); + int len = llvmenv.getNewLocalVariableReference(IRType.INTEGER64); + builder.appendLine(t, "%" + len + " = call i64 @strlen(i8* %" + strPtr + ")"); + int truthStr = gen.icmp(ICmpPredicate.NE, IRType.INTEGER64, len, "0"); + gen.store(IRType.INTEGER1, truthStr, resultAlloca); + int brStrDoneSlot = builder.reserveLine(t); + + // --- Non-string path: float vs int/bool (branchless) --- + int lblNonStr = llvmenv.getGotoLabel(); + builder.fillReservedLine(brStrSlot, + () -> gen.brCond(isStr, lblStr, lblNonStr)); + builder.appendLabel(t, lblNonStr); + int isFloat = gen.isFloatTag(tagVar); + int asDbl = gen.bitcast(IRType.INTEGER64, payloadVar, IRType.DOUBLE); + int truthFloat = gen.fcmp(FCmpPredicate.UNE, IRType.DOUBLE, asDbl, "0.0"); + int truthInt = gen.icmp(ICmpPredicate.NE, IRType.INTEGER64, payloadVar, "0"); + int truthNonStr = gen.select(isFloat, IRType.INTEGER1, + truthFloat, truthInt); + gen.store(IRType.INTEGER1, truthNonStr, resultAlloca); + int brNonStrDoneSlot = builder.reserveLine(t); + + // --- Merge --- + int lblDone = llvmenv.getGotoLabel(); + builder.fillReservedLine(brStrDoneSlot, () -> gen.br(lblDone)); + builder.fillReservedLine(brNonStrDoneSlot, () -> gen.br(lblDone)); + builder.appendLabel(t, lblDone); + int result = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + gen.load(result, IRType.INTEGER1, resultAlloca); + return result; + } + + /** + * Emits branchless coercion of an ms_value payload to double at runtime. + * For float types: bitcast i64 to double. + * For int/bool types: sitofp i64 to double. + */ + public static int emitCoerceToDoubleRuntime(IRBuilder builder, Target t, + Environment env, int tagVar, int payloadVar) { + IRBuilder.Gen gen = builder.generator(t, env); + int isFloat = gen.isFloatTag(tagVar); + int asDbl = gen.bitcast(IRType.INTEGER64, payloadVar, IRType.DOUBLE); + int asInt = gen.sitofp(IRType.INTEGER64, payloadVar, IRType.DOUBLE); + return gen.select(isFloat, IRType.DOUBLE, asDbl, asInt); + } +} diff --git a/src/main/java/com/laytonsmith/core/asm/IRType.java b/src/main/java/com/laytonsmith/core/asm/IRType.java index c5e2467db..feb91f68b 100644 --- a/src/main/java/com/laytonsmith/core/asm/IRType.java +++ b/src/main/java/com/laytonsmith/core/asm/IRType.java @@ -8,70 +8,89 @@ * in LLVM, but we don't have support for it, to reduce the complexity here. */ public enum IRType { - /** - * 1 bit integer, that is, a boolean. - */ - INTEGER1("i1", Category.INTEGER, 1), /** * 8 bit integer */ - INTEGER8("i8", Category.INTEGER, 8), + INTEGER8("i8", Category.INTEGER, 8, 0), /** * 16 bit integer */ - INTEGER16("i16", Category.INTEGER, 16), + INTEGER16("i16", Category.INTEGER, 16, 1), /** * 32 bit integer */ - INTEGER32("i32", Category.INTEGER, 32), + INTEGER32("i32", Category.INTEGER, 32, 2), /** * 64 bit integer. Note that in MethodScript, "int" is defined as 64 bit, but * many native libraries use other integer sizes. */ - INTEGER64("i64", Category.INTEGER, 64), - /** - * A pointer to a 8 bit integer - */ - INTEGER8POINTER("i8*", Category.POINTER, -1), - /** - * A 2D pointer to an 8 bit integer - */ - INTEGER8POINTERPOINTER("i8**", Category.POINTER, -1), + INTEGER64("i64", Category.INTEGER, 64, 3), /** * 16 bit floating point value */ - HALF("half", Category.FLOAT, 16), + HALF("half", Category.FLOAT, 16, 4), /** * 32 bit floating point value */ - FLOAT("float", Category.FLOAT, 32), + FLOAT("float", Category.FLOAT, 32, 5), /** * 64 bit floating point value. Note that in MethodScript, "double" is defined as 64 bit. */ - DOUBLE("double", Category.FLOAT, 64), + DOUBLE("double", Category.FLOAT, 64, 6), /** * 128 bit floating point value */ - FP128("fp128", Category.FLOAT, 128), + FP128("fp128", Category.FLOAT, 128, 7), /** * A string is just an i8* (a char array) but for convenience, we refer to it as a different type. */ - STRING("i8*", Category.POINTER, -1), + STRING("i8*", Category.POINTER, -1, 8), + /** + * 1 bit integer, that is, a boolean. + */ + INTEGER1("i1", Category.INTEGER, 1, 9), + /** + * A pointer to a 8 bit integer + */ + INTEGER8POINTER("i8*", Category.POINTER, -1, -1), + /** + * A 2D pointer to an 8 bit integer + */ + INTEGER8POINTERPOINTER("i8**", Category.POINTER, -1, -1), + /** + * MethodScript null. Represented as an ms_value with a zero payload. + */ + MS_NULL("{ i8, i64 }", Category.STRUCT, -1, 10), + /** + * void type, for functions that return nothing. + */ + VOID("void", Category.UNSET, 0, -1), + /** + * The boxed auto type. A tagged union that can hold any MethodScript primitive value or a pointer. + * Layout: { i8 tag, i64 payload } + */ + MS_VALUE("{ i8, i64 }", Category.STRUCT, -1, -1), + /** + * Pointer to a boxed auto type value. + */ + MS_VALUE_PTR("{ i8, i64 }*", Category.POINTER, -1, -1), /** * OTHER is a type which means that this value can't be parsed directly. This should only be used for values which * are used internally within a single instruction group, and never returned as a type. (This is checked in the * relevant places in IRBuilder and an error is thrown). */ - OTHER(null, Category.UNSET, -1); + OTHER(null, Category.UNSET, -1, -1); private final String irType; private final Category category; private final Integer bitDepth; + private final int boxTag; - private IRType(String irType, Category category, Integer bitDepth) { + private IRType(String irType, Category category, Integer bitDepth, int boxTag) { this.irType = irType; this.category = category; this.bitDepth = bitDepth; + this.boxTag = boxTag; } public String getIRType() { @@ -82,6 +101,31 @@ public Category getCategory() { return this.category; } + /** + * Returns the tag value used to identify this type in an ms_value tagged union. Types that cannot be boxed + * return -1. Use {@link #isBoxable()} to check first. + * @return The tag value, or -1 if this type is not boxable. + */ + public int getBoxTag() { + return this.boxTag; + } + + /** + * Returns the tag value as a string, for use in IR generation. + * @return The tag value as a string. + */ + public String getBoxTagString() { + return Integer.toString(this.boxTag); + } + + /** + * Returns true if this type can be stored in an ms_value tagged union. + * @return + */ + public boolean isBoxable() { + return this.boxTag >= 0; + } + /** * Returns true if the value is variable length. If this is variable length, and getBitDepth is called on this * type, an Error is thrown, instead, you have to dynamically calculate the bit depth. @@ -127,6 +171,7 @@ public static enum Category { INTEGER, FLOAT, POINTER, - ARRAY + ARRAY, + STRUCT } } diff --git a/src/main/java/com/laytonsmith/core/asm/LLVMArgumentValidation.java b/src/main/java/com/laytonsmith/core/asm/LLVMArgumentValidation.java index 36cbe066f..a1f8d690a 100644 --- a/src/main/java/com/laytonsmith/core/asm/LLVMArgumentValidation.java +++ b/src/main/java/com/laytonsmith/core/asm/LLVMArgumentValidation.java @@ -5,6 +5,7 @@ import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.asm.IRType.Category; import com.laytonsmith.core.compiler.CompilerEnvironment; +import com.laytonsmith.core.constructs.CBoolean; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CDouble; import com.laytonsmith.core.constructs.CFunction; @@ -54,7 +55,26 @@ private static IRData convert(Target t, IRType expectedType, IRBuilder builder, } return IRDataBuilder.setReturnVariable(ret, expectedType); } + if(data.getResultType() == IRType.MS_VALUE) { + // Unbox ms_value to the expected concrete type + IRData unboxed = emitUnbox(builder, t, env, expectedType, data.getResultVariable()); + return unboxed; + } + if(expectedType == IRType.MS_VALUE) { + // Box the concrete type into an ms_value + return emitBox(builder, t, env, data.getResultType(), data.getReference()); + } if(expectedType == IRType.STRING) { + if(data.getResultType() == IRType.INTEGER1) { + // Boolean to string: select between "true" and "false" constants + String trueStr = llvmenv.getOrPutStringConstant("true"); + String falseStr = llvmenv.getOrPutStringConstant("false"); + int result = llvmenv.getNewLocalVariableReference(IRType.STRING); + builder.appendLine(t, "%" + result + " = select " + data.getReference() + + ", i8* getelementptr inbounds ([5 x i8], [5 x i8]* @" + trueStr + ", i64 0, i64 0)" + + ", i8* getelementptr inbounds ([6 x i8], [6 x i8]* @" + falseStr + ", i64 0, i64 0)"); + return IRDataBuilder.setReturnVariable(result, IRType.STRING); + } // Everything can be converted to a string via sprintf, but it requires slightly different code for each. if(os.isWindows()) { llvmenv.addSystemHeader("stdio.h"); @@ -101,7 +121,7 @@ public static IRData getInt64(IRBuilder builder, Environment env, ParseTree c, T public static IRData getInt32(IRBuilder builder, Environment env, ParseTree c, Target t) throws ConfigCompileException { if(c.isConst()) { Mixed data = c.getData(); - int i = ArgumentValidation.getInt32(data, t); + int i = ArgumentValidation.getInt32(data, t, null); return IRDataBuilder.asConstant(IRType.INTEGER32, Integer.toString(i)); } else if(c.getData() instanceof CFunction) { return handleFunction(t, IRType.INTEGER32, builder, env, c); @@ -152,28 +172,31 @@ public static IRData getString(IRBuilder builder, Environment env, ParseTree c, } /** - * Given a CClassType, returns the IRType that this maps to. Auto and other unmappable concepts return the generic - * struct type. + * Given a CClassType, returns the IRType that this maps to. Types that map 1:1 to a concrete LLVM primitive + * (int, double, string, boolean) return the corresponding IRType. Everything else (auto, number, mixed, + * arrays, objects, interface types) returns MS_VALUE. * @param type * @return - * @throws ClassNotFoundException */ public static IRType convertCClassTypeToIRType(CClassType type) { + if(type == CInt.TYPE) { + return IRType.INTEGER64; + } else if(type == CDouble.TYPE) { + return IRType.DOUBLE; + } else if(type == CBoolean.TYPE) { + return IRType.INTEGER1; + } else if(CClassType.AUTO.equals(type)) { + return IRType.MS_VALUE; + } try { - if(CInt.TYPE.isExtendedBy(type)) { - return IRType.INTEGER64; - } else if(CDouble.TYPE.isExtendedBy(type)) { - return IRType.DOUBLE; - } else if(CString.TYPE.isExtendedBy(type)) { + if(CString.TYPE.isExtendedBy(type)) { return IRType.STRING; } } catch (ClassNotFoundException ex) { throw new UnsupportedOperationException(ex); } - // TODO: Eventually, this will just return the arbitrary data structure type. However, for native types, - // we should support all of them directly, so for the time being, this just throws. - throw new UnsupportedOperationException(type.getFQCN().getFQCN() + " is not yet supported"); + return IRType.MS_VALUE; } public static String getValueFromConstant(IRBuilder builder, ParseTree data, Environment env) { @@ -206,10 +229,8 @@ public static IRData getAny(IRBuilder builder, Environment env, ParseTree c, Tar int load = e.getNewLocalVariableReference(datatype); builder.generator(t, env).allocaStoreAndLoad(alloca, datatype, data, load); return IRDataBuilder.setReturnVariable(load, datatype); - } else if(c.getData() instanceof CFunction cf) { - Set exceptions = new HashSet<>(); - CClassType retType = cf.getCachedFunction().typecheck(e.getStaticAnalysis(), c, env, exceptions); - IRData data = handleFunction(t, convertCClassTypeToIRType(retType), builder, env, c); + } else if(c.getData() instanceof CFunction) { + IRData data = AsmCompiler.getIR(builder, c, env); int alloca = e.getNewLocalVariableReference(data.getResultType()); int load = e.getNewLocalVariableReference(data.getResultType()); builder.generator(t, env).allocaStoreAndLoad(alloca, data.getResultType(), data.getResultVariable(), load); @@ -217,10 +238,146 @@ public static IRData getAny(IRBuilder builder, Environment env, ParseTree c, Tar } else if(c.getData() instanceof IVariable ivar) { String name = ivar.getVariableName(); IRType datatype = convertCClassTypeToIRType(e.getVariableType(name)); - int load = e.getNewLocalVariableReference(datatype); - builder.generator(t, env).load(load, datatype, e.getVariableMapping(name)); - return IRDataBuilder.setReturnVariable(load, datatype); + int varRef = e.getVariableMapping(name); + return IRDataBuilder.setReturnVariable(varRef, datatype); } throw new UnsupportedOperationException(); } + + /** + * Boxes a concrete value into an ms_value. If the value is already an ms_value (or ms_null), + * returns it unchanged. + * @param builder The IR builder to append to. + * @param t The code target for debug info. + * @param env The environment. + * @param data The value to box. + * @return IRData pointing to the boxed ms_value variable. + */ + public static IRData boxToMsValue(IRBuilder builder, Target t, Environment env, IRData data) { + if(data.getResultType() == IRType.MS_VALUE || data.getResultType() == IRType.MS_NULL) { + return data; + } + return emitBox(builder, t, env, data.getResultType(), data.getReference()); + } + + /** + * Emits IR to box a typed value into an ms_value. The result is an ms_value stored in a local variable. + * @param builder The IR builder to append to. + * @param t The code target for debug info. + * @param env The environment. + * @param sourceType The concrete IR type of the value being boxed. + * @param sourceRef The reference to the value (e.g. "i64 %5" or "i64 42"). + * @return IRData pointing to the boxed ms_value variable. + */ + private static IRData emitBox(IRBuilder builder, Target t, Environment env, + IRType sourceType, String sourceRef) { + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + if(!sourceType.isBoxable()) { + throw new UnsupportedOperationException("Cannot box type " + sourceType.getIRType()); + } + + // Convert the value to i64 payload + String payloadRef; + if(sourceType.getCategory() == IRType.Category.INTEGER) { + if(sourceType == IRType.INTEGER64) { + payloadRef = sourceRef; + } else { + int zext = llvmenv.getNewLocalVariableReference(IRType.INTEGER64); + builder.appendLine(t, "%" + zext + " = zext " + sourceRef + " to i64"); + payloadRef = "i64 %" + zext; + } + } else if(sourceType.getCategory() == IRType.Category.FLOAT) { + if(sourceType == IRType.DOUBLE) { + int bitcast = llvmenv.getNewLocalVariableReference(IRType.INTEGER64); + builder.appendLine(t, "%" + bitcast + " = bitcast " + sourceRef + " to i64"); + payloadRef = "i64 %" + bitcast; + } else { + // Upcast to double first, then bitcast to i64 + int fpext = llvmenv.getNewLocalVariableReference(IRType.DOUBLE); + builder.appendLine(t, "%" + fpext + " = fpext " + sourceRef + " to double"); + int bitcast = llvmenv.getNewLocalVariableReference(IRType.INTEGER64); + builder.appendLine(t, "%" + bitcast + " = bitcast double %" + fpext + " to i64"); + payloadRef = "i64 %" + bitcast; + } + } else if(sourceType == IRType.STRING || sourceType == IRType.INTEGER8POINTER) { + int ptrtoint = llvmenv.getNewLocalVariableReference(IRType.INTEGER64); + builder.appendLine(t, "%" + ptrtoint + " = ptrtoint " + sourceRef + " to i64"); + payloadRef = "i64 %" + ptrtoint; + } else { + throw new UnsupportedOperationException("Cannot box type " + sourceType.getIRType()); + } + + // Build the struct field by field from undef + int insertTag = llvmenv.getNewLocalVariableReference(IRType.MS_VALUE); + int insertPayload = llvmenv.getNewLocalVariableReference(IRType.MS_VALUE); + builder.appendLine(t, "%" + insertTag + " = insertvalue { i8, i64 } undef, i8 " + + sourceType.getBoxTag() + ", 0"); + builder.appendLine(t, "%" + insertPayload + " = insertvalue { i8, i64 } %" + insertTag + + ", " + payloadRef + ", 1"); + + return IRDataBuilder.setReturnVariable(insertPayload, IRType.MS_VALUE); + } + + /** + * Emits IR to unbox an ms_value into a specific expected type. + * @param builder The IR builder to append to. + * @param t The code target for debug info. + * @param env The environment. + * @param expectedType The concrete IR type to extract. + * @param boxedVariable The variable reference holding the ms_value. + * @return IRData pointing to the unboxed value. + */ + private static IRData emitUnbox(IRBuilder builder, Target t, Environment env, + IRType expectedType, int boxedVariable) { + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + + // Extract the i64 payload from index 1 + int payload = llvmenv.getNewLocalVariableReference(IRType.INTEGER64); + builder.appendLine(t, "%" + payload + " = extractvalue { i8, i64 } %" + boxedVariable + ", 1"); + + if(expectedType == IRType.INTEGER64) { + return IRDataBuilder.setReturnVariable(payload, IRType.INTEGER64); + } else if(expectedType.getCategory() == IRType.Category.INTEGER) { + // Truncate from i64 to smaller integer + int trunc = llvmenv.getNewLocalVariableReference(expectedType); + builder.appendLine(t, "%" + trunc + " = trunc i64 %" + payload + " to " + + expectedType.getIRType()); + return IRDataBuilder.setReturnVariable(trunc, expectedType); + } else if(expectedType == IRType.DOUBLE) { + // Reinterpret the i64 bits as double + int bitcast = llvmenv.getNewLocalVariableReference(IRType.DOUBLE); + builder.appendLine(t, "%" + bitcast + " = bitcast i64 %" + payload + " to double"); + return IRDataBuilder.setReturnVariable(bitcast, IRType.DOUBLE); + } else if(expectedType.getCategory() == IRType.Category.FLOAT) { + // Reinterpret as double first, then narrow to smaller float + int bitcast = llvmenv.getNewLocalVariableReference(IRType.DOUBLE); + builder.appendLine(t, "%" + bitcast + " = bitcast i64 %" + payload + " to double"); + int fptrunc = llvmenv.getNewLocalVariableReference(expectedType); + builder.appendLine(t, "%" + fptrunc + " = fptrunc double %" + bitcast + " to " + + expectedType.getIRType()); + return IRDataBuilder.setReturnVariable(fptrunc, expectedType); + } else if(expectedType == IRType.STRING || expectedType == IRType.INTEGER8POINTER) { + // Convert the integer back to a pointer + int inttoptr = llvmenv.getNewLocalVariableReference(expectedType); + builder.appendLine(t, "%" + inttoptr + " = inttoptr i64 %" + payload + " to " + + expectedType.getIRType()); + return IRDataBuilder.setReturnVariable(inttoptr, expectedType); + } + throw new UnsupportedOperationException("Cannot unbox to type " + expectedType.getIRType()); + } + + /** + * Emits IR to extract the type tag from an ms_value. + * @param builder The IR builder to append to. + * @param t The code target for debug info. + * @param env The environment. + * @param boxedVariable The variable reference holding the ms_value. + * @return IRData pointing to the i8 tag value. + */ + static IRData emitGetTag(IRBuilder builder, Target t, Environment env, int boxedVariable) { + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + int tag = llvmenv.getNewLocalVariableReference(IRType.INTEGER8); + builder.appendLine(t, "%" + tag + " = extractvalue { i8, i64 } %" + boxedVariable + ", 0"); + return IRDataBuilder.setReturnVariable(tag, IRType.INTEGER8); + } } diff --git a/src/main/java/com/laytonsmith/core/asm/LLVMEnvironment.java b/src/main/java/com/laytonsmith/core/asm/LLVMEnvironment.java index e65f2bfe6..e5ce974cf 100644 --- a/src/main/java/com/laytonsmith/core/asm/LLVMEnvironment.java +++ b/src/main/java/com/laytonsmith/core/asm/LLVMEnvironment.java @@ -2,9 +2,12 @@ import com.laytonsmith.PureUtilities.Common.OSUtils; +import com.laytonsmith.core.LogLevel; +import com.laytonsmith.core.MSLog; import com.laytonsmith.core.asm.metadata.LLVMMetadataRegistry; import com.laytonsmith.core.compiler.analysis.StaticAnalysis; import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.environments.Environment; import java.util.HashMap; @@ -24,7 +27,7 @@ public class LLVMEnvironment implements Environment.EnvironmentImpl { private final Map strings = new HashMap<>(); private final Stack> variableTable = new Stack<>(); private final Stack> variableTableType = new Stack<>(); - private final Stack> irVariableTableType = new Stack<>(); + private final Map irVariableTableType = new HashMap<>(); private final Set globalDeclarations = new HashSet<>(); private final StaticAnalysis staticAnalysis = new StaticAnalysis(false); private int localVariableCounter = 0; @@ -146,6 +149,8 @@ public String getGlobalDeclarations() { */ public void newMethodFrame(String methodName) { localVariableCounter = 0; + irVariableTableType.clear(); + pushVariableScope(); } /** @@ -155,7 +160,7 @@ public void newMethodFrame(String methodName) { */ public int getNewLocalVariableReference(IRType type) { int value = localVariableCounter++; - irVariableTableType.peek().put(value, type); + irVariableTableType.put(value, type); return value; } @@ -200,36 +205,66 @@ public void setOptimizationLevel(OptimizationLevel optimizationLevel) { public void pushVariableScope() { variableTable.push(new HashMap<>()); variableTableType.push(new HashMap<>()); - irVariableTableType.push(new HashMap<>()); } public void popVariableScope() { variableTable.pop(); variableTableType.pop(); - irVariableTableType.pop(); } public void addVariableMapping(String methodscriptVariableName, int llvmVariableName, CClassType type) { + // Check for shadowing in enclosing scopes (not the current scope - redefinition in same scope is fine) + for(int i = variableTable.size() - 2; i >= 0; i--) { + if(variableTable.get(i).containsKey(methodscriptVariableName)) { + MSLog.GetLogger().Log(MSLog.Tags.COMPILER, LogLevel.WARNING, + "Variable " + methodscriptVariableName + " shadows a variable of the same name" + + " in an enclosing scope.", Target.UNKNOWN); + break; + } + } variableTable.peek().put(methodscriptVariableName, llvmVariableName); variableTableType.peek().put(methodscriptVariableName, type); } /** * Returns the IR reference to this variable. All variables are allocaStoreAndLoaded, so this is the value of - * the load instruction. + * the load instruction. Walks the scope stack from innermost to outermost, returning the first match. * @param methodscriptVariableName - * @return + * @return The variable reference. + * @throws RuntimeException if the variable is not defined in any scope. */ public int getVariableMapping(String methodscriptVariableName) { - return variableTable.peek().get(methodscriptVariableName); + for(int i = variableTable.size() - 1; i >= 0; i--) { + Integer result = variableTable.get(i).get(methodscriptVariableName); + if(result != null) { + return result; + } + } + throw new RuntimeException("Variable " + methodscriptVariableName + " is not defined."); } + /** + * Returns the CClassType for the given variable. Walks the scope stack from innermost to outermost. + * @param methodscriptVariableName + * @return The variable type, or null if not found in any scope. + */ public CClassType getVariableType(String methodscriptVariableName) { - return variableTableType.peek().get(methodscriptVariableName); + for(int i = variableTableType.size() - 1; i >= 0; i--) { + CClassType result = variableTableType.get(i).get(methodscriptVariableName); + if(result != null) { + return result; + } + } + return null; } + /** + * Returns the IRType for the given local variable reference. Walks the scope stack from innermost to outermost. + * @param variable + * @return The IR type, or null if not found in any scope. + */ public IRType getIRType(int variable) { - return irVariableTableType.peek().get(variable); + return irVariableTableType.get(variable); } public int getNewMetadataId() { diff --git a/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java b/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java index 05cc39ef6..e7bd7d205 100644 --- a/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java +++ b/src/main/java/com/laytonsmith/core/asm/LLVMFunction.java @@ -45,17 +45,28 @@ public String docs() { return getDefaultDocs(); } - private FunctionBase getDefaultFunction() throws ConfigCompileException { + /** + * Returns the Function object as defined in the Java Intepreter. This can be used to grab common values like + * the signature. + * @return + * @throws ConfigCompileException + */ + protected Function getDefaultFunction() { CFunction f = new CFunction(getName(), Target.UNKNOWN); - FunctionBase fb = FunctionList.getFunction(f, api.Platforms.INTERPRETER_JAVA, null); - return fb; + FunctionBase fb; + try { + fb = FunctionList.getFunction(f, api.Platforms.INTERPRETER_JAVA, null); + } catch(ConfigCompileException ex) { + throw new Error(ex); + } + return (Function) fb; } private String getDefaultDocs() { try { FunctionBase fb = getDefaultFunction(); return fb.docs(); - } catch (ConfigCompileException ex) { + } catch (Error ex) { return "mixed {...} This function is missing documentation. Please report it."; } } diff --git a/src/main/java/com/laytonsmith/core/asm/LLVMPlatformResolver.java b/src/main/java/com/laytonsmith/core/asm/LLVMPlatformResolver.java index 33026372d..cc3be7be6 100644 --- a/src/main/java/com/laytonsmith/core/asm/LLVMPlatformResolver.java +++ b/src/main/java/com/laytonsmith/core/asm/LLVMPlatformResolver.java @@ -1,5 +1,6 @@ package com.laytonsmith.core.asm; +import com.laytonsmith.core.constructs.CBoolean; import com.laytonsmith.core.constructs.CDouble; import com.laytonsmith.core.constructs.CInt; import com.laytonsmith.core.constructs.CString; @@ -19,7 +20,9 @@ public static IRData outputConstant(IRBuilder builder, Mixed c, Environment env) if(c == null) { throw new NullPointerException("Unexpected null value"); } - if(c instanceof CInt ci) { + if(c instanceof CBoolean cb) { + return IRDataBuilder.asConstant(IRType.INTEGER1, cb.getBoolean() ? "1" : "0"); + } else if(c instanceof CInt ci) { return IRDataBuilder.asConstant(IRType.INTEGER64, Long.toString(ci.getInt())); } else if(c instanceof CDouble cd) { // Get the raw bytes, and write those out as a hex string, to keep full precision. diff --git a/src/main/java/com/laytonsmith/core/constructs/CArray.java b/src/main/java/com/laytonsmith/core/constructs/CArray.java index c9263ea2f..df5e6e6dc 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CArray.java +++ b/src/main/java/com/laytonsmith/core/constructs/CArray.java @@ -1166,7 +1166,7 @@ public void ensureCapacity(int capacity) { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.FINAL); + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); } @Override diff --git a/src/main/java/com/laytonsmith/core/constructs/CBoolean.java b/src/main/java/com/laytonsmith/core/constructs/CBoolean.java index 8fe73d2a0..e649d9e46 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CBoolean.java +++ b/src/main/java/com/laytonsmith/core/constructs/CBoolean.java @@ -146,7 +146,7 @@ public CClassType[] getInterfaces() { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.FINAL); + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); } @Override diff --git a/src/main/java/com/laytonsmith/core/constructs/CByteArray.java b/src/main/java/com/laytonsmith/core/constructs/CByteArray.java index 9aac413bc..a2d469730 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CByteArray.java +++ b/src/main/java/com/laytonsmith/core/constructs/CByteArray.java @@ -775,7 +775,7 @@ public CClassType[] getSuperclasses() { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.STATIC, ObjectModifier.FINAL); + return EnumSet.of(ObjectModifier.STATIC, ObjectModifier.FINAL, ObjectModifier.NATIVE); } @Override @@ -819,7 +819,7 @@ public CClassType[] getSuperclasses() { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.STATIC); + return EnumSet.of(ObjectModifier.STATIC, ObjectModifier.NATIVE); } @Override diff --git a/src/main/java/com/laytonsmith/core/constructs/CDouble.java b/src/main/java/com/laytonsmith/core/constructs/CDouble.java index b873010ae..79d212830 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CDouble.java +++ b/src/main/java/com/laytonsmith/core/constructs/CDouble.java @@ -5,13 +5,17 @@ import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.constructs.generics.GenericParameters; import com.laytonsmith.core.exceptions.CRE.CREFormatException; +import com.laytonsmith.core.objects.ObjectModifier; + +import java.util.EnumSet; +import java.util.Set; /** * * */ @typeof("ms.lang.double") -public class CDouble extends CNumber implements Cloneable { +public final class CDouble extends CNumber implements Cloneable { @SuppressWarnings("FieldNameHidesFieldInSuperclass") public static final CClassType TYPE = CClassType.get(CDouble.class); @@ -78,6 +82,11 @@ public double getNumber() { return val; } + @Override + public Set getObjectModifiers() { + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); + } + @Override public GenericParameters getGenericParameters() { return null; diff --git a/src/main/java/com/laytonsmith/core/constructs/CInt.java b/src/main/java/com/laytonsmith/core/constructs/CInt.java index d48ab6708..4ba624eba 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CInt.java +++ b/src/main/java/com/laytonsmith/core/constructs/CInt.java @@ -5,13 +5,17 @@ import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.constructs.generics.GenericParameters; import com.laytonsmith.core.exceptions.CRE.CREFormatException; +import com.laytonsmith.core.objects.ObjectModifier; + +import java.util.EnumSet; +import java.util.Set; /** * * */ @typeof("ms.lang.int") -public class CInt extends CNumber implements Cloneable { +public final class CInt extends CNumber implements Cloneable { @SuppressWarnings("FieldNameHidesFieldInSuperclass") public static final CClassType TYPE = CClassType.get(CInt.class); @@ -77,6 +81,11 @@ public double getNumber() { return (double) val; } + @Override + public Set getObjectModifiers() { + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); + } + @Override public GenericParameters getGenericParameters() { return null; diff --git a/src/main/java/com/laytonsmith/core/constructs/CMutablePrimitive.java b/src/main/java/com/laytonsmith/core/constructs/CMutablePrimitive.java index b32896aec..5e8278e8b 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CMutablePrimitive.java +++ b/src/main/java/com/laytonsmith/core/constructs/CMutablePrimitive.java @@ -199,7 +199,7 @@ public CClassType[] getInterfaces() { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.FINAL); + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); } } diff --git a/src/main/java/com/laytonsmith/core/constructs/CResource.java b/src/main/java/com/laytonsmith/core/constructs/CResource.java index cbd507a34..4df6a2797 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CResource.java +++ b/src/main/java/com/laytonsmith/core/constructs/CResource.java @@ -136,7 +136,7 @@ public CClassType[] getInterfaces() { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.FINAL); + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); } @Override diff --git a/src/main/java/com/laytonsmith/core/constructs/CVoid.java b/src/main/java/com/laytonsmith/core/constructs/CVoid.java index 9d29a6929..3df235fff 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CVoid.java +++ b/src/main/java/com/laytonsmith/core/constructs/CVoid.java @@ -78,7 +78,7 @@ public CClassType[] getInterfaces() { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.FINAL); + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); } @Override diff --git a/src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java b/src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java index 676e4a526..e875cd339 100644 --- a/src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java +++ b/src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java @@ -695,7 +695,7 @@ public LeftHandSideType getNakedType(Target t, Environment env) { @Override public Set getObjectModifiers() { - return EnumSet.of(ObjectModifier.FINAL); + return EnumSet.of(ObjectModifier.FINAL, ObjectModifier.NATIVE); } /** diff --git a/src/main/java/com/laytonsmith/core/functions/Math.java b/src/main/java/com/laytonsmith/core/functions/Math.java index 0d94e0a88..23d7b896f 100644 --- a/src/main/java/com/laytonsmith/core/functions/Math.java +++ b/src/main/java/com/laytonsmith/core/functions/Math.java @@ -21,6 +21,8 @@ import com.laytonsmith.core.StepAction.StepResult; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.compiler.OptimizationUtilities; +import com.laytonsmith.core.compiler.signature.FunctionSignatures; +import com.laytonsmith.core.compiler.signature.SignatureBuilder; import com.laytonsmith.core.constructs.CArray; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CDouble; @@ -1235,12 +1237,12 @@ public ExampleScript[] examples() throws ConfigCompileException { } @Override - public CClassType getReturnType(Target t, List argTypes, List argTargets, Environment env, Set exceptions) { - if(argTypes.size() == 0) { - return CDouble.TYPE; - } else { - return CInt.TYPE; - } + public FunctionSignatures getSignatures() { + return new SignatureBuilder(CDouble.TYPE, "No-arg variant, which returns a double") + .newSignature(CInt.TYPE, "One or two arg variant, which returns an integer within a range.") + .param(CInt.TYPE, "minOrMax", "With only one argument, this is the max. With two, this is the min.") + .param(CInt.TYPE, "max", "The max. Max is exclusive.", true) + .build(); } } diff --git a/src/main/java/com/laytonsmith/core/functions/asm/BasicLogic.java b/src/main/java/com/laytonsmith/core/functions/asm/BasicLogic.java new file mode 100644 index 000000000..0ce955ca5 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/functions/asm/BasicLogic.java @@ -0,0 +1,369 @@ +package com.laytonsmith.core.functions.asm; + +import com.laytonsmith.PureUtilities.Version; +import com.laytonsmith.annotations.api; +import com.laytonsmith.core.ParseTree; +import com.laytonsmith.core.asm.AsmCommonLibTemplates; +import com.laytonsmith.core.asm.IRBuilder; +import com.laytonsmith.core.asm.IRBuilder.FCmpPredicate; +import com.laytonsmith.core.asm.IRBuilder.ICmpPredicate; +import com.laytonsmith.core.asm.IRCoercion; +import com.laytonsmith.core.asm.IRData; +import com.laytonsmith.core.asm.IRDataBuilder; +import com.laytonsmith.core.asm.IRType; +import com.laytonsmith.core.asm.LLVMArgumentValidation; +import com.laytonsmith.core.asm.LLVMEnvironment; +import com.laytonsmith.core.asm.LLVMFunction; +import com.laytonsmith.core.asm.LLVMVersion; +import com.laytonsmith.core.compiler.signature.FunctionSignatures; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.GenericParameters; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREThrowable; +import com.laytonsmith.core.exceptions.ConfigCompileException; + +/** + * + */ +public class BasicLogic { + + /** + * Comparison strategy for compile-time known types. Matches the interpreter's priority: + * boolean first, then string, then numeric. + */ + private enum ComparisonStrategy { + BOOLEAN, + NUMERIC, + STRING + } + + /** + * Determines the comparison strategy based on compile-time known types. If any arg is + * boolean, all are compared as booleans. If all are strings, compared as strings. Otherwise + * compared as doubles (numeric). + */ + private static ComparisonStrategy determineStrategy(IRData[] args) { + boolean anyBoolean = false; + boolean anyString = false; + boolean anyNumeric = false; + + for(IRData arg : args) { + IRType type = arg.getResultType(); + if(type == IRType.INTEGER1) { + anyBoolean = true; + } else if(type == IRType.STRING) { + anyString = true; + } else if(type.getCategory() == IRType.Category.INTEGER + || type.getCategory() == IRType.Category.FLOAT) { + anyNumeric = true; + } else { + throw new UnsupportedOperationException( + "Unsupported type in comparison: " + type); + } + } + + if(anyBoolean) { + return ComparisonStrategy.BOOLEAN; + } + if(anyString) { + if(anyNumeric) { + throw new UnsupportedOperationException( + "String + numeric comparison is not yet implemented"); + } + return ComparisonStrategy.STRING; + } + return ComparisonStrategy.NUMERIC; + } + + /** + * Emits a pairwise comparison for compile-time known types. + */ + private static int emitPairComparison(IRBuilder builder, Target t, Environment env, + ComparisonStrategy strategy, IRData left, IRData right) { + IRBuilder.Gen gen = builder.generator(t, env); + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + switch(strategy) { + case BOOLEAN: { + int l = IRCoercion.coerceToBoolean(builder, t, env, left); + int r = IRCoercion.coerceToBoolean(builder, t, env, right); + return gen.icmp(ICmpPredicate.EQ, IRType.INTEGER1, l, r); + } + case NUMERIC: { + int l = IRCoercion.coerceToDouble(builder, t, env, left); + int r = IRCoercion.coerceToDouble(builder, t, env, right); + return gen.fcmp(FCmpPredicate.OEQ, IRType.DOUBLE, l, r); + } + case STRING: { + llvmenv.addGlobalDeclaration(AsmCommonLibTemplates.STRCMP, env); + int cmp = llvmenv.getNewLocalVariableReference(IRType.INTEGER32); + builder.appendLine(t, "%" + cmp + " = call i32 @strcmp(" + + left.getReference() + ", " + + right.getReference() + ")"); + return gen.icmp(ICmpPredicate.EQ, IRType.INTEGER32, cmp, "0"); + } + default: + throw new UnsupportedOperationException( + "Unknown strategy: " + strategy); + } + } + + // ---- ms_value runtime dispatch helpers ---- + + /** + * Chains an array of i1 values with AND, returning the final result variable. + */ + private static int andChain(IRBuilder.Gen gen, int[] vals) { + int result = vals[0]; + for(int i = 1; i < vals.length; i++) { + result = gen.and(IRType.INTEGER1, result, vals[i]); + } + return result; + } + + /** + * Chains an array of i1 values with OR, returning the final result variable. + */ + private static int orChain(IRBuilder.Gen gen, int[] vals) { + int result = vals[0]; + for(int i = 1; i < vals.length; i++) { + result = gen.or(IRType.INTEGER1, result, vals[i]); + } + return result; + } + + /** + * Emits full runtime dispatch for comparing ms_value arguments. + * Follows the interpreter's priority: null > boolean > string > numeric. + */ + private static IRData emitMsValueEquals(IRBuilder builder, Target t, + Environment env, IRData[] args) { + IRBuilder.Gen gen = builder.generator(t, env); + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + llvmenv.addGlobalDeclaration(AsmCommonLibTemplates.STRCMP, env); + int n = args.length; + + int resultAlloca = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + gen.alloca(resultAlloca, IRType.INTEGER1); + + java.util.List doneSlots = new java.util.ArrayList<>(); + + // Extract tags and payloads for all args + int[] tags = new int[n]; + int[] payloads = new int[n]; + for(int i = 0; i < n; i++) { + tags[i] = gen.extractvalue(IRType.MS_VALUE, + args[i].getResultVariable(), 0, IRType.INTEGER8); + payloads[i] = gen.extractvalue(IRType.MS_VALUE, + args[i].getResultVariable(), 1, IRType.INTEGER64); + } + + // Check if any tag is null (tag == 10) + int[] isNull = new int[n]; + for(int i = 0; i < n; i++) { + isNull[i] = gen.isNullTag(tags[i]); + } + int anyNull = orChain(gen, isNull); + + int brNullSlot = builder.reserveLine(t); + + // --- Null path: equal iff ALL are null --- + int lblNullResult = llvmenv.getGotoLabel(); + builder.appendLabel(t, lblNullResult); + int allNull = andChain(gen, isNull); + gen.store(IRType.INTEGER1, allNull, resultAlloca); + doneSlots.add(builder.reserveLine(t)); + + int lblNotNull = llvmenv.getGotoLabel(); + builder.fillReservedLine(brNullSlot, + () -> gen.brCond(anyNull, lblNullResult, lblNotNull)); + + // --- Not null: check for booleans --- + builder.appendLabel(t, lblNotNull); + int[] isBool = new int[n]; + for(int i = 0; i < n; i++) { + isBool[i] = gen.isBoolTag(tags[i]); + } + int anyBool = orChain(gen, isBool); + + int brBoolSlot = builder.reserveLine(t); + + // --- Boolean path: coerce all to bool, compare pairwise --- + int lblBoolCmp = llvmenv.getGotoLabel(); + builder.appendLabel(t, lblBoolCmp); + int[] boolVals = new int[n]; + for(int i = 0; i < n; i++) { + boolVals[i] = IRCoercion.emitCoerceToBoolRuntime(builder, t, env, + tags[i], payloads[i]); + } + int[] boolPairs = new int[n - 1]; + for(int i = 0; i < n - 1; i++) { + boolPairs[i] = gen.icmp(ICmpPredicate.EQ, IRType.INTEGER1, + boolVals[i], boolVals[i + 1]); + } + int boolResult = andChain(gen, boolPairs); + gen.store(IRType.INTEGER1, boolResult, resultAlloca); + doneSlots.add(builder.reserveLine(t)); + + int lblStrCheck = llvmenv.getGotoLabel(); + builder.fillReservedLine(brBoolSlot, + () -> gen.brCond(anyBool, lblBoolCmp, lblStrCheck)); + + // --- String check: any string tags? --- + builder.appendLabel(t, lblStrCheck); + int[] isStr = new int[n]; + for(int i = 0; i < n; i++) { + isStr[i] = gen.isStringTag(tags[i]); + } + int anyStr = orChain(gen, isStr); + + int brStrSlot = builder.reserveLine(t); + + // --- String dispatch: verify ALL are strings --- + int lblStrDispatch = llvmenv.getGotoLabel(); + builder.appendLabel(t, lblStrDispatch); + int allStr = andChain(gen, isStr); + + int brAllStrSlot = builder.reserveLine(t); + + // --- String compare: strcmp each pair --- + int lblStrCmp = llvmenv.getGotoLabel(); + builder.appendLabel(t, lblStrCmp); + int[] strPairs = new int[n - 1]; + for(int i = 0; i < n - 1; i++) { + int ptrA = gen.inttoptr(IRType.INTEGER64, payloads[i], + IRType.STRING); + int ptrB = gen.inttoptr(IRType.INTEGER64, payloads[i + 1], + IRType.STRING); + int cmp = llvmenv.getNewLocalVariableReference(IRType.INTEGER32); + builder.appendLine(t, "%" + cmp + " = call i32 @strcmp(i8* %" + + ptrA + ", i8* %" + ptrB + ")"); + strPairs[i] = gen.icmp(ICmpPredicate.EQ, IRType.INTEGER32, cmp, "0"); + } + int strResult = andChain(gen, strPairs); + gen.store(IRType.INTEGER1, strResult, resultAlloca); + doneSlots.add(builder.reserveLine(t)); + + // --- String mismatch: mixed string + other at runtime, return false --- + int lblStrMismatch = llvmenv.getGotoLabel(); + builder.fillReservedLine(brAllStrSlot, + () -> gen.brCond(allStr, lblStrCmp, lblStrMismatch)); + + builder.appendLabel(t, lblStrMismatch); + gen.store(IRType.INTEGER1, "i1 0", resultAlloca); + doneSlots.add(builder.reserveLine(t)); + + int lblNumCmp = llvmenv.getGotoLabel(); + builder.fillReservedLine(brStrSlot, + () -> gen.brCond(anyStr, lblStrDispatch, lblNumCmp)); + + // --- Numeric path: coerce all to double, compare pairwise --- + builder.appendLabel(t, lblNumCmp); + int[] dblVals = new int[n]; + for(int i = 0; i < n; i++) { + dblVals[i] = IRCoercion.emitCoerceToDoubleRuntime(builder, t, env, + tags[i], payloads[i]); + } + int[] numPairs = new int[n - 1]; + for(int i = 0; i < n - 1; i++) { + numPairs[i] = gen.fcmp(FCmpPredicate.OEQ, IRType.DOUBLE, + dblVals[i], dblVals[i + 1]); + } + int numResult = andChain(gen, numPairs); + gen.store(IRType.INTEGER1, numResult, resultAlloca); + doneSlots.add(builder.reserveLine(t)); + + // --- Merge: fill all deferred branches and load result --- + int lblDone = llvmenv.getGotoLabel(); + for(int slot : doneSlots) { + builder.fillReservedLine(slot, () -> gen.br(lblDone)); + } + builder.appendLabel(t, lblDone); + int result = llvmenv.getNewLocalVariableReference(IRType.INTEGER1); + gen.load(result, IRType.INTEGER1, resultAlloca); + + return IRDataBuilder.setReturnVariable(result, IRType.INTEGER1); + } + + @api(environments = LLVMEnvironment.class, platform = api.Platforms.COMPILER_LLVM) + public static class equals extends LLVMFunction { + + @Override + public IRData buildIR(IRBuilder builder, Target t, Environment env, + GenericParameters generics, ParseTree... nodes) + throws ConfigCompileException { + if(nodes.length < 2) { + throw new ConfigCompileException( + "At least two arguments must be passed to equals", t); + } + + // Evaluate all arguments, preserving their native types + IRData[] args = new IRData[nodes.length]; + for(int i = 0; i < nodes.length; i++) { + args[i] = LLVMArgumentValidation.getAny( + builder, env, nodes[i], t); + } + + // Check if any arg is ms_value (runtime-typed) + boolean anyMsValue = false; + for(IRData arg : args) { + if(arg.getResultType() == IRType.MS_VALUE + || arg.getResultType() == IRType.MS_NULL) { + anyMsValue = true; + break; + } + } + + if(anyMsValue) { + // Box all concrete args to ms_value for uniform dispatch + for(int i = 0; i < args.length; i++) { + args[i] = LLVMArgumentValidation.boxToMsValue( + builder, t, env, args[i]); + } + return emitMsValueEquals(builder, t, env, args); + } + + // All compile-time known types - determine strategy statically + IRBuilder.Gen gen = builder.generator(t, env); + ComparisonStrategy strategy = determineStrategy(args); + int finalResult = -1; + for(int i = 0; i < args.length - 1; i++) { + int pairResult = emitPairComparison(builder, t, env, + strategy, args[i], args[i + 1]); + if(finalResult == -1) { + finalResult = pairResult; + } else { + finalResult = gen.and(IRType.INTEGER1, + finalResult, pairResult); + } + } + + return IRDataBuilder.setReturnVariable(finalResult, + IRType.INTEGER1); + } + + @Override + public String getName() { + return "equals"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{Integer.MAX_VALUE}; + } + + @Override + public Class[] thrown() { + return getDefaultFunction().thrown(); + } + + @Override + public Version since() { + return LLVMVersion.V0_0_1; + } + + @Override + public FunctionSignatures getSignatures() { + return getDefaultFunction().getSignatures(); + } + } +} diff --git a/src/main/java/com/laytonsmith/core/functions/asm/Compiler.java b/src/main/java/com/laytonsmith/core/functions/asm/Compiler.java index 717482573..ceb894b9d 100644 --- a/src/main/java/com/laytonsmith/core/functions/asm/Compiler.java +++ b/src/main/java/com/laytonsmith/core/functions/asm/Compiler.java @@ -20,6 +20,16 @@ * */ public class Compiler { + @api(environments = LLVMEnvironment.class, platform = api.Platforms.COMPILER_LLVM) + public static class __unsafe_assign__ extends DataHandling.assign { + + @Override + public String getName() { + return "__unsafe_assign__"; + } + + } + @api(environments = LLVMEnvironment.class, platform = api.Platforms.COMPILER_LLVM) public static class __statements__ extends LLVMFunction { diff --git a/src/main/java/com/laytonsmith/core/functions/asm/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/asm/ControlFlow.java index c04f49c88..782478e5f 100644 --- a/src/main/java/com/laytonsmith/core/functions/asm/ControlFlow.java +++ b/src/main/java/com/laytonsmith/core/functions/asm/ControlFlow.java @@ -1,81 +1,168 @@ package com.laytonsmith.core.functions.asm; +import com.laytonsmith.PureUtilities.Version; +import com.laytonsmith.annotations.api; +import com.laytonsmith.core.ParseTree; +import com.laytonsmith.core.asm.AsmCompiler; +import com.laytonsmith.core.asm.IRBuilder; +import com.laytonsmith.core.asm.IRCoercion; +import com.laytonsmith.core.asm.IRData; +import com.laytonsmith.core.asm.IRDataBuilder; +import com.laytonsmith.core.asm.LLVMArgumentValidation; +import com.laytonsmith.core.asm.LLVMEnvironment; +import com.laytonsmith.core.asm.LLVMFunction; +import com.laytonsmith.core.asm.LLVMVersion; +import com.laytonsmith.core.compiler.signature.FunctionSignatures; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.GenericParameters; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREThrowable; +import com.laytonsmith.core.exceptions.ConfigCompileException; + /** * */ public class ControlFlow { -// @api(environments = LLVMEnvironment.class, platform = api.Platforms.COMPILER_LLVM) -// public static class _if extends LLVMFunction { -// -// @Override -// public boolean usePreExecution() { -// return true; -// } -// -// @Override -// public IRData getIR(Target t, Environment env, Script parent, IRData... nodes) throws ConfigCompileException { -// throw new Error(); -// } -// -// -// @Override -// public IRData preGetIR(Target t, Environment env, Script parent, ParseTree... nodes) throws ConfigCompileException { -// StringBuilder output = new StringBuilder(); -// LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); -// IRData conditionIR = AsmCompiler.getIR(nodes[0], env); -// output.append(conditionIR.getIr()); -// int condition; -// if(conditionIR.getReturnCategory() == IRReturnCategory.VOID) { -// condition = llvmenv.getNewLocalVariableReference(); -// output.append(AsmUtil.formatLine(t, llvmenv, "%" + condition + " = i32 0")); -// } else { -// condition = conditionIR.getResultVariable(); -// } -// -// int firstJmp = llvmenv.getNewLocalVariableReference(); -// int secondJmp = llvmenv.getNewLocalVariableReference(); -// int finalJmp = llvmenv.getNewLocalVariableReference(); -// -// // TODO: Need to potentially (probably?) cast the condition to a i1 -// String jmpLine = "br i1 %" + condition + ", label %" + firstJmp; -// if(nodes.length == 2) { -// jmpLine += ", label %" + finalJmp; -// } else if(nodes.length > 2) { -// jmpLine += ", label %" + secondJmp; -// } -// -// output.append(AsmUtil.formatLine(t, llvmenv, jmpLine)); -// output.append(AsmUtil.formatLine(t, llvmenv, firstJmp + ":")); -// output.append(AsmCompiler.getIR(nodes[1], env).getIr()); -// output.append(AsmUtil.formatLine(t, llvmenv, "br label %" + finalJmp)); -// if(nodes.length > 2) { -// output.append(AsmUtil.formatLine(t, llvmenv, secondJmp + ":")); -// output.append(AsmCompiler.getIR(nodes[2], env).getIr()); -// } -// output.append(AsmUtil.formatLine(t, llvmenv, finalJmp + ":")); -// // TODO This shouldn't return void -// return IRDataBuilder.setRawIR(output.toString()).asVoid(); -// } -// -// @Override -// public String getName() { -// return "if"; -// } -// -// @Override -// public Integer[] numArgs() { -// return new Integer[]{2, 3}; -// } -// -// @Override -// public Class[] thrown() { -// return null; -// } -// -// @Override -// public Version since() { -// return LLVMVersion.V0_0_1; -// } -// -// } + + @api(environments = LLVMEnvironment.class, platform = api.Platforms.COMPILER_LLVM) + public static class ifelse extends LLVMFunction { + + @Override + public IRData buildIR(IRBuilder builder, Target t, Environment env, + GenericParameters generics, ParseTree... nodes) + throws ConfigCompileException { + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + IRBuilder.Gen gen = builder.generator(t, env); + boolean hasElse = nodes.length % 2 == 1; + int numPairs = nodes.length / 2; + + int[] condLabels = new int[numPairs]; + int[] codeLabels = new int[numPairs]; + int[] brCondSlots = new int[numPairs]; + int[] brCodeDoneSlots = new int[numPairs]; + int[] condBools = new int[numPairs]; + + for(int i = 0; i < numPairs; i++) { + // Condition block needs a label so the previous false branch can target it + if(i > 0) { + condLabels[i] = llvmenv.getGotoLabel(); + builder.appendLabel(t, condLabels[i]); + } + + // Evaluate condition and coerce to i1 + IRData condData = LLVMArgumentValidation.getAny(builder, env, nodes[i * 2], t); + condBools[i] = IRCoercion.toBool(builder, t, env, condData); + brCondSlots[i] = builder.reserveLine(t); + + // Code block + codeLabels[i] = llvmenv.getGotoLabel(); + builder.appendLabel(t, codeLabels[i]); + llvmenv.pushVariableScope(); + AsmCompiler.getIR(builder, nodes[i * 2 + 1], env); + llvmenv.popVariableScope(); + brCodeDoneSlots[i] = builder.reserveLine(t); + } + + // Else block + int lblElse = -1; + int brElseDoneSlot = -1; + if(hasElse) { + lblElse = llvmenv.getGotoLabel(); + builder.appendLabel(t, lblElse); + llvmenv.pushVariableScope(); + AsmCompiler.getIR(builder, nodes[nodes.length - 1], env); + llvmenv.popVariableScope(); + brElseDoneSlot = builder.reserveLine(t); + } + + // Merge block + int lblMerge = llvmenv.getGotoLabel(); + builder.appendLabel(t, lblMerge); + + // Fill deferred branches + for(int i = 0; i < numPairs; i++) { + final int codeLabel = codeLabels[i]; + final int cond = condBools[i]; + final int falseLabel; + if(i + 1 < numPairs) { + falseLabel = condLabels[i + 1]; + } else if(hasElse) { + falseLabel = lblElse; + } else { + falseLabel = lblMerge; + } + builder.fillReservedLine(brCondSlots[i], + () -> gen.brCond(cond, codeLabel, falseLabel)); + builder.fillReservedLine(brCodeDoneSlots[i], + () -> gen.br(lblMerge)); + } + if(hasElse) { + builder.fillReservedLine(brElseDoneSlot, + () -> gen.br(lblMerge)); + } + + return IRDataBuilder.asVoid(); + } + + @Override + public String getName() { + return "ifelse"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{Integer.MAX_VALUE}; + } + + @Override + public Class[] thrown() { + return getDefaultFunction().thrown(); + } + + @Override + public Version since() { + return LLVMVersion.V0_0_1; + } + + @Override + public FunctionSignatures getSignatures() { + return getDefaultFunction().getSignatures(); + } + } + + @api(environments = LLVMEnvironment.class, platform = api.Platforms.COMPILER_LLVM) + public static class _if extends LLVMFunction { + + @Override + public IRData buildIR(IRBuilder builder, Target t, Environment env, + GenericParameters generics, ParseTree... nodes) + throws ConfigCompileException { + return new ifelse().buildIR(builder, t, env, generics, nodes); + } + + @Override + public String getName() { + return "if"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2, 3}; + } + + @Override + public Class[] thrown() { + return getDefaultFunction().thrown(); + } + + @Override + public Version since() { + return LLVMVersion.V0_0_1; + } + + @Override + public FunctionSignatures getSignatures() { + return getDefaultFunction().getSignatures(); + } + } } diff --git a/src/main/java/com/laytonsmith/core/functions/asm/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/asm/DataHandling.java index ce8291216..f9176257d 100644 --- a/src/main/java/com/laytonsmith/core/functions/asm/DataHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/asm/DataHandling.java @@ -7,6 +7,7 @@ import com.laytonsmith.core.asm.IRBuilder; import com.laytonsmith.core.asm.IRData; import com.laytonsmith.core.asm.IRDataBuilder; +import com.laytonsmith.core.asm.IRType; import com.laytonsmith.core.asm.LLVMArgumentValidation; import com.laytonsmith.core.asm.LLVMEnvironment; import com.laytonsmith.core.asm.LLVMFunction; @@ -40,7 +41,7 @@ public IRData buildIR(IRBuilder builder, Target t, Environment env, GenericParam throw new CRECastException(getName() + " with 3 arguments only accepts an ivariable as the second argument.", t); } name = ((IVariable) nodes[offset].getData()).getVariableName(); - type = ArgumentValidation.getClassType(nodes[0].getData(), t); + type = ArgumentValidation.getClassType(nodes[0].getData(), t, env); // TODO: Add duplicate check here, or remove if not needed // if(list.has(name) && env.getEnv(GlobalEnv.class).GetFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN) == null) { // if(env.getEnv(GlobalEnv.class).GetFlag(GlobalEnv.FLAG_CLOSURE_WARN_OVERWRITE) != null) { @@ -65,6 +66,10 @@ public IRData buildIR(IRBuilder builder, Target t, Environment env, GenericParam } IRData data = LLVMArgumentValidation.getAny(builder, env, nodes[offset + 1], t); + IRType declaredIRType = LLVMArgumentValidation.convertCClassTypeToIRType(type); + if(declaredIRType == IRType.MS_VALUE && data.getResultType() != IRType.MS_VALUE) { + data = LLVMArgumentValidation.boxToMsValue(builder, t, env, data); + } llvmenv.addVariableMapping(name, data.getResultVariable(), type); return IRDataBuilder.asVoid(); } diff --git a/src/main/java/com/laytonsmith/core/functions/asm/Math.java b/src/main/java/com/laytonsmith/core/functions/asm/Math.java index 89c4fdc4e..00ddd0a06 100644 --- a/src/main/java/com/laytonsmith/core/functions/asm/Math.java +++ b/src/main/java/com/laytonsmith/core/functions/asm/Math.java @@ -14,6 +14,7 @@ import com.laytonsmith.core.asm.LLVMFunction; import com.laytonsmith.core.asm.LLVMVersion; import com.laytonsmith.core.compiler.CompilerEnvironment; +import com.laytonsmith.core.compiler.signature.FunctionSignatures; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.constructs.generics.GenericParameters; import com.laytonsmith.core.environments.Environment; @@ -85,7 +86,7 @@ public IRData buildIR(IRBuilder builder, Target t, Environment env, GenericParam String min; String max; if(nodes[0].isConst()) { - long vMax = ArgumentValidation.getInt(nodes[0].getData(), t); + long vMax = ArgumentValidation.getInt(nodes[0].getData(), t, null); if(vMax > Integer.MAX_VALUE) { throw new ConfigCompileException("max and min must be below int max, defined as " + Integer.MAX_VALUE, t); @@ -110,7 +111,7 @@ public IRData buildIR(IRBuilder builder, Target t, Environment env, GenericParam max = dmax.getReference(); if(nodes[1].isConst()) { - long vMax = ArgumentValidation.getInt(nodes[1].getData(), t); + long vMax = ArgumentValidation.getInt(nodes[1].getData(), t, null); if(vMax > Integer.MAX_VALUE) { throw new ConfigCompileException("max and min must be below int max, defined as " + Integer.MAX_VALUE, t); @@ -159,5 +160,11 @@ public Class[] thrown() { public Version since() { return LLVMVersion.V0_0_1; } + + @Override + public FunctionSignatures getSignatures() { + return getDefaultFunction().getSignatures(); + } + } } diff --git a/src/test/java/com/laytonsmith/core/asm/AsmIntegrationTestUtils.java b/src/test/java/com/laytonsmith/core/asm/AsmIntegrationTestUtils.java index 596db086b..5a60a942c 100644 --- a/src/test/java/com/laytonsmith/core/asm/AsmIntegrationTestUtils.java +++ b/src/test/java/com/laytonsmith/core/asm/AsmIntegrationTestUtils.java @@ -10,6 +10,7 @@ import com.laytonsmith.testing.StaticTest; import java.io.File; +import java.nio.file.Files; import static org.junit.Assert.assertEquals; @@ -73,12 +74,11 @@ public static File installProgram(String program) throws Exception { if(skipTest()) { throw new UnsupportedOperationException(); } - File f = File.createTempFile("methodScriptAsmIntegrationTest", ".ms"); + File tempDir = Files.createTempDirectory("methodScriptAsmIntegrationTest").toFile(); + File f = new File(tempDir, "program.ms"); FileUtil.write(program, f); - f.deleteOnExit(); - File target = new File(f.getParentFile(), "target"); + File target = new File(tempDir, "target"); target.mkdir(); - target.deleteOnExit(); return f; } @@ -95,6 +95,7 @@ public static void compileFile(File file) throws Exception { throw new UnsupportedOperationException(); } AsmMainCmdlineTool tool = new AsmMain.AsmMainCmdlineTool(); + tool.setThrowOnError(true); tool.execute(tool.getArgumentParser().match("\"" + file.getAbsolutePath() + "\" -o \"" + new File(file.getParentFile(), "target").getCanonicalPath() + "\" --verbose")); } @@ -151,6 +152,7 @@ public static String executeProgram(File file, String... args) throws Exception * @param args Any arguments to pass to the program on the command line. * @return */ + public static void integrationTest(String expected, String program, String... args) throws Exception { if(skipTest()) { return; @@ -179,12 +181,16 @@ public static String integrationTestAndReturn(String program, String... args) th } installToolchain(); File msFile = installProgram(program); - compileFile(msFile); - File exe = getExecutableFromMSFile(msFile); - String output = executeProgram(exe, args); - if(output.endsWith(OSUtils.GetLineEnding())) { - output = StringUtils.replaceLast(output, OSUtils.GetLineEnding(), ""); + try { + compileFile(msFile); + File exe = getExecutableFromMSFile(msFile); + String output = executeProgram(exe, args); + if(output.endsWith(OSUtils.GetLineEnding())) { + output = StringUtils.replaceLast(output, OSUtils.GetLineEnding(), ""); + } + return output; + } finally { + FileUtil.recursiveDelete(msFile.getParentFile()); } - return output; } } diff --git a/src/test/java/com/laytonsmith/core/asm/BasicLogicIntegrationTests.java b/src/test/java/com/laytonsmith/core/asm/BasicLogicIntegrationTests.java new file mode 100644 index 000000000..7068794a3 --- /dev/null +++ b/src/test/java/com/laytonsmith/core/asm/BasicLogicIntegrationTests.java @@ -0,0 +1,96 @@ +package com.laytonsmith.core.asm; + +import com.laytonsmith.testing.AbstractIntegrationTest; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import static com.laytonsmith.core.asm.AsmIntegrationTestUtils.integrationTest; + +public class BasicLogicIntegrationTests extends AbstractIntegrationTest { + + @Test + public void testBoolToString() throws Exception { + List> cases = List.of( + Map.entry("true", " boolean @a = true; sys_out(@a);"), + Map.entry("false", " boolean @a = false; sys_out(@a);") + ); + for(Map.Entry entry : cases) { + integrationTest(entry.getKey(), entry.getValue()); + } + } + + @Test + public void testEqualsConcreteTypes() throws Exception { + List> cases = List.of( + // int == int + Map.entry("true", " sys_out(dyn(1) == dyn(1));"), + Map.entry("false", " sys_out(dyn(1) == dyn(2));"), + // double == double + Map.entry("true", " sys_out(dyn(1.5) == dyn(1.5));"), + Map.entry("false", " sys_out(dyn(1.5) == dyn(2.5));"), + // int == double (numeric coercion) + Map.entry("true", " sys_out(dyn(1) == dyn(1.0));"), + Map.entry("false", " sys_out(dyn(1) == dyn(1.5));"), + // string == string + Map.entry("true", " sys_out(dyn('hello') == dyn('hello'));"), + Map.entry("false", " sys_out(dyn('hello') == dyn('world'));"), + // boolean == boolean + Map.entry("true", " sys_out(dyn(true) == dyn(true));"), + Map.entry("false", " sys_out(dyn(true) == dyn(false));") + ); + for(Map.Entry entry : cases) { + integrationTest(entry.getKey(), entry.getValue()); + } + } + + @Test + public void testEqualsVariadic() throws Exception { + List> cases = List.of( + Map.entry("true", " sys_out(equals(dyn(1), dyn(1), dyn(1)));"), + Map.entry("false", " sys_out(equals(dyn(1), dyn(1), dyn(2)));") + ); + for(Map.Entry entry : cases) { + integrationTest(entry.getKey(), entry.getValue()); + } + } + + @Test + public void testEqualsAutoTypes() throws Exception { + List> cases = List.of( + // auto int == auto int + Map.entry("true", " auto @a = dyn(5); auto @b = dyn(5); sys_out(@a == @b);"), + Map.entry("false", " auto @a = dyn(5); auto @b = dyn(6); sys_out(@a == @b);"), + // auto double == auto double + Map.entry("true", " auto @a = dyn(3.14); auto @b = dyn(3.14); sys_out(@a == @b);"), + Map.entry("false", " auto @a = dyn(3.14); auto @b = dyn(2.71); sys_out(@a == @b);"), + // auto vs concrete + Map.entry("true", " auto @a = dyn(42); int @b = dyn(42); sys_out(@a == @b);"), + Map.entry("false", " auto @a = dyn(42); int @b = dyn(43); sys_out(@a == @b);") + ); + for(Map.Entry entry : cases) { + integrationTest(entry.getKey(), entry.getValue()); + } + } + + @Test + public void testEqualsBoolStringCoercion() throws Exception { + List> cases = List.of( + // Concrete types: bool + string, boolean priority + Map.entry("true", " sys_out(dyn(true) == dyn('hello'));"), + Map.entry("false", " sys_out(dyn(false) == dyn('hello'));"), + Map.entry("true", " sys_out(dyn(false) == dyn(''));"), + Map.entry("false", " sys_out(dyn(true) == dyn(''));"), + // Auto types: bool + string, ms_value dispatch + Map.entry("true", " auto @a = dyn(true); auto @b = dyn('hello'); sys_out(@a == @b);"), + Map.entry("false", " auto @a = dyn(false); auto @b = dyn('hello'); sys_out(@a == @b);"), + Map.entry("true", " auto @a = dyn(false); auto @b = dyn(''); sys_out(@a == @b);"), + Map.entry("false", " auto @a = dyn(true); auto @b = dyn(''); sys_out(@a == @b);") + ); + for(Map.Entry entry : cases) { + integrationTest(entry.getKey(), entry.getValue()); + } + } +} diff --git a/src/test/java/com/laytonsmith/core/asm/ControlFlowIntegrationTests.java b/src/test/java/com/laytonsmith/core/asm/ControlFlowIntegrationTests.java new file mode 100644 index 000000000..bdf6b59e7 --- /dev/null +++ b/src/test/java/com/laytonsmith/core/asm/ControlFlowIntegrationTests.java @@ -0,0 +1,81 @@ +package com.laytonsmith.core.asm; + +import com.laytonsmith.PureUtilities.Common.OSUtils; +import com.laytonsmith.testing.AbstractIntegrationTest; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import static com.laytonsmith.core.asm.AsmIntegrationTestUtils.integrationTest; + +public class ControlFlowIntegrationTests extends AbstractIntegrationTest { + + @Test + public void testIfTrue() throws Exception { + integrationTest("yes", + " if(dyn(true)) { sys_out('yes'); }"); + } + + @Test + public void testIfFalse() throws Exception { + integrationTest("done", + " if(dyn(false)) { sys_out('no'); } sys_out('done');"); + } + + @Test + public void testIfElse() throws Exception { + integrationTest("false branch", + " if(dyn(false)) { sys_out('true branch'); } else { sys_out('false branch'); }"); + } + + @Test + public void testIfElseTrue() throws Exception { + integrationTest("true branch", + " if(dyn(true)) { sys_out('true branch'); } else { sys_out('false branch'); }"); + } + + @Test + public void testIfWithIntCondition() throws Exception { + integrationTest("nonzero", + " int @x = dyn(42); if(@x) { sys_out('nonzero'); } else { sys_out('zero'); }"); + } + + @Test + public void testIfWithZeroCondition() throws Exception { + integrationTest("zero", + " int @x = dyn(0); if(@x) { sys_out('nonzero'); } else { sys_out('zero'); }"); + } + + @Test + public void testIfElseChain() throws Exception { + integrationTest("second", + " if(dyn(false)) { sys_out('first'); }" + + " else if(dyn(true)) { sys_out('second'); }" + + " else { sys_out('third'); }"); + } + + @Test + public void testIfElseChainFallthrough() throws Exception { + integrationTest("third", + " if(dyn(false)) { sys_out('first'); }" + + " else if(dyn(false)) { sys_out('second'); }" + + " else { sys_out('third'); }"); + } + + @Test + public void testIfWithAssign() throws Exception { + String expected = "inside" + OSUtils.GetLineEnding() + "after"; + integrationTest(expected, + " int @x = dyn(1);" + + " if(@x) { sys_out('inside'); }" + + " sys_out('after');"); + } + + @Test + public void testNestedIf() throws Exception { + integrationTest("inner", + " if(dyn(true)) { if(dyn(true)) { sys_out('inner'); } else { sys_out('nope'); } }"); + } +} diff --git a/src/test/java/com/laytonsmith/core/asm/LLVMArgumentValidationTest.java b/src/test/java/com/laytonsmith/core/asm/LLVMArgumentValidationTest.java new file mode 100644 index 000000000..51179d048 --- /dev/null +++ b/src/test/java/com/laytonsmith/core/asm/LLVMArgumentValidationTest.java @@ -0,0 +1,88 @@ +package com.laytonsmith.core.asm; + +import com.laytonsmith.core.Static; +import com.laytonsmith.core.constructs.CBoolean; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.CDouble; +import com.laytonsmith.core.constructs.CInt; +import com.laytonsmith.core.constructs.CString; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.CommandHelperEnvironment; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.testing.AbstractIntegrationTest; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class LLVMArgumentValidationTest extends AbstractIntegrationTest { + + Environment env; + + @Before + public void setUp() throws Exception { + env = Static.GenerateStandaloneEnvironment(); + env = env.cloneAndAdd(new CommandHelperEnvironment(), new LLVMEnvironment()); + LLVMEnvironment llvmenv = env.getEnv(LLVMEnvironment.class); + llvmenv.newMethodFrame("test"); + } + + @Test + public void testConvertCClassTypeToIRType() { + Map expected = Map.of( + CInt.TYPE, IRType.INTEGER64, + CDouble.TYPE, IRType.DOUBLE, + CString.TYPE, IRType.STRING, + CBoolean.TYPE, IRType.INTEGER1, + CClassType.AUTO, IRType.MS_VALUE + ); + for(Map.Entry entry : expected.entrySet()) { + assertEquals("Mapping for " + entry.getKey(), + entry.getValue(), + LLVMArgumentValidation.convertCClassTypeToIRType(entry.getKey())); + } + } + + @Test + public void testBoxTagsAreUnique() { + Set seen = new HashSet<>(); + for(IRType type : IRType.values()) { + if(type.isBoxable()) { + assertTrue("Duplicate box tag " + type.getBoxTag() + " on " + type, + seen.add(type.getBoxTag())); + } + } + } + + @Test + public void testNonBoxableTypes() { + IRType[] nonBoxable = { + IRType.INTEGER8POINTER, IRType.INTEGER8POINTERPOINTER, + IRType.VOID, IRType.MS_VALUE, IRType.MS_VALUE_PTR, IRType.OTHER + }; + for(IRType type : nonBoxable) { + assertFalse(type + " should not be boxable", type.isBoxable()); + } + } + + @Test + public void testEmitGetTagEmitsExtractvalue() { + IRBuilder builder = new IRBuilder(); + IRData result = LLVMArgumentValidation.emitGetTag(builder, Target.UNKNOWN, env, 5); + assertEquals(IRType.INTEGER8, result.getResultType()); + boolean found = false; + for(String line : builder.lines) { + if(line.contains("extractvalue { i8, i64 } %5, 0")) { + found = true; + break; + } + } + assertTrue("Expected extractvalue instruction for tag extraction", found); + } +}