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[] 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;