From a74c7d687c77121d82c2f8541a863ce269623b27 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Thu, 20 Nov 2025 04:44:59 +0100 Subject: [PATCH 01/11] Typecheck procedure signatures --- .../compiler/analysis/StaticAnalysis.java | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java b/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java index eacf81b14..3ec3a1171 100644 --- a/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java +++ b/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java @@ -14,6 +14,8 @@ import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.Static; import com.laytonsmith.core.compiler.CompilerEnvironment; +import com.laytonsmith.core.compiler.signature.FunctionSignatures; +import com.laytonsmith.core.compiler.signature.SignatureBuilder; import com.laytonsmith.core.constructs.Auto; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CFunction; @@ -379,28 +381,68 @@ public CClassType typecheck(ParseTree ast, Environment env, Set argTypes = new ArrayList<>(ast.numberOfChildren()); + List argTargets = new ArrayList<>(ast.numberOfChildren()); for(ParseTree child : ast.getChildren()) { - this.typecheck(child, env, exceptions); + argTypes.add(this.typecheck(child, env, exceptions)); + argTargets.add(child.getTarget()); } - // Return procedure return type. + // Get procedure declaration. String procName = cFunc.val(); - Scope scope = this.getTermScope(ast); - if(scope != null) { - Set decls = scope.getDeclarations(Namespace.PROCEDURE, procName); - if(decls.isEmpty()) { - return CClassType.AUTO; // Proc cannot be resolved. Exception for this is already generated. - } else { - // TODO - Get the most specific type when multiple declarations exist. - return decls.iterator().next().getType(); - } - } else { + Target procTarget = cFunc.getTarget(); + Scope procRefScope = this.getTermScope(ast); + if(procRefScope == null) { + // If this runs, then a proc reference was created without setting its Scope using setTermScope(). exceptions.add(new ConfigCompileException("Procedure cannot be resolved (missing procedure scope," + " this is an internal error that should never happen): " - + procName, cFunc.getTarget())); + + procName, procTarget)); return CClassType.AUTO; } + Set decls = procRefScope.getDeclarations(Namespace.PROCEDURE, procName); + if(decls.isEmpty()) { + return CClassType.AUTO; // Proc cannot be resolved. Exception already generated. + } + + // Create procedure signatures. + // TODO - Consider creating one SignatureBuilder per declaration. With the current implementation, matching any possible declaration is sufficient. We want it to match all instead. + List procReturnTypes = new ArrayList<>(1); + for(Declaration decl : decls) { + if(decl instanceof ProcDeclaration procDecl) { + + // Create new procedure signature. + SignatureBuilder signatureBuilder = new SignatureBuilder(procDecl.getType()); + for(ParamDeclaration paramDecl : procDecl.getParameters()) { + CClassType paramType = paramDecl.getType(); + if(paramType.isVariadicType()) { + signatureBuilder.varParam( + paramType.getVarargsBaseType(), paramDecl.getIdentifier(), null); + } else { + signatureBuilder.param(paramType, + paramDecl.getIdentifier(), null, paramDecl.getDefaultValue() != null); + } + } + + // Typecheck arguments against new procedure signature. + FunctionSignatures procSignature = signatureBuilder.build(); + procReturnTypes.add(procSignature.getReturnType( + procTarget, argTypes, argTargets, env, exceptions)); + } else { + + // If this runs, then a wrong declaration type proc declaration was created. + exceptions.add(new ConfigCompileException("Procedure resolves to non-procedure declaration" + + " (this is an internal error that should never happen): " + + procName + " -> " + decl.getIdentifier(), procTarget)); + } + } + if(procReturnTypes.isEmpty()) { + return CClassType.AUTO; // No ProcDeclarations found. Exception already generated. + } + + // Return procedure return type. + // TODO - Get the most specific type when multiple declarations exist. + return procReturnTypes.get(0); } else { throw new Error("Unsupported " + CFunction.class.getSimpleName() + " type in type checking for node with value: " + cFunc.val()); From c792f50e477573e3a13fdcca5643adfe1a7c4052 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Fri, 21 Nov 2025 04:09:00 +0100 Subject: [PATCH 02/11] Disallow mandatory-after-optional params in proc signature typechecking --- .../java/com/laytonsmith/core/Procedure.java | 18 ++++++++++++++---- .../core/compiler/analysis/StaticAnalysis.java | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/Procedure.java b/src/main/java/com/laytonsmith/core/Procedure.java index 8e62f139c..43cbb2805 100644 --- a/src/main/java/com/laytonsmith/core/Procedure.java +++ b/src/main/java/com/laytonsmith/core/Procedure.java @@ -66,10 +66,12 @@ public Procedure(String name, CClassType returnType, List varList, Sm this.definedAt = t; this.varList = new HashMap<>(); this.procComment = procComment; + boolean optionalParamDetected = false; for(int i = 0; i < varList.size(); i++) { IVariable var = varList.get(i); if(var.getDefinedType().isVariadicType() && i != varList.size() - 1) { - throw new CREFormatException("Varargs can only be added to the last argument.", t); + throw new CREFormatException( + "Varargs can only be added to the last parameter in a procedure.", var.getTarget()); } try { this.varList.put(var.getVariableName(), var.clone()); @@ -77,14 +79,22 @@ public Procedure(String name, CClassType returnType, List varList, Sm this.varList.put(var.getVariableName(), var); } this.varIndex.add(var); - if(var.getDefinedType().isVariadicType() && var.ival() != CNull.UNDEFINED) { - throw new CREFormatException("Varargs may not have default values", t); + boolean paramOptional = (var.ival() != CNull.UNDEFINED); + if(paramOptional) { + if(var.getDefinedType().isVariadicType()) { + throw new CREFormatException("Varargs may not have default values.", var.getTarget()); + } + optionalParamDetected = true; + } else if(optionalParamDetected && !var.getDefinedType().isVariadicType()) { + throw new CREFormatException( + "Procedure parameters after optional parameters must be optional or varargs.", var.getTarget()); } this.originals.put(var.getVariableName(), var.ival()); } this.tree = tree; if(!PROCEDURE_NAME_REGEX.matcher(name).matches()) { - throw new CREFormatException("Procedure names must start with an underscore, and may only contain letters, underscores, and digits. (Found " + this.name + ")", t); + throw new CREFormatException("Procedure names must start with an underscore," + + " and may only contain letters, underscores, and digits. (Found " + this.name + ")", t); } //Let's look through the tree now, and see if this is possibly constant or not. //If it is, it may or may not help us during compilation, but if it's not, diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java b/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java index 3ec3a1171..7ab7cafb0 100644 --- a/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java +++ b/src/main/java/com/laytonsmith/core/compiler/analysis/StaticAnalysis.java @@ -406,21 +406,31 @@ public CClassType typecheck(ParseTree ast, Environment env, Set procReturnTypes = new ArrayList<>(1); for(Declaration decl : decls) { if(decl instanceof ProcDeclaration procDecl) { // Create new procedure signature. SignatureBuilder signatureBuilder = new SignatureBuilder(procDecl.getType()); + boolean optionalParamDetected = false; + boolean varargsParamDetected = false; for(ParamDeclaration paramDecl : procDecl.getParameters()) { CClassType paramType = paramDecl.getType(); + if(varargsParamDetected) { + return CClassType.AUTO; // Invalid procedure signature. Exception(s) already generated. + } if(paramType.isVariadicType()) { signatureBuilder.varParam( paramType.getVarargsBaseType(), paramDecl.getIdentifier(), null); + varargsParamDetected = true; } else { - signatureBuilder.param(paramType, - paramDecl.getIdentifier(), null, paramDecl.getDefaultValue() != null); + boolean paramOptional = paramDecl.getDefaultValue() != null; + signatureBuilder.param(paramType, paramDecl.getIdentifier(), null, paramOptional); + if(paramOptional) { + optionalParamDetected = true; + } else if(optionalParamDetected) { + return CClassType.AUTO; // Invalid procedure signature. Exception(s) already generated. + } } } From 0e1db49358ace4e7f38d7dbee5819cf15fce1838 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Sun, 23 Nov 2025 18:31:42 +0100 Subject: [PATCH 03/11] Update procedure behavior - Prevent injected `@arguments` variable overwriting an explicitly declared procedure parameter named `@arguments`. - Initialize procedure varargs parameter with an empty array if it matches no arguments. Prevents varargs parameter value being `CNull.UNDEFINED` when it has no matching arguments. --- .../java/com/laytonsmith/core/Procedure.java | 118 ++++++++++-------- .../core/functions/DataHandling.java | 5 +- 2 files changed, 69 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/Procedure.java b/src/main/java/com/laytonsmith/core/Procedure.java index 43cbb2805..483221a56 100644 --- a/src/main/java/com/laytonsmith/core/Procedure.java +++ b/src/main/java/com/laytonsmith/core/Procedure.java @@ -7,6 +7,7 @@ import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CFunction; import com.laytonsmith.core.constructs.CNull; +import com.laytonsmith.core.constructs.CString; import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.IVariable; @@ -199,92 +200,107 @@ public Mixed cexecute(List args, Environment env, Target t) { * Executes this procedure, with the arguments that were passed in * * @param args The arguments passed to the procedure call. - * @param oldEnv The environment to be cloned. + * @param parentEnv The environment to be cloned. * @param t * @return */ - public Mixed execute(List args, Environment oldEnv, Target t) { - boolean prev = oldEnv.getEnv(GlobalEnv.class).getCloneVars(); - oldEnv.getEnv(GlobalEnv.class).setCloneVars(false); + public Mixed execute(List args, Environment parentEnv, Target t) { + boolean prevCloneVars = parentEnv.getEnv(GlobalEnv.class).getCloneVars(); + parentEnv.getEnv(GlobalEnv.class).setCloneVars(false); Environment env; try { - env = oldEnv.clone(); + env = parentEnv.clone(); env.getEnv(GlobalEnv.class).setCloneVars(true); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } - oldEnv.getEnv(GlobalEnv.class).setCloneVars(prev); + parentEnv.getEnv(GlobalEnv.class).setCloneVars(prevCloneVars); Script fakeScript = Script.GenerateScript(tree, env.getEnv(GlobalEnv.class).GetLabel(), null); // Create container for the @arguments variable. CArray arguments = new CArray(Target.UNKNOWN, this.varIndex.size()); + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, "@arguments", arguments, t)); + + // Initialize varargs parameter if present. + CArray vararg = null; + IVariable varargsVar = (!this.varIndex.isEmpty() + && this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType() + ? this.varIndex.get(this.varIndex.size() - 1) : null); + if(varargsVar != null) { + vararg = new CArray(t); // TODO - Add type once generics are implemented. + Target varargsTarget = (args.size() >= this.varIndex.size() + ? args.get(this.varIndex.size() - 1).getTarget() : t); + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, + varargsVar.getVariableName(), vararg, varargsTarget)); + } // Handle passed procedure arguments. int varInd; - CArray vararg = null; for(varInd = 0; varInd < args.size(); varInd++) { - Mixed c = args.get(varInd); - arguments.push(c, t); - if(this.varIndex.size() > varInd - || (!this.varIndex.isEmpty() - && this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType())) { - IVariable var; - if(varInd < this.varIndex.size() - 1 - || !this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType()) { - var = this.varIndex.get(varInd); - } else { - var = this.varIndex.get(this.varIndex.size() - 1); - if(vararg == null) { - // TODO: Once generics are added, add the type - vararg = new CArray(t); - env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, - var.getVariableName(), vararg, c.getTarget())); - } - } - // Type check "void" value. - if(c instanceof CVoid - && !(var.getDefinedType().equals(Auto.TYPE) || var.getDefinedType().equals(CVoid.TYPE))) { - throw new CRECastException("Procedure \"" + name + "\" expects a value of type " - + var.getDefinedType().val() + " in argument " + (varInd + 1) + ", but" - + " a void value was found instead.", c.getTarget()); - } + // Get argument value. + Mixed argVal = args.get(varInd); - // Type check vararg parameter. - if(var.getDefinedType().isVariadicType()) { - if(InstanceofUtil.isInstanceof(c.typeof(), var.getDefinedType().getVarargsBaseType(), env)) { - vararg.push(c, t); - continue; - } else { - throw new CRECastException("Procedure \"" + name + "\" expects a value of type " - + var.getDefinedType().val() + " in argument " + (varInd + 1) + ", but" - + " a value of type " + c.typeof() + " was found instead.", c.getTarget()); - } - } + // Add argument value to @arguments array. + arguments.push(argVal, t); + + // Get parameter to assign argument value to. + IVariable var; + if(this.varIndex.size() > varInd) { + var = this.varIndex.get(varInd); + } else if(varargsVar != null) { + var = varargsVar; + } else { + continue; // No parameters remaining and no varargs present. Ignore excessive arguments. + } + + // Type check "void" argument/parameter. + if(argVal instanceof CVoid + && !(var.getDefinedType().equals(Auto.TYPE) || var.getDefinedType().equals(CVoid.TYPE))) { + throw new CRECastException("Procedure \"" + name + "\" expects a value of type " + + var.getDefinedType().val() + " in argument " + (varInd + 1) + ", but" + + " a void value was found instead.", argVal.getTarget()); + } - // Type check non-vararg parameter. - if(InstanceofUtil.isInstanceof(c.typeof(), var.getDefinedType(), env)) { - env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(var.getDefinedType(), - var.getVariableName(), c, c.getTarget())); + // Type check vararg argument/parameter. + if(var.getDefinedType().isVariadicType()) { + if(InstanceofUtil.isInstanceof(argVal.typeof(), var.getDefinedType().getVarargsBaseType(), env)) { + vararg.push(argVal, t); continue; } else { throw new CRECastException("Procedure \"" + name + "\" expects a value of type " + var.getDefinedType().val() + " in argument " + (varInd + 1) + ", but" - + " a value of type " + c.typeof() + " was found instead.", c.getTarget()); + + " a value of type " + argVal.typeof() + " was found instead.", argVal.getTarget()); } } + + // Type check non-vararg argument/parameter. + if(InstanceofUtil.isInstanceof(argVal.typeof(), var.getDefinedType(), env)) { + env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(var.getDefinedType(), + var.getVariableName(), argVal, argVal.getTarget())); + continue; + } else { + throw new CRECastException("Procedure \"" + name + "\" expects a value of type " + + var.getDefinedType().val() + " in argument " + (varInd + 1) + ", but" + + " a value of type " + argVal.typeof() + " was found instead.", argVal.getTarget()); + } } // Assign default values to remaining proc arguments. - while(varInd < this.varIndex.size()) { - String varName = this.varIndex.get(varInd++).getVariableName(); + int nonVarargVarSize = (vararg == null ? this.varIndex.size() : this.varIndex.size() - 1); + while(varInd < nonVarargVarSize) { + IVariable paramVar = this.varIndex.get(varInd++); + String varName = paramVar.getVariableName(); Mixed defVal = this.originals.get(varName); + if(defVal == CNull.UNDEFINED) { + defVal = new CString("", paramVar.getTarget()); // Fill in empty string for undefined parameters. + } env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(Auto.TYPE, varName, defVal, defVal.getTarget())); arguments.push(defVal, t); } - env.getEnv(GlobalEnv.class).GetVarList().set(new IVariable(CArray.TYPE, "@arguments", arguments, t)); + // Execute procedure body. StackTraceManager stManager = env.getEnv(GlobalEnv.class).GetStackTraceManager(); stManager.addStackTraceElement(new ConfigRuntimeException.StackTraceElement("proc " + name, getTarget())); try { diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java index f0dfc4296..08dbca786 100644 --- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java @@ -1594,8 +1594,7 @@ public static Procedure getProcedure(Target t, Environment env, Script parent, P 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(); + paramDefaultValues[i] = originalList.get(ivar.getVariableName(), t, env).ival(); } // Mark proc as not constant if a default parameter is not a constant. @@ -1678,7 +1677,7 @@ public static Procedure getProcedure(Target t, Environment env, Script parent, P varNames.add(varName); // Get IVariable value. - Mixed ivarVal = (paramDefaultValue != null ? paramDefaultValue : new CString("", t)); + Mixed ivarVal = (paramDefaultValue != null ? paramDefaultValue : CNull.UNDEFINED); // Store IVariable object clone. vars.add(new IVariable(ivar.getDefinedType(), ivar.getVariableName(), ivarVal, ivar.getTarget())); From c5f680147c2fc63f7bf7973ec3b3c86ffe1ec93f Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Tue, 25 Nov 2025 03:32:03 +0100 Subject: [PATCH 04/11] Add compile error on missing returns in procedures Generate compile error for missing return statements in any code path of non-void procedure code. --- .../core/functions/ControlFlow.java | 4 +-- .../core/functions/DataHandling.java | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java index b571627e0..ecca7be4a 100644 --- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java +++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java @@ -2708,8 +2708,8 @@ public CClassType typecheck(StaticAnalysis analysis, } } - // Return void. - return CVoid.TYPE; + // Return null to indicate that this call does not result in a value. + return null; } @Override diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java index 08dbca786..611c98b7a 100644 --- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java @@ -1699,6 +1699,34 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntime return CVoid.VOID; } + @Override + public CClassType typecheck(StaticAnalysis analysis, + ParseTree ast, Environment env, Set exceptions) { + + // Fall back to super implementation for invalid usage. + if(ast.numberOfChildren() < 2) { + return super.typecheck(analysis, ast, env, exceptions); + } + + // Get procedure declared return type (not the proc() return type). + CClassType returnType = (ast.getChildAt(0).getData() instanceof CClassType type ? type : CClassType.AUTO); + + // Typecheck arguments excluding code block. + for(int i = 0; i < ast.numberOfChildren() - 1; i++) { + analysis.typecheck(ast.getChildAt(i), env, exceptions); + } + + // Typecheck code block. + CClassType codeBlockType = analysis.typecheck(ast.getChildAt(ast.numberOfChildren() - 1), env, exceptions); + if(returnType != CClassType.AUTO && returnType != CVoid.TYPE && codeBlockType != null) { + exceptions.add(new ConfigCompileException( + "Missing return statement; Not all code paths return a value.", ast.getTarget())); + } + + // Return void. + return CVoid.TYPE; + } + @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { From 03f63356bae19a7b35f81570d8c702912654bd19 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Tue, 25 Nov 2025 04:37:12 +0100 Subject: [PATCH 05/11] Implement getReturnType() in control flow functions - Forward `null` return type during typechecking when the function won't return at all. - Return least specific type among possible return types in control flow functions with multiple possible outcomes. These essentially need to return a multi-type CClassType, but this is the best we can do in the current code base without introducing harm. --- .../core/functions/ControlFlow.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java index ecca7be4a..6a5e3d50d 100644 --- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java +++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java @@ -84,6 +84,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -438,6 +439,56 @@ public FunctionSignatures getSignatures() { return super.getSignatures(); } + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Override return type with null if the first condition (or lonely else code) is terminating. + if(argTypes.size() >= 1 && argTypes.get(0) == null) { + return null; + } + + // Override return type with null if all code branches (including the else code branch) are terminating. + if((argTypes.size() & 0x01) != 0x00) { // (size % 2) != 0. + + // Get all code branch return types. + boolean allBranchesTerminate = true; + Set returnTypes = new HashSet<>(); + for(int i = 1; i < argTypes.size(); i += 2) { + CClassType argType = argTypes.get(i); + returnTypes.add(argType); + allBranchesTerminate &= (argType == null); + } + CClassType defaultArgType = argTypes.get(argTypes.size() - 1); + returnTypes.add(defaultArgType); + allBranchesTerminate &= (defaultArgType == null); + + // Return null if all branches (including the else code branch) are terminating. + if(allBranchesTerminate) { + return null; + } + + // Return an occurring return type if all return types extend that type. + // TODO - Make this return a multitype instead as soon as all typechecking code supports multitypes. + for(CClassType type1 : returnTypes) { + if(type1 != null) { + for(CClassType type2 : returnTypes) { + if(!InstanceofUtil.isInstanceof(type2, type1, env)) { + break; + } + } + return type1; + } + } + } + + // Return the super result. + return retType; + } + @Override public boolean useSpecialExec() { return true; @@ -726,6 +777,56 @@ public FunctionSignatures getSignatures() { return super.getSignatures(); } + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Override return type with null if the value is terminating. + if(argTypes.size() >= 1 && argTypes.get(0) == null) { + return null; + } + + // Override return type with null if all cases (including the default case) are terminating. + if((argTypes.size() & 0x01) == 0x00) { // (size % 2) == 0. + + // Get all case/default code branch return types. + boolean allBranchesTerminate = true; + Set returnTypes = new HashSet<>(); + for(int i = 2; i < argTypes.size(); i += 2) { + CClassType argType = argTypes.get(i); + returnTypes.add(argType); + allBranchesTerminate &= (argType == null); + } + CClassType defaultArgType = argTypes.get(argTypes.size() - 1); + returnTypes.add(defaultArgType); + allBranchesTerminate &= (defaultArgType == null); + + // Return null if all cases (including the default case) are terminating. + if(allBranchesTerminate) { + return null; + } + + // Return an occurring return type if all return types extend that type. + // TODO - Make this return a multitype instead as soon as all typechecking code supports multitypes. + for(CClassType type1 : returnTypes) { + if(type1 != null) { + for(CClassType type2 : returnTypes) { + if(!InstanceofUtil.isInstanceof(type2, type1, env)) { + break; + } + } + } + return type1; + } + } + + // Return the super result. + return retType; + } + @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -1119,6 +1220,22 @@ public FunctionSignatures getSignatures() { .param(null, "loopCode", "The code that is executed in the loop.").build(); } + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Override return type with null if the assign or condition is terminating. + if(argTypes.size() == 4 && (argTypes.get(0) == null || argTypes.get(1) == null)) { + return null; + } + + // Return the super result. + return retType; + } + @Override public Class[] thrown() { return new Class[]{CRECastException.class}; @@ -1383,6 +1500,22 @@ public FunctionSignatures getSignatures() { + " false in the first iteration of the loop.").build(); } + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Override return type with null if the assign or condition is terminating. + if(argTypes.size() == 5 && (argTypes.get(0) == null || argTypes.get(1) == null)) { + return null; + } + + // Return the super result. + return retType; + } + @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -2117,6 +2250,22 @@ public FunctionSignatures getSignatures() { .param(null, "code", "The code that is executed in the loop.", true).build(); } + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Override return type with null if the condition is terminating. + if(argTypes.size() == 2 && argTypes.get(0) == null) { + return null; + } + + // Return the super result. + return retType; + } + @Override public boolean useSpecialExec() { return true; @@ -2241,6 +2390,22 @@ public FunctionSignatures getSignatures() { "The loop condition that is checked each time after the code is executed.").build(); } + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Override return type with null if the code branch or condition is terminating. + if(argTypes.size() == 2 && (argTypes.get(0) == null || argTypes.get(1) == null)) { + return null; + } + + // Return the super result. + return retType; + } + @Override public Integer[] numArgs() { return new Integer[]{2}; From 254d17109d159c15f3cadaacc200c8d6b4ad58b9 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Wed, 26 Nov 2025 04:32:20 +0100 Subject: [PATCH 06/11] Implement throw() function signatures --- .../core/functions/Exceptions.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main/java/com/laytonsmith/core/functions/Exceptions.java b/src/main/java/com/laytonsmith/core/functions/Exceptions.java index 916aab22e..f64f65bb7 100644 --- a/src/main/java/com/laytonsmith/core/functions/Exceptions.java +++ b/src/main/java/com/laytonsmith/core/functions/Exceptions.java @@ -28,6 +28,8 @@ import com.laytonsmith.core.compiler.VariableScope; import com.laytonsmith.core.compiler.analysis.Scope; 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.CArray; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CClosure; @@ -375,6 +377,29 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws CancelCommand throw throwable; } } + + @Override + public FunctionSignatures getSignatures() { + return new SignatureBuilder(null).param(CArray.TYPE, "exception", "The exception to throw.") + .newSignature(null).param(CREThrowable.TYPE, "exception", "The exception to throw.") + .newSignature(null) + .param(CString.TYPE, "exceptionType", "The exception type string.") + .param(CString.TYPE, "msg", "The exception message.") + .param(CArray.TYPE, "causedBy", "The causing exception object.", true) + .newSignature(null) + .param(CString.TYPE, "exceptionType", "The exception type string.") + .param(CString.TYPE, "msg", "The exception message.") + .param(CREThrowable.TYPE, "causedBy", "The causing exception object.", true) + .newSignature(null) + .param(CClassType.TYPE, "exceptionType", "The exception type.") + .param(CString.TYPE, "msg", "The exception message.") + .param(CArray.TYPE, "causedBy", "The causing exception object.", true) + .newSignature(null) + .param(CClassType.TYPE, "exceptionType", "The exception type.") + .param(CString.TYPE, "msg", "The exception message.") + .param(CREThrowable.TYPE, "causedBy", "The causing exception object.", true) + .build(); + } } @api From 215f3b5d0ff60a81e1d18a6bfdc96a7359bf3899 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Wed, 26 Nov 2025 23:42:05 +0100 Subject: [PATCH 07/11] Throw exception when invoking proc forward declaration --- .../core/compiler/keywords/ProcKeyword.java | 19 ++++++++++++++----- .../core/functions/Exceptions.java | 4 +++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/compiler/keywords/ProcKeyword.java b/src/main/java/com/laytonsmith/core/compiler/keywords/ProcKeyword.java index 394e4ec8b..65414a31c 100644 --- a/src/main/java/com/laytonsmith/core/compiler/keywords/ProcKeyword.java +++ b/src/main/java/com/laytonsmith/core/compiler/keywords/ProcKeyword.java @@ -12,8 +12,9 @@ import com.laytonsmith.core.constructs.CString; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.exceptions.ConfigCompileException; +import com.laytonsmith.core.exceptions.CRE.CREInvalidProcedureException; import com.laytonsmith.core.functions.DataHandling; -import com.laytonsmith.core.functions.Meta; +import com.laytonsmith.core.functions.Exceptions; import com.laytonsmith.core.functions.Compiler; import java.util.List; @@ -71,11 +72,19 @@ public int process(List list, int keywordPosition) throws ConfigCompi list.remove(keywordPosition); // Remove __cbrace__ function node. } else { - // Define as forward declaration by add a "noop()" as code block. + // Define as forward declaration by adding an exception throw as code block. + FileOptions forwardImplFileOptions = list.get(keywordPosition + 1).getFileOptions(); + ParseTree throwNode = new ParseTree(new CFunction(Exceptions._throw.NAME, Target.UNKNOWN), + forwardImplFileOptions, true); + throwNode.addChild(new ParseTree( + new CString(CREInvalidProcedureException.TYPE.getSimpleName(), Target.UNKNOWN), + options, true)); + throwNode.addChild(new ParseTree( + new CString("Cannot invoke procedure forward declaration.", Target.UNKNOWN), + options, true)); ParseTree statement = new ParseTree(new CFunction(Compiler.__statements__.NAME, Target.UNKNOWN), - list.get(keywordPosition + 1).getFileOptions(), true); - statement.addChild(new ParseTree(new CFunction(Meta.noop.NAME, Target.UNKNOWN), - list.get(keywordPosition + 1).getFileOptions(), true)); + forwardImplFileOptions, true); + statement.addChild(throwNode); procNode.addChild(statement); // Remove processed nodes from AST. diff --git a/src/main/java/com/laytonsmith/core/functions/Exceptions.java b/src/main/java/com/laytonsmith/core/functions/Exceptions.java index f64f65bb7..c0bcefb6b 100644 --- a/src/main/java/com/laytonsmith/core/functions/Exceptions.java +++ b/src/main/java/com/laytonsmith/core/functions/Exceptions.java @@ -284,9 +284,11 @@ public List isScope(List children) { @seealso({_try.class, com.laytonsmith.tools.docgen.templates.Exceptions.class}) public static class _throw extends AbstractFunction { + public static final String NAME = "throw"; + @Override public String getName() { - return "throw"; + return NAME; } @Override From afcdcf1613a714d8b95f5d85e950d942355ab959 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Thu, 16 Jul 2026 04:16:53 +0200 Subject: [PATCH 08/11] Implement try() and complex_try() function signatures --- .../core/functions/Exceptions.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/main/java/com/laytonsmith/core/functions/Exceptions.java b/src/main/java/com/laytonsmith/core/functions/Exceptions.java index c0bcefb6b..9be1861eb 100644 --- a/src/main/java/com/laytonsmith/core/functions/Exceptions.java +++ b/src/main/java/com/laytonsmith/core/functions/Exceptions.java @@ -211,6 +211,40 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws CancelCommand return CVoid.VOID; } + @Override + public FunctionSignatures getSignatures() { + return new SignatureBuilder(CVoid.TYPE) + .param(null, "tryCode", "The try code block.") + .param(null, "catchCode", "The catch code block.", true) + .newSignature(CVoid.TYPE) + .param(null, "tryCode", "The try code block.") + .param(null, "exVar", "The exception variable.") + .param(null, "catchCode", "The catch code block.") + .param(CString.TYPE, "exceptionType", "The exception type to catch.") + .newSignature(CVoid.TYPE) + .param(null, "tryCode", "The try code block.") + .param(null, "exVar", "The exception variable.") + .param(null, "catchCode", "The catch code block.") + .param(CArray.TYPE, "exceptionTypes", "Array of exception types to catch.", true).build(); + } + + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Return null if both try and catch block do not return. + if((argTypes.size() == 2 && argTypes.get(0) == null && argTypes.get(1) == null) + || (argTypes.size() >= 3 && argTypes.get(0) == null && argTypes.get(2) == null)) { + return null; + } + + // Return return type based on function signatures. + return retType; + } + @Override @SuppressWarnings({"checkstyle:fallthrough", "checkstyle:defaultcomeslast"}) // Intended for control flow. public Scope linkScope(StaticAnalysis analysis, Scope parentScope, @@ -585,6 +619,40 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) return CVoid.VOID; } + @Override + public FunctionSignatures getSignatures() { + + // TODO - Define a better way to define "[catchVariable, catchCode]*, [catchCode]" in function signatures. + return new SignatureBuilder(CVoid.TYPE) + .param(null, "tryCode", "The try code block.") + .varParam(null, "[catchVariable, catchCode]*, [catchCode]", "The catch blocks.").build(); + } + + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Return null if both try and all catch blocks do not return. + if(argTypes.size() > 1 && argTypes.get(0) == null) { + search: { + for(int i = 2; i < argTypes.size(); i += 2) { + if(argTypes.get(i) != null) { + break search; + } + } + if(!MathUtils.isEven(argTypes.size()) || argTypes.get(argTypes.size() - 1) == null) { + return null; + } + } + } + + // Return return type based on function signatures. + return retType; + } + @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { From b665b41adbf054bd3ca65c8562decc9e4cbcb540 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Thu, 16 Jul 2026 05:31:21 +0200 Subject: [PATCH 09/11] Update control flow function typechecking --- .../core/functions/ControlFlow.java | 167 +++++++----------- .../core/functions/Exceptions.java | 5 +- 2 files changed, 65 insertions(+), 107 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java index 6a5e3d50d..ba1b28807 100644 --- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java +++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java @@ -1,5 +1,6 @@ package com.laytonsmith.core.functions; +import com.laytonsmith.PureUtilities.MathUtils; import com.laytonsmith.PureUtilities.TermColors; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.Version; @@ -136,14 +137,22 @@ public Mixed exec(Target t, Environment env, Mixed... args) @Override public FunctionSignatures getSignatures() { /* - * TODO - Decide how to define the ternary return value. - * Note that getReturnType is overridden, so these signatures are not used for typechecking. + * TODO - Decide how to define the ternary return value. */ return new SignatureBuilder(CClassType.AUTO, MatchType.MATCH_FIRST) .param(Booleanish.TYPE, "cond", "The condition.") .param(Mixed.TYPE, "ifValue", "The value that is returned when the condition is true.") .param(Mixed.TYPE, "elseValue", "The value that is returned when the condition is false.") - .newSignature(CVoid.TYPE).param(Booleanish.TYPE, "cond", "The condition.") + .newSignature(CClassType.AUTO) + .param(Booleanish.TYPE, "cond", "The condition.") + .param(Mixed.TYPE, "ifValue", "The value that is returned when the condition is true.") + .param(null, "elseCode", "The optional code that runs when the condition is false. This code must be terminating.") + .newSignature(CClassType.AUTO) + .param(Booleanish.TYPE, "cond", "The condition.") + .param(null, "ifCode", "The code that runs when the condition is true. This code must be terminating.") + .param(Mixed.TYPE, "elseValue", "The value that is returned when the condition is false.") + .newSignature(CVoid.TYPE) + .param(Booleanish.TYPE, "cond", "The condition.") .param(null, "ifCode", "The code that runs when the condition is true.") .param(null, "elseCode", "The optional code that runs when the condition is false.", true).build(); } @@ -155,9 +164,9 @@ public CClassType getReturnType(Target t, List argTypes, // Get return type based on the function signatures. This generates all necessary compile errors. CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); - // When void is returned, ternary usage could still be possible when a branch is terminating. - // It is also possible that both branches are terminating, in which case this should return null as well. - if(retType == CVoid.TYPE && argTypes.size() == 3) { + // Return the type of the other code branch / value if one branch is terminating. + // Return null if both branches are terminating. + if(argTypes.size() == 3) { // Return the type of the other branch if one branch is terminating (ternary, terminating or void). if(argTypes.get(1) == null) { @@ -182,7 +191,7 @@ public CClassType getReturnType(Target t, List argTypes, } } - // Return the super result. + // Return super result. return retType; } @@ -432,11 +441,14 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) @Override public FunctionSignatures getSignatures() { + /* - * TODO - Implement a way to define [cond, code]* using signatures, and use it here. - * Also check switch() and switch_ic(), as they need the same feature. + * TODO - Implement a way to define [cond, code]*, [elseCode] using signatures, and use it here. + * Also check complex_try(), switch() and switch_ic(), as they need the same feature. + * Implement Booleanish typecheck for all conditions. These are currently checked in getReturnType(). */ - return super.getSignatures(); + return new SignatureBuilder(CVoid.TYPE) + .varParam(null, "[cond, code]*, [elseCode]", "The conditional code blocks.").build(); } @Override @@ -446,25 +458,28 @@ public CClassType getReturnType(Target t, List argTypes, // Get return type based on the function signatures. This generates all necessary compile errors. CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); - // Override return type with null if the first condition (or lonely else code) is terminating. - if(argTypes.size() >= 1 && argTypes.get(0) == null) { - return null; - } - // Override return type with null if all code branches (including the else code branch) are terminating. - if((argTypes.size() & 0x01) != 0x00) { // (size % 2) != 0. + if(argTypes.size() >= 1) { // Get all code branch return types. boolean allBranchesTerminate = true; Set returnTypes = new HashSet<>(); for(int i = 1; i < argTypes.size(); i += 2) { - CClassType argType = argTypes.get(i); - returnTypes.add(argType); - allBranchesTerminate &= (argType == null); + + // Typecheck condition arguments. + // TODO - Remove this typecheck when it is implemented through function signatures. + CClassType condType = argTypes.get(i - 1); + StaticAnalysis.requireType(condType, Booleanish.TYPE, argTargets.get(i - 1), env, exceptions); + + CClassType codeType = argTypes.get(i); + returnTypes.add(codeType); + allBranchesTerminate &= (codeType == null); + } + if(MathUtils.isOdd(argTypes.size())) { + CClassType defaultArgType = argTypes.get(argTypes.size() - 1); + returnTypes.add(defaultArgType); + allBranchesTerminate &= (defaultArgType == null); } - CClassType defaultArgType = argTypes.get(argTypes.size() - 1); - returnTypes.add(defaultArgType); - allBranchesTerminate &= (defaultArgType == null); // Return null if all branches (including the else code branch) are terminating. if(allBranchesTerminate) { @@ -473,19 +488,21 @@ public CClassType getReturnType(Target t, List argTypes, // Return an occurring return type if all return types extend that type. // TODO - Make this return a multitype instead as soon as all typechecking code supports multitypes. + search: for(CClassType type1 : returnTypes) { if(type1 != null) { for(CClassType type2 : returnTypes) { - if(!InstanceofUtil.isInstanceof(type2, type1, env)) { - break; + if(type2 != null && !InstanceofUtil.isInstanceof(type2, type1, env)) { + continue search; } } return type1; } } + return CClassType.AUTO; } - // Return the super result. + // Return super result. return retType; } @@ -513,7 +530,7 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, } // Handle optional else branch in separate scope. - if((ast.numberOfChildren() & 0x01) == 0x01) { // (size % 2) == 1. + if(MathUtils.isOdd(ast.numberOfChildren())) { analysis.linkScope(analysis.createNewScope(parentScope), ast.getChildAt(ast.numberOfChildren() - 1), env, exceptions); } @@ -770,11 +787,14 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) @Override public FunctionSignatures getSignatures() { + /* * TODO - Implement a way to define [case, code]* using signatures, and use it here. - * Also check ifelse() and switch_ic(), as they need the same feature. + * Also check complex_try(), ifelse() and switch_ic(), as they need the same feature. */ - return super.getSignatures(); + return new SignatureBuilder(CVoid.TYPE) + .param(Mixed.TYPE, "value", "The switch value.") + .varParam(null, "[caseVal, caseCode]*, [defaultCode]", "The case blocks.").build(); } @Override @@ -784,13 +804,8 @@ public CClassType getReturnType(Target t, List argTypes, // Get return type based on the function signatures. This generates all necessary compile errors. CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); - // Override return type with null if the value is terminating. - if(argTypes.size() >= 1 && argTypes.get(0) == null) { - return null; - } - // Override return type with null if all cases (including the default case) are terminating. - if((argTypes.size() & 0x01) == 0x00) { // (size % 2) == 0. + if(argTypes.size() >= 2) { // Get all case/default code branch return types. boolean allBranchesTerminate = true; @@ -800,9 +815,11 @@ public CClassType getReturnType(Target t, List argTypes, returnTypes.add(argType); allBranchesTerminate &= (argType == null); } - CClassType defaultArgType = argTypes.get(argTypes.size() - 1); - returnTypes.add(defaultArgType); - allBranchesTerminate &= (defaultArgType == null); + if(MathUtils.isEven(argTypes.size())) { + CClassType defaultArgType = argTypes.get(argTypes.size() - 1); + returnTypes.add(defaultArgType); + allBranchesTerminate &= (defaultArgType == null); + } // Return null if all cases (including the default case) are terminating. if(allBranchesTerminate) { @@ -811,19 +828,21 @@ public CClassType getReturnType(Target t, List argTypes, // Return an occurring return type if all return types extend that type. // TODO - Make this return a multitype instead as soon as all typechecking code supports multitypes. + search: for(CClassType type1 : returnTypes) { if(type1 != null) { for(CClassType type2 : returnTypes) { - if(!InstanceofUtil.isInstanceof(type2, type1, env)) { - break; + if(type2 != null && !InstanceofUtil.isInstanceof(type2, type1, env)) { + continue search; } } + return type1; } - return type1; } + return CClassType.AUTO; } - // Return the super result. + // Return super result. return retType; } @@ -858,7 +877,7 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, } // Handle optional default branch in separate scope. - if((children.size() & 0x01) == 0x00) { // (size % 2) == 0. + if(MathUtils.isEven(children.size())) { analysis.linkScope( analysis.createNewScope(caseParentScope), children.get(children.size() - 1), env, exceptions); } @@ -1220,22 +1239,6 @@ public FunctionSignatures getSignatures() { .param(null, "loopCode", "The code that is executed in the loop.").build(); } - @Override - public CClassType getReturnType(Target t, List argTypes, - List argTargets, Environment env, Set exceptions) { - - // Get return type based on the function signatures. This generates all necessary compile errors. - CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); - - // Override return type with null if the assign or condition is terminating. - if(argTypes.size() == 4 && (argTypes.get(0) == null || argTypes.get(1) == null)) { - return null; - } - - // Return the super result. - return retType; - } - @Override public Class[] thrown() { return new Class[]{CRECastException.class}; @@ -1500,22 +1503,6 @@ public FunctionSignatures getSignatures() { + " false in the first iteration of the loop.").build(); } - @Override - public CClassType getReturnType(Target t, List argTypes, - List argTargets, Environment env, Set exceptions) { - - // Get return type based on the function signatures. This generates all necessary compile errors. - CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); - - // Override return type with null if the assign or condition is terminating. - if(argTypes.size() == 5 && (argTypes.get(0) == null || argTypes.get(1) == null)) { - return null; - } - - // Return the super result. - return retType; - } - @Override public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast, Environment env, Set exceptions) { @@ -2250,22 +2237,6 @@ public FunctionSignatures getSignatures() { .param(null, "code", "The code that is executed in the loop.", true).build(); } - @Override - public CClassType getReturnType(Target t, List argTypes, - List argTargets, Environment env, Set exceptions) { - - // Get return type based on the function signatures. This generates all necessary compile errors. - CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); - - // Override return type with null if the condition is terminating. - if(argTypes.size() == 2 && argTypes.get(0) == null) { - return null; - } - - // Return the super result. - return retType; - } - @Override public boolean useSpecialExec() { return true; @@ -2390,22 +2361,6 @@ public FunctionSignatures getSignatures() { "The loop condition that is checked each time after the code is executed.").build(); } - @Override - public CClassType getReturnType(Target t, List argTypes, - List argTargets, Environment env, Set exceptions) { - - // Get return type based on the function signatures. This generates all necessary compile errors. - CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); - - // Override return type with null if the code branch or condition is terminating. - if(argTypes.size() == 2 && (argTypes.get(0) == null || argTypes.get(1) == null)) { - return null; - } - - // Return the super result. - return retType; - } - @Override public Integer[] numArgs() { return new Integer[]{2}; diff --git a/src/main/java/com/laytonsmith/core/functions/Exceptions.java b/src/main/java/com/laytonsmith/core/functions/Exceptions.java index 9be1861eb..862028a2b 100644 --- a/src/main/java/com/laytonsmith/core/functions/Exceptions.java +++ b/src/main/java/com/laytonsmith/core/functions/Exceptions.java @@ -622,7 +622,10 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) @Override public FunctionSignatures getSignatures() { - // TODO - Define a better way to define "[catchVariable, catchCode]*, [catchCode]" in function signatures. + /* + * TODO - Implement a way to define [catchVariable, catchCode]*, [catchCode] using signatures, and use it here. + * Also check ifelse(), switch() and switch_ic(), as they need the same feature. + */ return new SignatureBuilder(CVoid.TYPE) .param(null, "tryCode", "The try code block.") .varParam(null, "[catchVariable, catchCode]*, [catchCode]", "The catch blocks.").build(); From 4b5c72a033d9f46349896aa907a52484eff69306 Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Fri, 17 Jul 2026 06:07:48 +0200 Subject: [PATCH 10/11] Add none type and missing return false error tests --- .../core/MethodScriptCompilerTest.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java b/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java index 1a34c8796..fc293338b 100644 --- a/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java +++ b/src/test/java/com/laytonsmith/core/MethodScriptCompilerTest.java @@ -1444,4 +1444,126 @@ public void testSoftCastSyntaxCompiles() throws Exception { sa.setLocalEnable(true); MethodScriptCompiler.compile(MethodScriptCompiler.lex(script, env, null, true), env, env.getEnvClasses(), sa); } + + @Test + public void testNoFalseMissingReturnInProcErrors() throws Exception { + String script = """ + + int proc _a() { + return 1; + } + _a(); + int proc _b(@a, @b) { + if(@a) { + return 1; + } else if(@b) { + return 1; + } else { + return 1; + } + } + _b(1, 2); + int proc _c(@a) { + switch(@a) { + case 1: return 1; + case 2: return 1; + default: return 1; + } + } + _c(1); + int proc _d(@a) { + switch_ic(@a) { + case 'a': return 1; + case 'b': return 1; + default: return 1; + } + } + _d('str'); + int proc _e() { + try(return(1), return(1)); + } + _e(); + int proc _f() { + try { + return 1; + } catch(RangeException @ex) { + return 1; + } catch(CastException @ex) { + return 1; + } + } + _f(); + """; + Environment env = Static.GenerateStandaloneEnvironment(); + StaticAnalysis sa = new StaticAnalysis(true); + sa.setLocalEnable(true); + MethodScriptCompiler.compile(MethodScriptCompiler.lex(script, env, null, true), env, env.getEnvClasses(), sa); + } + + @Test + public void testNoFalseNoneTypeErrorsInBranchAssigns() throws Exception { + String script = """ + + boolean @cond = dyn(true); + boolean @cond2 = dyn(true); + string @str = dyn(''); + + int @a1 = if(@cond, 1, 2); + int @a2 = if(@cond, die(), 2); + int @a3 = if(@cond, 1, die()); + + int @b1 = if(@cond) {1} else if(@cond2) {2} else {3} + int @b2 = if(@cond) {die()} else if(@cond2) {2} else {3} + int @b3 = if(@cond) {1} else if(@cond2) {die()} else {3} + int @b4 = if(@cond) {1} else if(@cond2) {2} else {die()} + int @b5 = if(@cond) {die()} else if(@cond2) {die()} else {3} + int @b6 = if(@cond) {die()} else if(@cond2) {2} else {die()} + int @b7 = if(@cond) {1} else if(@cond2) {die()} else {die()} + + int @c1 = switch(@str) { + default: 1 + } + int @c2 = switch(@str) { + case 1: 1 + default: 1 + } + int @c3 = switch(@str) { + case 1: die() + default: 1 + } + int @c4 = switch(@str) { + case 1: 1 + default: die() + } + int @c5 = switch(@str) { + case 1: 1 + case 2: 1 + default: 1 + } + int @c6 = switch(@str) { + case 1: die() + case 2: 1 + default: 1 + } + int @c7 = switch(@str) { + case 1: 1 + case 2: die() + default: 1 + } + int @c8 = switch(@str) { + case 1: 1 + case 2: 1 + default: die() + } + + int @d1 = if(@cond, 1, try(die(), die())); + + int @e1 = if(@cond, 1, try {die()} catch(Exception @ex) {die()}); + int @e2 = if(@cond, 1, try {die()} catch(RangeException @ex) {die()} catch(CastException @ex) {die()}); + """; + 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 e90e4c83fe6be9d134c68b46bf7f2fb730eaee9f Mon Sep 17 00:00:00 2001 From: Pieter12345 Date: Fri, 17 Jul 2026 17:40:57 +0200 Subject: [PATCH 11/11] Implement func sign noneTypeAllowed per param + Fix dor/dand signatures - Implement function signature none type allowed boolean per Param instead of per FunctionSignature. This is reflected in the function signatures string. - Apply none type allowed boolean to all type of Param instead of only to varparams. - Update function signatures of `dand()` and `dor()`. Fixes NPE when calling `dor()` without arguments (given that SA is enabled). --- .../compiler/signature/FunctionSignature.java | 20 ++---- .../core/compiler/signature/Param.java | 34 +++++++++- .../compiler/signature/SignatureBuilder.java | 36 ++++++++-- .../core/functions/BasicLogic.java | 65 +++++++++++++++++-- 4 files changed, 132 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java b/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java index 73dedf58a..53c67cec4 100644 --- a/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java +++ b/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java @@ -20,21 +20,17 @@ public class FunctionSignature { private final ReturnType returnType; private final List params; private final List throwsList; - private boolean noneIsAllowed; /** * Creates a new {@link FunctionSignature} with the given properties. * @param returnType - The function return type. * @param params - The function parameters. * @param throwsList - The list of possibly thrown exceptions by the function. - * @param noneIsAllowed - If the none (Java {@code null}) type is allowed and should be treated as - * {@link CClassType.AUTO}. */ - public FunctionSignature(ReturnType returnType, List params, List throwsList, boolean noneIsAllowed) { + public FunctionSignature(ReturnType returnType, List params, List throwsList) { this.returnType = returnType; this.params = params; this.throwsList = throwsList; - this.noneIsAllowed = noneIsAllowed; } /** @@ -43,7 +39,7 @@ public FunctionSignature(ReturnType returnType, List params, List * @param returnType - The function return type. */ public FunctionSignature(ReturnType returnType) { - this(returnType, new ArrayList<>(), new ArrayList<>(), false); + this(returnType, new ArrayList<>(), new ArrayList<>()); } /** @@ -61,10 +57,6 @@ protected void addThrows(Throws throwsObj) { this.throwsList.add(throwsObj); } - protected void setNoneIsAllowed(boolean noneIsAllowed) { - this.noneIsAllowed = noneIsAllowed; - } - /** * Gets the function's return type. * @return The return type. @@ -109,7 +101,8 @@ public boolean matches(List argTypes, Environment env, boolean allow // Match normal or optional parameter. if(argIndex < argTypes.size() - && InstanceofUtil.isInstanceof(argTypes.get(argIndex), param.getType(), env)) { + && ((param.getNoneTypeAllowed() && argTypes.get(argIndex) == null) + || InstanceofUtil.isInstanceof(argTypes.get(argIndex), param.getType(), env))) { // Keep track of the optional parameter match. if(param.isOptional()) { @@ -130,7 +123,7 @@ public boolean matches(List argTypes, Environment env, boolean allow // Match as many arguments as possible with this varparam. int numMatches = 0; while(argIndex < argTypes.size() - && ((argTypes.get(argIndex) == null && this.noneIsAllowed) + && ((param.getNoneTypeAllowed() && argTypes.get(argIndex) == null) || InstanceofUtil.isInstanceof(argTypes.get(argIndex), param.getType(), env))) { argIndex++; numMatches++; @@ -172,7 +165,8 @@ public boolean matches(List argTypes, Environment env, boolean allow */ public String getParamTypesString() { return "(" + StringUtils.Join(this.params, ", ", null, null, null, (Param param) -> { - String ret = (param.getType() == null ? "any" : param.getType().getSimpleName()); + String ret = (param.getType() == null ? "any" : (param.getNoneTypeAllowed() + ? param.getType().getSimpleName() + "|none" : param.getType().getSimpleName())); if(param.isVarParam()) { ret += "..."; } diff --git a/src/main/java/com/laytonsmith/core/compiler/signature/Param.java b/src/main/java/com/laytonsmith/core/compiler/signature/Param.java index 9a1642ead..a74189111 100644 --- a/src/main/java/com/laytonsmith/core/compiler/signature/Param.java +++ b/src/main/java/com/laytonsmith/core/compiler/signature/Param.java @@ -13,6 +13,7 @@ public class Param { private final String desc; private final boolean isVarParam; private final boolean isOptional; + private final boolean noneTypeAllowed; /** * Creates a new {@link Param} with the given properties. @@ -24,14 +25,34 @@ public class Param { * of the given type, or one argument of type {@code array}. {@code false} otherwise. * Note that a varparam is only usable as type {@code array}. * @param isOptional - {@code true} if the parameter is optional, {@code false} otherwise. + * @param noneTypeAllowed - {@code true} if the none (Java {@code null}) type is allowed to match this parameter in + * addition to what the parameter type already matches. + * {@code false} otherwise. If the parameter type is {@code null}, then this boolean parameter is ignored. */ - public Param(CClassType type, String name, String desc, boolean isVarParam, boolean isOptional) { + public Param(CClassType type, String name, String desc, + boolean isVarParam, boolean isOptional, boolean noneTypeAllowed) { assert !isVarParam || !isOptional : "A parameter cannot be variadic and optional at the same time."; this.type = type; this.name = name; this.desc = desc; this.isVarParam = isVarParam; this.isOptional = isOptional; + this.noneTypeAllowed = noneTypeAllowed; + } + + /** + * Creates a new {@link Param} with the given properties. + * Parameters cannot be variadic and optional at the same time, as varparams already imply optionality. + * @param type - The (parent) type of the parameter. + * @param name - The name of the parameter. + * @param desc - The description of the parameter. + * @param isVarParam - {@code true} if the parameter is a varparam, meaning that it matches zero or more arguments + * of the given type, or one argument of type {@code array}. {@code false} otherwise. + * Note that a varparam is only usable as type {@code array}. + * @param isOptional - {@code true} if the parameter is optional, {@code false} otherwise. + */ + public Param(CClassType type, String name, String desc, boolean isVarParam, boolean isOptional) { + this(type, name, desc, isVarParam, isOptional, false); } /** @@ -85,4 +106,15 @@ public boolean isVarParam() { public boolean isOptional() { return this.isOptional; } + + /** + * Gets whether the none type is allowed to match this parameter in addition to what the parameter type matches. + * When this returns {@code false}, the none type can still match this parameter if {@ref #getType()} returns + * {@code null}. + * @return {@code true} if a none type argument can match this parameter in addition to what the parameter type + * matches, {@code false} otherwise. + */ + public boolean getNoneTypeAllowed() { + return this.noneTypeAllowed; + } } diff --git a/src/main/java/com/laytonsmith/core/compiler/signature/SignatureBuilder.java b/src/main/java/com/laytonsmith/core/compiler/signature/SignatureBuilder.java index 41a6b490c..77603585a 100644 --- a/src/main/java/com/laytonsmith/core/compiler/signature/SignatureBuilder.java +++ b/src/main/java/com/laytonsmith/core/compiler/signature/SignatureBuilder.java @@ -56,6 +56,22 @@ public SignatureBuilder(CClassType returnType, String returnValDesc, MatchType m this.signature = new FunctionSignature(new ReturnType(returnType, returnValDesc)); } + /** + * Adds a normal function parameter. Parameters should be added from left to right. + * @param paramType - The {@link CClassType} of the parameter. + * @param paramName - The name of the parameter. + * @param paramDesc - The description of the parameter. + * @param isOptional - Whether the parameter is optional or not. + * @param noneTypeAllowed - Whether a none type (Java {@code null}) is allowed to match this parameter in addition + * to what the parameter type already matches. + * @return This {@link SignatureBuilder}, for chaining builder methods. + */ + public SignatureBuilder param( + CClassType paramType, String paramName, String paramDesc, boolean isOptional, boolean noneTypeAllowed) { + this.signature.addParam(new Param(paramType, paramName, paramDesc, false, isOptional, noneTypeAllowed)); + return this; + } + /** * Adds a normal function parameter. Parameters should be added from left to right. * @param paramType - The {@link CClassType} of the parameter. @@ -80,6 +96,21 @@ public SignatureBuilder param(CClassType paramType, String paramName, String par return this.param(paramType, paramName, paramDesc, false); } + /** + * Adds a variadic function parameter (varparam). Parameters should be added from left to right. + * @param paramType - The {@link CClassType} of the parameter (the 'paramType' in 'paramType... paramName'). + * @param paramName - The name of the parameter. + * @param paramDesc - The description of the parameter. + * @param noneTypeAllowed - Whether a none type (Java {@code null}) is allowed to match an entry in this parameter + * in addition to what the parameter type already matches. + * @return This {@link SignatureBuilder}, for chaining builder methods. + */ + public SignatureBuilder varParam( + CClassType paramType, String paramName, String paramDesc, boolean noneTypeAllowed) { + this.signature.addParam(new Param(paramType, paramName, paramDesc, true, false, noneTypeAllowed)); + return this; + } + /** * Adds a variadic function parameter (varparam). Parameters should be added from left to right. * @param paramType - The {@link CClassType} of the parameter (the 'paramType' in 'paramType... paramName'). @@ -103,11 +134,6 @@ public SignatureBuilder throwsEx(Class exception, String return this; } - public SignatureBuilder setNoneIsAllowed(boolean allowed) { - this.signature.setNoneIsAllowed(allowed); - return this; - } - /** * Finalizes the last function signature and starts a new function signature with the given return type. * @param returnType - The return type of the new function signature. diff --git a/src/main/java/com/laytonsmith/core/functions/BasicLogic.java b/src/main/java/com/laytonsmith/core/functions/BasicLogic.java index 064028027..e5844cbd4 100644 --- a/src/main/java/com/laytonsmith/core/functions/BasicLogic.java +++ b/src/main/java/com/laytonsmith/core/functions/BasicLogic.java @@ -16,7 +16,6 @@ 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.Auto; import com.laytonsmith.core.constructs.CArray; import com.laytonsmith.core.constructs.CBoolean; import com.laytonsmith.core.constructs.CClassType; @@ -28,6 +27,7 @@ import com.laytonsmith.core.constructs.CString; import com.laytonsmith.core.constructs.CSymbol; import com.laytonsmith.core.constructs.CVoid; +import com.laytonsmith.core.constructs.InstanceofUtil; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.GlobalEnv; @@ -1275,7 +1275,37 @@ public FunctionSignatures getSignatures() { * Note that getReturnType could be overridden, not using this signature for typechecking. * That implementation is not yet possible until A OR B OR ... types can be described. */ - return new SignatureBuilder(CClassType.AUTO).varParam(Mixed.TYPE, "vals", null).build(); + return new SignatureBuilder(CClassType.AUTO) + .varParam(Mixed.TYPE, "vals", "The values.") + .newSignature(CClassType.AUTO) + .param(Mixed.TYPE, "val", "The first value.") + .varParam(Mixed.TYPE, "vals", "The values.") + .param(Mixed.TYPE, "termVal", "The final terminating (non-returning) value.", true, true).build(); + } + + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Return an occurring argument type if all argument types extend that type. + // TODO - Make this return a multitype instead as soon as all typechecking code supports multitypes. + search: + for(CClassType type1 : argTypes) { + if(type1 != null) { + for(CClassType type2 : argTypes) { + if(type2 != null && !InstanceofUtil.isInstanceof(type2, type1, env)) { + continue search; + } + } + return type1; + } + } + + // Return super result. + return retType; } @Override @@ -1570,8 +1600,35 @@ public FunctionSignatures getSignatures() { * Note that getReturnType could be overridden, not using this signature for typechecking. * That implementation is not yet possible until A OR B OR ... types can be described. */ - return new SignatureBuilder(Auto.TYPE).varParam(Mixed.TYPE, "vals", null) - .setNoneIsAllowed(true).build(); + return new SignatureBuilder(CClassType.AUTO) + .param(Mixed.TYPE, "val", "The first value.") + .varParam(Mixed.TYPE, "vals", "The values.") + .param(Mixed.TYPE, "termVal", "The final terminating (non-returning) value.", true, true).build(); + } + + @Override + public CClassType getReturnType(Target t, List argTypes, + List argTargets, Environment env, Set exceptions) { + + // Get return type based on the function signatures. This generates all necessary compile errors. + CClassType retType = super.getReturnType(t, argTypes, argTargets, env, exceptions); + + // Return an occurring argument type if all argument types extend that type. + // TODO - Make this return a multitype instead as soon as all typechecking code supports multitypes. + search: + for(CClassType type1 : argTypes) { + if(type1 != null) { + for(CClassType type2 : argTypes) { + if(type2 != null && !InstanceofUtil.isInstanceof(type2, type1, env)) { + continue search; + } + } + return type1; + } + } + + // Return super result. + return retType; } @Override