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