From 9ae42c41b13f7aebf89dd532511d8c45973f97b9 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Fri, 26 Sep 2025 05:59:06 +0200
Subject: [PATCH 01/12] Add __cast__ function and syntax
Add `__cast__(val, type)` function and syntax. Syntax is `(type) val`.
---
.../laytonsmith/core/functions/Compiler.java | 184 +++++++++++++++---
1 file changed, 156 insertions(+), 28 deletions(-)
diff --git a/src/main/java/com/laytonsmith/core/functions/Compiler.java b/src/main/java/com/laytonsmith/core/functions/Compiler.java
index 8e553e1f7..19cce448e 100644
--- a/src/main/java/com/laytonsmith/core/functions/Compiler.java
+++ b/src/main/java/com/laytonsmith/core/functions/Compiler.java
@@ -6,6 +6,7 @@
import com.laytonsmith.annotations.hide;
import com.laytonsmith.annotations.noboilerplate;
import com.laytonsmith.annotations.noprofile;
+import com.laytonsmith.core.ArgumentValidation;
import com.laytonsmith.core.FullyQualifiedClassName;
import com.laytonsmith.core.MSVersion;
import com.laytonsmith.core.Optimizable;
@@ -13,6 +14,8 @@
import com.laytonsmith.core.Script;
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.compiler.analysis.StaticAnalysis;
+import com.laytonsmith.core.compiler.signature.FunctionSignatures;
+import com.laytonsmith.core.compiler.signature.SignatureBuilder;
import com.laytonsmith.core.constructs.CBareString;
import com.laytonsmith.core.constructs.CBracket;
import com.laytonsmith.core.constructs.CClassType;
@@ -26,6 +29,7 @@
import com.laytonsmith.core.constructs.CVoid;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.IVariable;
+import com.laytonsmith.core.constructs.InstanceofUtil;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.constructs.Token;
import com.laytonsmith.core.environments.Environment;
@@ -187,27 +191,32 @@ public static ParseTree rewrite(List list, boolean returnSConcat,
//If any of our nodes are CSymbols, we have different behavior
boolean inSymbolMode = false; //caching this can save Xn
+ // Rewrite execute operator.
rewriteParenthesis(list);
- //Assignment
- //Note that we are walking the array in reverse, because multiple assignments,
- //say @a = @b = 1 will break if they look like assign(assign(@a, @b), 1),
- //they need to be assign(@a, assign(@b, 1)). As a variation, we also have
- //to support something like 1 + @a = 2, which will turn into add(1, assign(@a, 2),
- //and 1 + @a = @b + 3 would turn into add(1, assign(@a, add(@b, 3))).
+ // Rewrite assignment operator.
+ /*
+ * Note that we are walking the array in reverse, because multiple assignments,
+ * say @a = @b = 1 will break if they look like assign(assign(@a, @b), 1),
+ * they need to be assign(@a, assign(@b, 1)). As a variation, we also have
+ * to support something like 1 + @a = 2, which will turn into add(1, assign(@a, 2),
+ * and 1 + @a = @b + 3 would turn into add(1, assign(@a, add(@b, 3))).
+ */
for(int i = list.size() - 2; i >= 0; i--) {
ParseTree node = list.get(i + 1);
if(node.getData() instanceof CSymbol && ((CSymbol) node.getData()).isAssignment()) {
+
+ // Get assign left hand side and autoconcat assign right hand side if necessary.
ParseTree lhs = list.get(i);
- ParseTree assignNode = new ParseTree(
- new CFunction(assign.NAME, node.getTarget()), node.getFileOptions());
- ParseTree rhs;
if(i < list.size() - 3) {
- //Need to autoconcat
List valChildren = new ArrayList<>();
int index = i + 2;
// add all preceding symbols
- while(list.size() > index + 1 && list.get(index).getData() instanceof CSymbol) {
+ while(list.size() > index + 1 && (list.get(index).getData() instanceof CSymbol
+ || (list.get(index).getData() instanceof CFunction cf
+ && cf.hasFunction() && cf.getFunction() != null
+ && cf.getFunction().getName().equals(Compiler.p.NAME)
+ && list.get(index).numberOfChildren() == 1))) {
valChildren.add(list.get(index));
list.remove(index);
}
@@ -237,26 +246,31 @@ public static ParseTree rewrite(List list, boolean returnSConcat,
if(list.size() <= i + 2) {
throw new ConfigCompileException("Unexpected end of statement", list.get(i).getTarget());
}
+ ParseTree rhs = list.get(i + 2);
- // Additive assignment
+ // Wrap additive assignment in right hand side (e.g. convert @a += 1 to @a = @a + 1).
CSymbol sy = (CSymbol) node.getData();
String conversionFunction = sy.convertAssignment();
if(conversionFunction != null) {
- ParseTree conversion = new ParseTree(new CFunction(conversionFunction, node.getTarget()), node.getFileOptions());
- conversion.addChild(lhs);
- conversion.addChild(list.get(i + 2));
- list.set(i + 2, conversion);
+ ParseTree rhsReplacement = new ParseTree(
+ new CFunction(conversionFunction, node.getTarget()), node.getFileOptions());
+ rhsReplacement.addChild(lhs);
+ rhsReplacement.addChild(rhs);
+ rhs = rhsReplacement;
}
- rhs = list.get(i + 2);
+ // Rewrite to assign node.
+ ParseTree assignNode = new ParseTree(
+ new CFunction(assign.NAME, node.getTarget()), node.getFileOptions());
assignNode.addChild(lhs);
assignNode.addChild(rhs);
- list.set(i, assignNode);
- list.remove(i + 1);
- list.remove(i + 1);
+ list.set(i, assignNode); // Overwrite lhs with assign node.
+ list.remove(i + 1); // Remove "=" node.
+ list.remove(i + 1); // Remove rhs node.
}
}
- //postfix
+
+ // Rewrite postfix operators.
for(int i = 0; i < list.size(); i++) {
ParseTree node = list.get(i);
if(node.getData() instanceof CSymbol) {
@@ -280,9 +294,10 @@ public static ParseTree rewrite(List list, boolean returnSConcat,
}
}
}
+
+ // Rewrite unary operators.
if(inSymbolMode) {
try {
- //look for unary operators
for(int i = 0; i < list.size() - 1; i++) {
ParseTree node = list.get(i);
if(node.getData() instanceof CSymbol && ((CSymbol) node.getData()).isUnary()) {
@@ -326,6 +341,44 @@ public static ParseTree rewrite(List list, boolean returnSConcat,
conversion.addChild(rewrite(ac, returnSConcat, envs));
}
}
+ } catch (IndexOutOfBoundsException e) {
+ throw new ConfigCompileException("Unexpected symbol (" + list.get(list.size() - 1).getData().val() + ")",
+ list.get(list.size() - 1).getTarget());
+ }
+ }
+
+ // Rewrite cast operator.
+ for(int i = list.size() - 2; i >= 0; i--) {
+ ParseTree node = list.get(i);
+ if(node.getData() instanceof CFunction cf && cf.hasFunction() && cf.getFunction() != null
+ && cf.getFunction().getName().equals(Compiler.p.NAME) && node.numberOfChildren() == 1) {
+
+ // Convert bare string or concat() to type reference if needed.
+ ParseTree typeNode = node.getChildAt(0);
+ if(!typeNode.getData().isInstanceOf(CClassType.TYPE)) {
+ ParseTree convertedTypeNode = __type_ref__.createFromBareStringOrConcats(typeNode);
+ if(convertedTypeNode != null) {
+ typeNode = convertedTypeNode;
+ } else {
+
+ // This is not a "(classtype)" format. Skip node.
+ continue;
+ }
+ }
+
+ // Rewrite p(A) and the next list entry B to __cast__(B, A).
+ ParseTree castNode = new ParseTree(
+ new CFunction(__cast__.NAME, node.getTarget()), node.getFileOptions());
+ castNode.addChild(list.get(i + 1));
+ castNode.addChild(typeNode);
+ list.set(i, castNode);
+ list.remove(i + 1);
+ }
+ }
+
+ // Rewrite binary operators.
+ if(inSymbolMode) {
+ try {
//Exponential
for(int i = 0; i < list.size() - 1; i++) {
@@ -586,18 +639,30 @@ private static void rewriteParenthesis(List list) throws ConfigCompil
for(int listInd = list.size() - 1; listInd >= 1; listInd--) {
Stack executes = new Stack<>();
while(listInd > 0) {
- ParseTree lastNode = list.get(listInd);
+ ParseTree node = list.get(listInd);
try {
- if(lastNode.getData() instanceof CFunction cf
+ if(node.getData() instanceof CFunction cf
&& cf.hasFunction()
&& cf.getFunction() != null
&& cf.getFunction().getName().equals(Compiler.p.NAME)) {
- Mixed prevNode = list.get(listInd - 1).getData();
- if(prevNode instanceof CSymbol || prevNode instanceof CLabel || prevNode instanceof CString) {
- // It's just a parenthesis like @a = (1); or key: (value), so we should leave it alone.
+ ParseTree prevNode = list.get(listInd - 1);
+ Mixed prevNodeVal = prevNode.getData();
+
+ // Do not rewrite parenthesis like "@a = (1);" or "key: (value)" to execute().
+ if(prevNodeVal instanceof CSymbol
+ || prevNodeVal instanceof CLabel || prevNodeVal instanceof CString) {
break;
}
- executes.push(lastNode);
+
+ // Do not rewrite casts to execute() if the callable is the cast (i.e. "(type) (val)").
+ if(prevNodeVal instanceof CFunction cfunc && cfunc.hasFunction() && cfunc.getFunction() != null
+ && cfunc.getFunction().getName().equals(Compiler.p.NAME) && prevNode.numberOfChildren() == 1
+ && (prevNode.getChildAt(0).getData().isInstanceOf(CClassType.TYPE)
+ || __type_ref__.createFromBareStringOrConcats(prevNode.getChildAt(0)) != null)) {
+ break;
+ }
+
+ executes.push(node);
list.remove(listInd--);
} else {
break;
@@ -1136,4 +1201,67 @@ public ParseTree postParseRewrite(ParseTree ast, Environment env,
}
}
}
+ @api
+ @noprofile
+ @hide("This is only used internally by the compiler.")
+ public static class __cast__ extends DummyFunction {
+
+ public static final String NAME = "__cast__";
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public FunctionSignatures getSignatures() {
+ return new SignatureBuilder(CClassType.AUTO)
+ .param(Mixed.TYPE, "value", "The value.")
+ .param(CClassType.TYPE, "type", "The type.")
+ .throwsEx(CRECastException.class, "When value cannot be cast to type.")
+ .build();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Class extends CREThrowable>[] thrown() {
+ return new Class[] {CRECastException.class};
+ }
+
+ @Override
+ public Integer[] numArgs() {
+ return new Integer[] {2};
+ }
+
+ @Override
+ public String docs() {
+ return "mixed {mixed value, ClassType type} Used internally by the compiler. You shouldn't use it.";
+ }
+
+ @Override
+ public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException {
+ Mixed value = args[0];
+ CClassType type = ArgumentValidation.getClassType(args[1], t);
+ if(!InstanceofUtil.isInstanceof(value, type, env)) {
+ throw new CRECastException(
+ "Cannot cast from " + value.typeof().getSimpleName() + " to " + type.getSimpleName() + ".", t);
+ }
+ // TODO - Perform runtime conversion to 'type' when necessary (cross-cast handling).
+ return value;
+ }
+
+ @Override
+ public CClassType typecheck(StaticAnalysis analysis,
+ ParseTree ast, Environment env, Set exceptions) {
+
+ // Typecheck children and validate function signature through super call.
+ super.typecheck(analysis, ast, env, exceptions);
+
+ // Return type that is being cast to.
+ if(ast.numberOfChildren() != 2 || !(ast.getChildAt(1).getData() instanceof CClassType)) {
+ return CClassType.AUTO;
+ }
+ return (CClassType) ast.getChildAt(1).getData();
+ }
+ }
}
From ba7f833070cd540febbcaf2a785c0a7e09f5da0c Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Thu, 16 Oct 2025 02:05:21 +0200
Subject: [PATCH 02/12] Warn on casts to same type + Error on impossible casts
---
.../laytonsmith/core/functions/Compiler.java | 41 ++++++++++++++++---
1 file changed, 35 insertions(+), 6 deletions(-)
diff --git a/src/main/java/com/laytonsmith/core/functions/Compiler.java b/src/main/java/com/laytonsmith/core/functions/Compiler.java
index 19cce448e..b230de6e0 100644
--- a/src/main/java/com/laytonsmith/core/functions/Compiler.java
+++ b/src/main/java/com/laytonsmith/core/functions/Compiler.java
@@ -12,6 +12,8 @@
import com.laytonsmith.core.Optimizable;
import com.laytonsmith.core.ParseTree;
import com.laytonsmith.core.Script;
+import com.laytonsmith.core.compiler.CompilerEnvironment;
+import com.laytonsmith.core.compiler.CompilerWarning;
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.compiler.analysis.StaticAnalysis;
import com.laytonsmith.core.compiler.signature.FunctionSignatures;
@@ -1246,7 +1248,6 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntime
throw new CRECastException(
"Cannot cast from " + value.typeof().getSimpleName() + " to " + type.getSimpleName() + ".", t);
}
- // TODO - Perform runtime conversion to 'type' when necessary (cross-cast handling).
return value;
}
@@ -1254,14 +1255,42 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntime
public CClassType typecheck(StaticAnalysis analysis,
ParseTree ast, Environment env, Set exceptions) {
- // Typecheck children and validate function signature through super call.
- super.typecheck(analysis, ast, env, exceptions);
+ // Fall back to default behavior for invalid usage.
+ if(ast.numberOfChildren() != 2) {
+ return super.typecheck(analysis, ast, env, exceptions);
+ }
- // Return type that is being cast to.
- if(ast.numberOfChildren() != 2 || !(ast.getChildAt(1).getData() instanceof CClassType)) {
+ // Typecheck value and type nodes.
+ ParseTree valNode = ast.getChildAt(0);
+ CClassType valType = analysis.typecheck(valNode, env, exceptions);
+ StaticAnalysis.requireType(valType, Mixed.TYPE, valType.getTarget(), env, exceptions);
+ ParseTree typeNode = ast.getChildAt(1);
+ CClassType typeType = analysis.typecheck(typeNode, env, exceptions);
+ StaticAnalysis.requireType(typeType, CClassType.TYPE, typeNode.getTarget(), env, exceptions);
+
+ // Get cast-to type.
+ if(!(typeNode.getData() instanceof CClassType)) {
+ assert !exceptions.isEmpty() : "Missing compile-time type error for cast type argument.";
return CClassType.AUTO;
}
- return (CClassType) ast.getChildAt(1).getData();
+ CClassType castToType = (CClassType) typeNode.getData();
+
+ // Generate redundancy warning for casts to the value type.
+ if(castToType.equals(valType)) {
+ env.getEnv(CompilerEnvironment.class).addCompilerWarning(ast.getFileOptions(),
+ new CompilerWarning("Redundant cast to " + castToType.getSimpleName(), ast.getTarget(),
+ FileOptions.SuppressWarning.UselessCode));
+ }
+
+ // Generate compile error for impossible casts.
+ if(!InstanceofUtil.isInstanceof(valType, castToType, env)
+ && !InstanceofUtil.isInstanceof(castToType, valType, env)) {
+ exceptions.add(new ConfigCompileException("Cannot cast from "
+ + valType.getSimpleName() + " to " + castToType.getSimpleName() + ".", ast.getTarget()));
+ }
+
+ // Return type that is being cast to.
+ return castToType;
}
}
}
From 471b5c9447a899a7c641753af56a0d2baedb091a Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Fri, 17 Oct 2025 02:56:08 +0200
Subject: [PATCH 03/12] Optimize __cast__()
- Mark `__cast__()` for constant and cached returns.
- Remove nested casts where the second executed cast is removed if the first executed cast passing ensures that the second executed cast will pass.
---
.../laytonsmith/core/functions/Compiler.java | 34 ++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/laytonsmith/core/functions/Compiler.java b/src/main/java/com/laytonsmith/core/functions/Compiler.java
index b230de6e0..f350c52dc 100644
--- a/src/main/java/com/laytonsmith/core/functions/Compiler.java
+++ b/src/main/java/com/laytonsmith/core/functions/Compiler.java
@@ -12,6 +12,7 @@
import com.laytonsmith.core.Optimizable;
import com.laytonsmith.core.ParseTree;
import com.laytonsmith.core.Script;
+import com.laytonsmith.core.Optimizable.OptimizationOption;
import com.laytonsmith.core.compiler.CompilerEnvironment;
import com.laytonsmith.core.compiler.CompilerWarning;
import com.laytonsmith.core.compiler.FileOptions;
@@ -35,6 +36,7 @@
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.constructs.Token;
import com.laytonsmith.core.environments.Environment;
+import com.laytonsmith.core.environments.Environment.EnvironmentImpl;
import com.laytonsmith.core.exceptions.CRE.CRECastException;
import com.laytonsmith.core.exceptions.CRE.CRENotFoundException;
import com.laytonsmith.core.exceptions.CRE.CREThrowable;
@@ -1206,7 +1208,7 @@ public ParseTree postParseRewrite(ParseTree ast, Environment env,
@api
@noprofile
@hide("This is only used internally by the compiler.")
- public static class __cast__ extends DummyFunction {
+ public static class __cast__ extends DummyFunction implements Optimizable {
public static final String NAME = "__cast__";
@@ -1292,5 +1294,35 @@ public CClassType typecheck(StaticAnalysis analysis,
// Return type that is being cast to.
return castToType;
}
+
+ @Override
+ public Set optimizationOptions() {
+ return EnumSet.of(
+ OptimizationOption.OPTIMIZE_DYNAMIC,
+ OptimizationOption.CONSTANT_OFFLINE,
+ OptimizationOption.CACHE_RETURN
+ );
+ }
+
+ @Override
+ public ParseTree optimizeDynamic(Target t, Environment env, Set> envs,
+ List children, FileOptions fileOptions)
+ throws ConfigCompileException, ConfigRuntimeException, ConfigCompileGroupException {
+
+ // Optimize __cast__(__cast__(val, type1), type2) to __cast__(val, type1) if the cast to type2 will always
+ // pass given that the cast to type1 has passed.
+ ParseTree valNode = children.get(0);
+ if(valNode.getData() instanceof CFunction cf && cf.getCachedFunction() != null
+ && cf.getCachedFunction().getName().equals(__cast__.NAME) && valNode.numberOfChildren() == 2) {
+ ParseTree typeNode = children.get(1);
+ ParseTree childTypeNode = valNode.getChildAt(1);
+ if(typeNode.getData() instanceof CClassType type
+ && childTypeNode.getData() instanceof CClassType childType
+ && InstanceofUtil.isInstanceof(childType, type, env)) {
+ return valNode;
+ }
+ }
+ return null;
+ }
}
}
From beb88975c616d8416f1f602fd634d41c112c24f2 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Fri, 17 Oct 2025 03:10:55 +0200
Subject: [PATCH 04/12] Fix unknown Target for sugared __cast__() syntax
---
src/main/java/com/laytonsmith/core/MethodScriptCompiler.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java b/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java
index 8e810e72b..bf1655b23 100644
--- a/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java
+++ b/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java
@@ -1579,7 +1579,7 @@ public static ParseTree compile(TokenStream stream, Environment environment,
environment.getEnv(CompilerEnvironment.class).addCompilerWarning(fileOptions, warning);
f = new ParseTree(new CFunction(Compiler.__autoconcat__.NAME, unknown), fileOptions);
} else {
- f = new ParseTree(new CFunction(Compiler.p.NAME, unknown), fileOptions);
+ f = new ParseTree(new CFunction(Compiler.p.NAME, t.getTarget()), fileOptions);
}
constructCount.push(new AtomicInteger(0));
tree.addChild(f);
From 02a96f1ccf8509dc0df56765eba017f79cefc0da Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Sun, 19 Oct 2025 07:12:07 +0200
Subject: [PATCH 05/12] Optimize assign() to __unsafe_assign__ ()
Optimize `assign()` to `__unsafe_assign__ ()` when it is known that the `assign()` typecheck will always pass.
---
.../core/constructs/IVariableList.java | 4 ++
.../laytonsmith/core/functions/Compiler.java | 53 +++++++++++++++
.../core/functions/DataHandling.java | 64 +++++++++++++++----
.../core/functions/Exceptions.java | 5 +-
.../core/NewExceptionHandlingTest.java | 4 +-
.../laytonsmith/core/OptimizationTest.java | 16 ++---
6 files changed, 121 insertions(+), 25 deletions(-)
diff --git a/src/main/java/com/laytonsmith/core/constructs/IVariableList.java b/src/main/java/com/laytonsmith/core/constructs/IVariableList.java
index ee9cc597a..b27bf0a18 100644
--- a/src/main/java/com/laytonsmith/core/constructs/IVariableList.java
+++ b/src/main/java/com/laytonsmith/core/constructs/IVariableList.java
@@ -79,6 +79,10 @@ public void set(IVariable v) {
varList.put(v.getVariableName(), v);
}
+ public IVariable get(String name) {
+ return varList.get(name);
+ }
+
public IVariable get(String name, Target t, boolean bypassAssignedCheck, Environment env) {
IVariable v = varList.get(name);
if(v == null) {
diff --git a/src/main/java/com/laytonsmith/core/functions/Compiler.java b/src/main/java/com/laytonsmith/core/functions/Compiler.java
index f350c52dc..208e1800d 100644
--- a/src/main/java/com/laytonsmith/core/functions/Compiler.java
+++ b/src/main/java/com/laytonsmith/core/functions/Compiler.java
@@ -32,10 +32,12 @@
import com.laytonsmith.core.constructs.CVoid;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.IVariable;
+import com.laytonsmith.core.constructs.IVariableList;
import com.laytonsmith.core.constructs.InstanceofUtil;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.constructs.Token;
import com.laytonsmith.core.environments.Environment;
+import com.laytonsmith.core.environments.GlobalEnv;
import com.laytonsmith.core.environments.Environment.EnvironmentImpl;
import com.laytonsmith.core.exceptions.CRE.CRECastException;
import com.laytonsmith.core.exceptions.CRE.CRENotFoundException;
@@ -1205,6 +1207,7 @@ public ParseTree postParseRewrite(ParseTree ast, Environment env,
}
}
}
+
@api
@noprofile
@hide("This is only used internally by the compiler.")
@@ -1325,4 +1328,54 @@ public ParseTree optimizeDynamic(Target t, Environment env, Set
Date: Sun, 19 Oct 2025 17:30:20 +0200
Subject: [PATCH 06/12] Reduce assign() runtime actions
- Do not redefine variables in variable list when not necessary.
- Unwrap IVariable values only once.
- Directly create new IVariable with correct values when necessary.
---
.../core/functions/DataHandling.java | 93 +++++++++++++------
1 file changed, 67 insertions(+), 26 deletions(-)
diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
index ecb94e372..4cbe9d68f 100644
--- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java
+++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
@@ -342,26 +342,27 @@ public Integer[] numArgs() {
@Override
public Mixed exec(Target t, Environment env, Mixed... args) throws CancelCommandException, ConfigRuntimeException {
IVariableList list = env.getEnv(GlobalEnv.class).GetVarList();
- int offset;
- CClassType type;
- String name;
+ IVariable var;
if(args.length == 3) {
- offset = 1;
- if(!(args[offset] instanceof IVariable)) {
+
+ // Get and validate variable name.
+ if(!(args[1] instanceof IVariable)) {
throw new CRECastException(getName() + " with 3 arguments only accepts an ivariable as the second argument.", t);
}
- name = ((IVariable) args[offset]).getVariableName();
- if(list.has(name) && env.getEnv(GlobalEnv.class).GetFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN) == null) {
+ String varName = ((IVariable) args[1]).getVariableName();
+ if(list.has(varName) && env.getEnv(GlobalEnv.class).GetFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN) == null) {
if(env.getEnv(GlobalEnv.class).GetFlag(GlobalEnv.FLAG_CLOSURE_WARN_OVERWRITE) != null) {
MSLog.GetLogger().Log(MSLog.Tags.RUNTIME, LogLevel.WARNING,
- "The variable " + name + " is hiding another value of the"
+ "The variable " + varName + " is hiding another value of the"
+ " same name in the main scope.", t);
- } else if(!StaticAnalysis.enabled() && t != list.get(name, t, true, env).getDefinedTarget()) {
- MSLog.GetLogger().Log(MSLog.Tags.RUNTIME, LogLevel.ERROR, name + " was already defined at "
- + list.get(name, t, true, env).getDefinedTarget() + " but is being redefined.", t);
+ } else if(!StaticAnalysis.enabled() && t != list.get(varName, t, true, env).getDefinedTarget()) {
+ MSLog.GetLogger().Log(MSLog.Tags.RUNTIME, LogLevel.ERROR, varName + " was already defined at "
+ + list.get(varName, t, true, env).getDefinedTarget() + " but is being redefined.", t);
}
}
- type = ArgumentValidation.getClassType(args[0], t);
+
+ // Get and validate variable type.
+ CClassType type = ArgumentValidation.getClassType(args[0], t);
Boolean varArgsAllowed = env.getEnv(GlobalEnv.class).GetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED);
if(varArgsAllowed == null) {
varArgsAllowed = false;
@@ -369,24 +370,64 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws CancelCommand
if(type.isVarargs() && !varArgsAllowed) {
throw new CRECastException("Cannot use varargs type in this context", t);
}
+ if(type.equals(CVoid.TYPE)) {
+ throw new CRECastException("Variables may not be of type void", t);
+ }
+
+ // Get assigned value.
+ Mixed val = args[2];
+
+ // Unwrap assigned value from IVariable if an IVariable is passed.
+ if(val instanceof IVariable ivar) {
+ val = list.get(ivar.getVariableName(), ivar.getTarget(), env).ival();
+ }
+
+ // Validate assigned value.
+ if(val instanceof CVoid) {
+ throw new CRECastException("Void may not be assigned to a variable", t);
+ }
+ if(!InstanceofUtil.isInstanceof(val.typeof(), type, env)) {
+ throw new CRECastException(varName + " is of type " + type.val() + ", but a value of type "
+ + val.typeof() + " was assigned to it.", t);
+ }
+
+ // Set variable in variable list.
+ var = new IVariable(type, varName, val, t);
+ list.set(var);
+
} else {
- offset = 0;
- if(!(args[offset] instanceof IVariable)) {
+
+ // Get and validate variable name.
+ if(!(args[0] instanceof IVariable)) {
throw new CRECastException(getName() + " with 2 arguments only accepts an ivariable as the first argument.", t);
}
- name = ((IVariable) args[offset]).getVariableName();
- IVariable listVar = list.get(name, t, true, env);
- t = listVar.getDefinedTarget();
- type = listVar.getDefinedType();
- }
- Mixed c = args[offset + 1];
- while(c instanceof IVariable) {
- IVariable cur = (IVariable) c;
- c = list.get(cur.getVariableName(), cur.getTarget(), env).ival();
+ String varName = ((IVariable) args[0]).getVariableName();
+
+ // Get assigned value.
+ Mixed val = args[1];
+
+ // Unwrap assigned value from IVariable if an IVariable is passed.
+ if(val instanceof IVariable ivar) {
+ val = list.get(ivar.getVariableName(), ivar.getTarget(), env).ival();
+ }
+
+ // Validate assigned value and set variable in variable list.
+ if(val instanceof CVoid) {
+ throw new CRECastException("Void may not be assigned to a variable", t);
+ }
+ var = list.get(varName);
+ if(var == null) {
+ var = new IVariable(Auto.TYPE, varName, val, t);
+ list.set(var);
+ } else {
+ if(!InstanceofUtil.isInstanceof(val.typeof(), var.getDefinedType(), env)) {
+ throw new CRECastException(varName + " is of type " + var.getDefinedType()
+ + ", but a value of type " + val.typeof() + " was assigned to it.", t);
+ }
+ var.setIval(val);
+ }
}
- IVariable v = new IVariable(type, name, c, t, env);
- list.set(v);
- return v;
+ return var;
}
@Override
From ce7f60a1f3125cde33e938825439e19bd0b2a97f Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Wed, 22 Oct 2025 04:23:45 +0200
Subject: [PATCH 07/12] Fix compile error on post-assign non-cast parenthesis
Fixes compile error in the following example code:
```
@a = (1 + 2)
msg(123)
```
---
src/main/java/com/laytonsmith/core/functions/Compiler.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/laytonsmith/core/functions/Compiler.java b/src/main/java/com/laytonsmith/core/functions/Compiler.java
index 208e1800d..aa28b7f73 100644
--- a/src/main/java/com/laytonsmith/core/functions/Compiler.java
+++ b/src/main/java/com/laytonsmith/core/functions/Compiler.java
@@ -222,7 +222,10 @@ public static ParseTree rewrite(List list, boolean returnSConcat,
|| (list.get(index).getData() instanceof CFunction cf
&& cf.hasFunction() && cf.getFunction() != null
&& cf.getFunction().getName().equals(Compiler.p.NAME)
- && list.get(index).numberOfChildren() == 1))) {
+ && list.get(index).numberOfChildren() == 1
+ && (list.get(index).getChildAt(0).getData() instanceof CClassType
+ || __type_ref__.createFromBareStringOrConcats(
+ list.get(index).getChildAt(0)) != null)))) {
valChildren.add(list.get(index));
list.remove(index);
}
From 7182474b0c9160cecb14153da11e1f588aee767b Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Thu, 23 Oct 2025 02:56:09 +0200
Subject: [PATCH 08/12] Add soft cast syntax compile test
---
.../core/MethodScriptCompilerTest.java | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java b/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java
index 0be64ce14..1a34c8796 100644
--- a/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java
+++ b/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java
@@ -1422,4 +1422,26 @@ public void testInvalidFQCNTypingCompileFailsStrict() throws Exception {
sa.setLocalEnable(true);
MethodScriptCompiler.compile(MethodScriptCompiler.lex(script, env, null, true), env, env.getEnvClasses(), sa);
}
+
+ @Test
+ public void testSoftCastSyntaxCompiles() throws Exception {
+ String script = """
+
+ int @a = (int) 1;
+ number @b = (int) 1;
+ mixed @c = (ms.lang.int) 1;
+ int @d = (int) (1);
+ int @e = (ms.lang.int) 1;
+ int @f = (int) (1 + 2);
+ int @g = (int) 1 + 2;
+ msg((string) 'Hello World!');
+ msg((string) 'Hello '.(string) 'World!');
+ mixed @h = (number) (int) 1;
+ mixed @i = (mixed) (number) (int) (int) (int) 1;
+ """;
+ Environment env = Static.GenerateStandaloneEnvironment();
+ StaticAnalysis sa = new StaticAnalysis(true);
+ sa.setLocalEnable(true);
+ MethodScriptCompiler.compile(MethodScriptCompiler.lex(script, env, null, true), env, env.getEnvClasses(), sa);
+ }
}
From 16419833db50353e2f23cb594ac5ab53c74cedf7 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Thu, 30 Oct 2025 02:28:37 +0100
Subject: [PATCH 09/12] Clone IVariables in IVariableList clone
Allows for not recreating a new `IVariable` for every assign operation.
---
.../java/com/laytonsmith/core/constructs/IVariable.java | 8 ++++++++
.../com/laytonsmith/core/constructs/IVariableList.java | 4 +++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/laytonsmith/core/constructs/IVariable.java b/src/main/java/com/laytonsmith/core/constructs/IVariable.java
index 6460bed0d..e1a30dd1c 100644
--- a/src/main/java/com/laytonsmith/core/constructs/IVariable.java
+++ b/src/main/java/com/laytonsmith/core/constructs/IVariable.java
@@ -96,6 +96,14 @@ public IVariable clone() throws CloneNotSupportedException {
return clone;
}
+ /**
+ * Create a clone of this {@link IVariable} using the same variable value reference.
+ * @return The clone.
+ */
+ public IVariable shallowClone() {
+ return new IVariable(type, name, varValue, definedTarget);
+ }
+
@Override
public boolean isDynamic() {
return true;
diff --git a/src/main/java/com/laytonsmith/core/constructs/IVariableList.java b/src/main/java/com/laytonsmith/core/constructs/IVariableList.java
index b27bf0a18..4a7e32bfe 100644
--- a/src/main/java/com/laytonsmith/core/constructs/IVariableList.java
+++ b/src/main/java/com/laytonsmith/core/constructs/IVariableList.java
@@ -137,7 +137,9 @@ public String toString() {
@Override
public IVariableList clone() {
IVariableList clone = new IVariableList(this);
- clone.varList = new HashMap<>(varList);
+ for(IVariable var : varList.values()) {
+ clone.set(var.shallowClone());
+ }
return clone;
}
From c39c562498eb542b3754611ec7671836f5542398 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Sun, 2 Nov 2025 01:28:35 +0100
Subject: [PATCH 10/12] Add cast to operators table in documentation
---
src/main/resources/docs/Operators | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/main/resources/docs/Operators b/src/main/resources/docs/Operators
index 8547ee48b..94e3a797d 100644
--- a/src/main/resources/docs/Operators
+++ b/src/main/resources/docs/Operators
@@ -71,6 +71,12 @@ are for instance closures and proc references, among a few others. These are fir
in a number of ways, for instance if we define @c as closure @c = closure() { msg('hi'); }; then we can
execute it as @c();.
|-
+| ''Soft cast''
+| %%NOWIKI|( )%%
+| {{function|__cast__}}
+| Right
+| Soft (checking / non-converting) cast. Syntax (type) value.
+|-
| ''Unary''
| ! ++ --
| {{function|not}}/{{function|inc}}/{{function|dec}}
From 89490e1e51aece0f405bbef1629b52da5dcd5a85 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Wed, 5 Nov 2025 05:34:57 +0100
Subject: [PATCH 11/12] Fix runtime proc parameter typecheck
---
src/main/java/com/laytonsmith/core/functions/Compiler.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/laytonsmith/core/functions/Compiler.java b/src/main/java/com/laytonsmith/core/functions/Compiler.java
index aa28b7f73..606264725 100644
--- a/src/main/java/com/laytonsmith/core/functions/Compiler.java
+++ b/src/main/java/com/laytonsmith/core/functions/Compiler.java
@@ -1371,8 +1371,9 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws CancelCommand
}
// Assign value to variable.
+ // Overwrite variable if the type differs (can occur during proc parameter assignment in cloned outer scope).
IVariable var = list.get(varName);
- if(var == null) {
+ if(var == null || (type != null && !type.equals(var.getDefinedType()))) {
var = new IVariable(type, varName, val, t);
list.set(var);
} else {
From 21971d9e5f35791c3dae513752ab9d2b2cf275d0 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Thu, 6 Nov 2025 05:25:17 +0100
Subject: [PATCH 12/12] Rewrite proc parameter execution
Fixes issue where variables in default parameter value expressions resolve to previous parameters instead of variables from the outer scope in runtime.
---
.../core/functions/DataHandling.java | 150 ++++++++++++------
1 file changed, 99 insertions(+), 51 deletions(-)
diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
index 4cbe9d68f..2565f86dc 100644
--- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java
+++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
@@ -1554,9 +1554,8 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes)
public static Procedure getProcedure(Target t, Environment env, Script parent, ParseTree... nodes) {
String name = "";
List vars = new ArrayList<>();
- ParseTree tree = null;
List varNames = new ArrayList<>();
- boolean usesAssign = false;
+ boolean procDefinitelyNotConstant = false;
CClassType returnType = Auto.TYPE;
NodeModifiers modifiers = null;
if(nodes[0].getData().equals(CVoid.VOID) || nodes[0].getData().isInstanceOf(CClassType.TYPE)) {
@@ -1575,71 +1574,120 @@ public static Procedure getProcedure(Target t, Environment env, Script parent, P
nodes[0].getNodeModifiers().merge(modifiers);
// We have to restore the variable list once we're done
IVariableList originalList = env.getEnv(GlobalEnv.class).GetVarList().clone();
- for(int i = 0; i < nodes.length; i++) {
- if(i == nodes.length - 1) {
- tree = nodes[i];
- } else {
- boolean thisNodeIsAssign = false;
- if(nodes[i].getData() instanceof CFunction) {
- String funcName = nodes[i].getData().val();
- if(funcName.equals(assign.NAME) || funcName.equals(__unsafe_assign__.NAME)) {
- thisNodeIsAssign = true;
- if((nodes[i].getChildren().size() == 3 && Construct.IsDynamicHelper(nodes[i].getChildAt(0).getData()))
- || Construct.IsDynamicHelper(nodes[i].getChildAt(1).getData())) {
- usesAssign = true;
- }
- } else if(funcName.equals(__autoconcat__.NAME)) {
- throw new CREInvalidProcedureException("Invalid arguments defined for procedure", t);
+
+ // Get code block node.
+ ParseTree code = nodes[nodes.length - 1];
+
+ // Execute default parameter values.
+ Mixed[] paramDefaultValues = new Mixed[nodes.length - 1];
+ for(int i = 1; i < nodes.length - 1; i++) { // Skip proc name and code block nodes.
+ ParseTree node = nodes[i];
+ if(node.getData() instanceof CFunction cf) {
+ if(cf.val().equals(assign.NAME) || cf.val().equals(__unsafe_assign__.NAME)) {
+ ParseTree paramDefaultValueNode = node.getChildAt(node.numberOfChildren() - 1);
+ env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true);
+ try {
+ paramDefaultValues[i] = parent.eval(paramDefaultValueNode, env);
+ } finally {
+ env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED);
}
+ if(paramDefaultValues[i] instanceof IVariable ivar) {
+ paramDefaultValues[i] = env.getEnv(GlobalEnv.class)
+ .GetVarList().get(ivar.getVariableName(), t, true, env).ival();
+ }
+
+ // Mark proc as not constant if a default parameter is not a constant.
+ if(Construct.IsDynamicHelper(paramDefaultValueNode.getData())) {
+ procDefinitelyNotConstant = true;
+ }
+ continue;
+ } else if(cf.val().equals(__autoconcat__.NAME)) {
+ throw new CREInvalidProcedureException("Invalid arguments defined for procedure", t);
}
+ }
+ paramDefaultValues[i] = null;
+ }
+
+ // Collect parameter IVariable objects with default parameter value assigned.
+ for(int i = 0; i < nodes.length - 1; i++) {
+ IVariable ivar;
+
+ // Get IVariable from parameter with type and/or default value.
+ Mixed paramDefaultValue = paramDefaultValues[i];
+ if(paramDefaultValue != null) {
+
+ // Construct temporary assign node to assign resulting default parameter value.
+ ParseTree assignNode = nodes[i];
+ CFunction assignFunc = (CFunction) assignNode.getData();
+ ParseTree tempAssignNode = new ParseTree(new CFunction(assignFunc.val(),
+ assignNode.getTarget()), assignNode.getFileOptions());
+ if(assignNode.numberOfChildren() == 3) {
+ tempAssignNode.addChild(assignNode.getChildAt(0));
+ tempAssignNode.addChild(assignNode.getChildAt(1));
+ tempAssignNode.addChild(new ParseTree(paramDefaultValue, assignNode.getFileOptions()));
+ } else {
+ tempAssignNode.addChild(assignNode.getChildAt(0));
+ tempAssignNode.addChild(new ParseTree(paramDefaultValue, assignNode.getFileOptions()));
+ }
+
+ // Assign resulting default parameter value to IVariable.
env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN, true);
env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true);
- Mixed cons;
try {
- cons = parent.eval(nodes[i], env);
+ ivar = (IVariable) parent.eval(tempAssignNode, env);
} finally {
env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED);
env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN);
}
+ } else {
+
+ // Execute node to obtain proc name or IVariable object.
+ env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN, true);
+ env.getEnv(GlobalEnv.class).SetFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED, true);
+ Mixed val;
+ try {
+ val = parent.eval(nodes[i], env);
+ } finally {
+ env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_VAR_ARGS_ALLOWED);
+ env.getEnv(GlobalEnv.class).ClearFlag(GlobalEnv.FLAG_NO_CHECK_DUPLICATE_ASSIGN);
+ }
+
+ // Handle proc name node.
if(i == 0) {
- if(cons instanceof IVariable) {
+ if(val instanceof IVariable) {
throw new CREInvalidProcedureException("Anonymous Procedures are not allowed", t);
}
- name = cons.val();
- } else {
- if(!(cons instanceof IVariable)) {
- throw new CREInvalidProcedureException("You must use IVariables as the arguments", t);
- }
- IVariable ivar = null;
- try {
- Mixed c = cons;
- String varName = ((IVariable) c).getVariableName();
- if(varNames.contains(varName)) {
- throw new CREInvalidProcedureException("Same variable name defined twice in " + name, t);
- }
- varNames.add(varName);
- while(c instanceof IVariable) {
- c = env.getEnv(GlobalEnv.class).GetVarList().get(((IVariable) c).getVariableName(), t,
- true, env).ival();
- }
- if(!thisNodeIsAssign) {
- //This is required because otherwise a default value that's already in the environment
- //would end up getting set to the existing value, thereby leaking in the global env
- //into this proc, if the call to the proc didn't have a value in this slot.
- c = new CString("", t);
- }
- ivar = new IVariable(((IVariable) cons).getDefinedType(),
- ((IVariable) cons).getVariableName(), c.clone(), t, env);
- } catch (CloneNotSupportedException ex) {
- //
- }
- vars.add(ivar);
+ name = val.val();
+ continue;
}
+
+ // Handle proc parameter node.
+ if(!(val instanceof IVariable)) {
+ throw new CREInvalidProcedureException("You must use IVariables as the arguments", t);
+ }
+ ivar = (IVariable) val;
+ }
+
+ // Check for duplicate parameter names.
+ String varName = ivar.getVariableName();
+ if(varNames.contains(varName)) {
+ throw new CREInvalidProcedureException("Same variable name defined twice in " + name, t);
}
+ varNames.add(varName);
+
+ // Get IVariable value.
+ Mixed ivarVal = (paramDefaultValue != null ? paramDefaultValue : new CString("", t));
+
+ // Store IVariable object clone.
+ vars.add(new IVariable(ivar.getDefinedType(), ivar.getVariableName(), ivarVal, ivar.getTarget()));
}
+
+ // Restore variable list.
env.getEnv(GlobalEnv.class).SetVarList(originalList);
- Procedure myProc = new Procedure(name, returnType, vars, nodes[0].getNodeModifiers().getComment(), tree, t);
- if(usesAssign) {
+
+ // Create and return procedure.
+ Procedure myProc = new Procedure(name, returnType, vars, nodes[0].getNodeModifiers().getComment(), code, t);
+ if(procDefinitelyNotConstant) {
myProc.definitelyNotConstant();
}
return myProc;