From 4575c8386abd638e6cb7886b0a99350e523e7e8d Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Sat, 11 Oct 2025 21:32:21 +0200
Subject: [PATCH 1/2] Typecheck return()
Typecheck `return;` and `return @val;` value against its expected type from the surrounding procedure, closure or bind.
---
.../compiler/analysis/IncludeReference.java | 2 +-
.../core/compiler/analysis/Namespace.java | 3 +-
.../analysis/ReturnableDeclaration.java | 22 +++++++
.../analysis/ReturnableReference.java | 18 ++++++
.../core/functions/ControlFlow.java | 58 +++++++++++++++++++
.../core/functions/DataHandling.java | 19 +++++-
.../core/functions/EventBinding.java | 4 ++
7 files changed, 121 insertions(+), 5 deletions(-)
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableDeclaration.java
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableReference.java
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/IncludeReference.java b/src/main/java/com/laytonsmith/core/compiler/analysis/IncludeReference.java
index a1ea4cccb1..df1c72d173 100644
--- a/src/main/java/com/laytonsmith/core/compiler/analysis/IncludeReference.java
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/IncludeReference.java
@@ -18,7 +18,7 @@ public class IncludeReference extends Reference {
* @param identifier - The include identifier (the path passed to include(), which should resolve to a file).
* @param inScope - The parent scope which the include should be linked to.
* @param outScope - The scope in which the include should be usable.
- * @param t - The target of the declaration.
+ * @param t - The target of the reference.
*/
public IncludeReference(String identifier, Scope inScope, Scope outScope, Target t) {
super(Namespace.INCLUDE, identifier, t);
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java b/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java
index cb0e1fdcfa..fcf9c47bac 100644
--- a/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java
@@ -9,5 +9,6 @@ public enum Namespace {
IVARIABLE,
IVARIABLE_ASSIGN,
PROCEDURE,
- INCLUDE
+ INCLUDE,
+ RETURNABLE
}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableDeclaration.java b/src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableDeclaration.java
new file mode 100644
index 0000000000..9040fc2914
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableDeclaration.java
@@ -0,0 +1,22 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.NodeModifiers;
+import com.laytonsmith.core.constructs.CClassType;
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a returnable declaration in a scope graph.
+ * @author P.J.S. Kools
+ */
+public class ReturnableDeclaration extends Declaration {
+
+ /**
+ * Creates a new {@link ReturnableDeclaration} in the {@link Namespace#RETURNABLE} namespace.
+ * @param type The expected return {@link CClassType}.
+ * @param modifiers The node modifiers.
+ * @param t The returnable target.
+ */
+ public ReturnableDeclaration(CClassType type, NodeModifiers modifiers, Target t) {
+ super(Namespace.RETURNABLE, null, type, modifiers, t);
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableReference.java b/src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableReference.java
new file mode 100644
index 0000000000..af63eacb33
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/ReturnableReference.java
@@ -0,0 +1,18 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a reference to a returnable in a scope graph.
+ * @author P.J.S. Kools
+ */
+public class ReturnableReference extends Reference {
+
+ /**
+ * Creates a new {@link ReturnableReference} in the {@link Namespace#RETURNABLE} scope.
+ * @param t - The target of the reference.
+ */
+ public ReturnableReference(Target t) {
+ super(Namespace.RETURNABLE, null, t);
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java
index 0883b07c8f..cdd3919f79 100644
--- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java
+++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java
@@ -23,6 +23,9 @@
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.compiler.SelfStatement;
import com.laytonsmith.core.compiler.VariableScope;
+import com.laytonsmith.core.compiler.analysis.Declaration;
+import com.laytonsmith.core.compiler.analysis.Namespace;
+import com.laytonsmith.core.compiler.analysis.ReturnableReference;
import com.laytonsmith.core.compiler.analysis.Scope;
import com.laytonsmith.core.compiler.analysis.StaticAnalysis;
import com.laytonsmith.core.compiler.signature.FunctionSignatures;
@@ -2411,11 +2414,66 @@ public String docs() {
+ " see the docs on try/catch, particularly the finally clause for example).";
}
+ @Override
+ public CClassType typecheck(StaticAnalysis analysis,
+ ParseTree ast, Environment env, Set exceptions) {
+
+ // Get value type.
+ CClassType valType;
+ Target valTarget;
+ if(ast.numberOfChildren() == 0) {
+ valType = CVoid.TYPE;
+ valTarget = ast.getTarget();
+ } else if(ast.numberOfChildren() == 1) {
+ ParseTree valNode = ast.getChildAt(0);
+ valType = analysis.typecheck(valNode, env, exceptions);
+ valTarget = valNode.getTarget();
+ } else {
+
+ // Fall back to default behavior for invalid usage.
+ return super.typecheck(analysis, ast, env, exceptions);
+ }
+
+ // Resolve this returnable reference to its returnable declaration to get its required return type.
+ Scope scope = analysis.getTermScope(ast);
+ if(scope != null) {
+ Set decls = scope.getDeclarations(Namespace.RETURNABLE, null);
+ if(decls.size() == 0) {
+ exceptions.add(new ConfigCompileException("Return is not valid in this context.", ast.getTarget()));
+ } else {
+
+ // Type check return value for all found declared return types.
+ for(Declaration decl : decls) {
+ StaticAnalysis.requireType(valType, decl.getType(), valTarget, env, exceptions);
+ }
+ }
+ }
+
+ // Return void.
+ return CVoid.TYPE;
+ }
+
@Override
public Class extends CREThrowable>[] thrown() {
return null;
}
+ @Override
+ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast,
+ Environment env, Set exceptions) {
+
+ // Handle children. These will execute before this return().
+ Scope scope = super.linkScope(analysis, parentScope, ast, env, exceptions);
+
+ // Add returnable reference in new scope.
+ scope = analysis.createNewScope(scope);
+ scope.addReference(new ReturnableReference(ast.getTarget()));
+ analysis.setTermScope(ast, scope);
+
+ // Return scope.
+ return scope;
+ }
+
@Override
public boolean isRestricted() {
return false;
diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
index d07be64451..993971e8e4 100644
--- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java
+++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
@@ -40,6 +40,7 @@
import com.laytonsmith.core.compiler.analysis.ProcDeclaration;
import com.laytonsmith.core.compiler.analysis.ProcRootDeclaration;
import com.laytonsmith.core.compiler.analysis.Reference;
+import com.laytonsmith.core.compiler.analysis.ReturnableDeclaration;
import com.laytonsmith.core.compiler.analysis.Scope;
import com.laytonsmith.core.compiler.analysis.StaticAnalysis;
import com.laytonsmith.core.compiler.signature.FunctionSignatures;
@@ -1626,6 +1627,9 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast
// Allow procedures to perform lookups in the decl scope.
paramScope.addSpecificParent(declScope, Namespace.PROCEDURE);
+ // Create returnable declaration in the inner root scope.
+ paramScope.addDeclaration(new ReturnableDeclaration(retType, ast.getNodeModifiers(), ast.getTarget()));
+
// Return the declaration scope. Parameters and their default values are not accessible after the procedure.
return declScope;
}
@@ -2703,10 +2707,16 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast
return parentScope;
}
- // Handle optional return type argument.
+ // Handle optional return type argument (CClassType or CVoid, default to AUTO).
int ind = 0;
- if(ast.getChildAt(ind).getData().isInstanceOf(CClassType.TYPE)) {
- analysis.linkScope(parentScope, ast.getChildAt(ind++), env, exceptions);
+ CClassType retType;
+ if(ast.getChildAt(ind).getData() instanceof CClassType) {
+ retType = (CClassType) ast.getChildAt(ind++).getData();
+ } else if(ast.getChildAt(ind).getData().equals(CVoid.VOID)) {
+ ind++;
+ retType = CVoid.TYPE;
+ } else {
+ retType = CClassType.AUTO;
}
// Create parameter scope. Set parent scope if this closure type is allowed to resolve in the parent scope.
@@ -2732,6 +2742,9 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast
paramScope = scopes[0];
}
+ // Create returnable declaration in the inner root scope.
+ paramScope.addDeclaration(new ReturnableDeclaration(retType, ast.getNodeModifiers(), ast.getTarget()));
+
// Handle closure code.
ParseTree code = ast.getChildAt(ast.numberOfChildren() - 1);
analysis.linkScope(paramScope, code, env, exceptions);
diff --git a/src/main/java/com/laytonsmith/core/functions/EventBinding.java b/src/main/java/com/laytonsmith/core/functions/EventBinding.java
index e35efe281d..f562c596a8 100644
--- a/src/main/java/com/laytonsmith/core/functions/EventBinding.java
+++ b/src/main/java/com/laytonsmith/core/functions/EventBinding.java
@@ -19,6 +19,7 @@
import com.laytonsmith.core.compiler.SelfStatement;
import com.laytonsmith.core.compiler.VariableScope;
import com.laytonsmith.core.compiler.analysis.Namespace;
+import com.laytonsmith.core.compiler.analysis.ReturnableDeclaration;
import com.laytonsmith.core.compiler.analysis.Scope;
import com.laytonsmith.core.compiler.analysis.StaticAnalysis;
import com.laytonsmith.core.constructs.CArray;
@@ -337,6 +338,9 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
}
analysis.linkScope(paramScope, code, env, exceptions);
+ // Create returnable declaration in the inner root scope.
+ paramScope.addDeclaration(new ReturnableDeclaration(CVoid.TYPE, ast.getNodeModifiers(), ast.getTarget()));
+
// Allow code after bind() to access declarations in assigned values, but not parameters themselves.
return valScope;
}
From 17cdf6f1145d680876a79153230727e4874f17a9 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Sun, 12 Oct 2025 01:59:35 +0200
Subject: [PATCH 2/2] Validate break() and continue()
Validate that `break()` and `continue()` are used within breakable / continuable code blocks during static analysis.
---
.../analysis/BreakableBoundDeclaration.java | 21 ++
.../analysis/BreakableDeclaration.java | 28 ++
.../compiler/analysis/BreakableReference.java | 18 ++
.../analysis/ContinuableBoundDeclaration.java | 21 ++
.../analysis/ContinuableDeclaration.java | 20 ++
.../analysis/ContinuableReference.java | 18 ++
.../core/compiler/analysis/Namespace.java | 4 +-
.../core/functions/ControlFlow.java | 255 +++++++++++++++++-
.../core/functions/DataHandling.java | 9 +-
.../core/functions/EventBinding.java | 11 +-
10 files changed, 389 insertions(+), 16 deletions(-)
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/BreakableBoundDeclaration.java
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/BreakableDeclaration.java
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/BreakableReference.java
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableBoundDeclaration.java
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableDeclaration.java
create mode 100644 src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableReference.java
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableBoundDeclaration.java b/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableBoundDeclaration.java
new file mode 100644
index 0000000000..d03557605b
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableBoundDeclaration.java
@@ -0,0 +1,21 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.NodeModifiers;
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a breakable (for(), while(), switch(), etc) bound declaration in a scope graph.
+ * This indicates a boundary where lookup for a breakable should stop.
+ * @author P.J.S. Kools
+ */
+public class BreakableBoundDeclaration extends Declaration {
+
+ /**
+ * Creates a new {@link BreakableBoundDeclaration} in the {@link Namespace#BREAKABLE} namespace.
+ * @param modifiers The node modifiers.
+ * @param t The breakable target.
+ */
+ public BreakableBoundDeclaration(NodeModifiers modifiers, Target t) {
+ super(Namespace.BREAKABLE, null, null, modifiers, t);
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableDeclaration.java b/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableDeclaration.java
new file mode 100644
index 0000000000..c794eadfd6
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableDeclaration.java
@@ -0,0 +1,28 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.NodeModifiers;
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a breakable (for(), while(), switch(), etc) declaration in a scope graph.
+ * @author P.J.S. Kools
+ */
+public class BreakableDeclaration extends Declaration {
+
+ private final Scope parentScope;
+
+ /**
+ * Creates a new {@link BreakableDeclaration} in the {@link Namespace#BREAKABLE} namespace.
+ * @param parentScope The parent scope of the breakable (that does not include this declaration).
+ * @param modifiers The node modifiers.
+ * @param t The breakable target.
+ */
+ public BreakableDeclaration(Scope parentScope, NodeModifiers modifiers, Target t) {
+ super(Namespace.BREAKABLE, null, null, modifiers, t);
+ this.parentScope = parentScope;
+ }
+
+ public Scope getParentScope() {
+ return this.parentScope;
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableReference.java b/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableReference.java
new file mode 100644
index 0000000000..5260266e4a
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/BreakableReference.java
@@ -0,0 +1,18 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a reference to a breakable (by the break() function) in a scope graph.
+ * @author P.J.S. Kools
+ */
+public class BreakableReference extends Reference {
+
+ /**
+ * Creates a new {@link BreakableReference} in the {@link Namespace#BREAKABLE} scope.
+ * @param t - The target of the reference.
+ */
+ public BreakableReference(Target t) {
+ super(Namespace.BREAKABLE, null, t);
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableBoundDeclaration.java b/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableBoundDeclaration.java
new file mode 100644
index 0000000000..7b327fd5ab
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableBoundDeclaration.java
@@ -0,0 +1,21 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.NodeModifiers;
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a continuable (for(), while(), etc) bound declaration in a scope graph.
+ * This indicates a boundary where lookup for a continuable should stop.
+ * @author P.J.S. Kools
+ */
+public class ContinuableBoundDeclaration extends Declaration {
+
+ /**
+ * Creates a new {@link ContinuableBoundDeclaration} in the {@link Namespace#CONTINUABLE} namespace.
+ * @param modifiers The node modifiers.
+ * @param t The continuable target.
+ */
+ public ContinuableBoundDeclaration(NodeModifiers modifiers, Target t) {
+ super(Namespace.CONTINUABLE, null, null, modifiers, t);
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableDeclaration.java b/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableDeclaration.java
new file mode 100644
index 0000000000..e9c9d78a15
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableDeclaration.java
@@ -0,0 +1,20 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.NodeModifiers;
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a continuable (for(), while(), etc) declaration in a scope graph.
+ * @author P.J.S. Kools
+ */
+public class ContinuableDeclaration extends Declaration {
+
+ /**
+ * Creates a new {@link ContinuableDeclaration} in the {@link Namespace#CONTINUABLE} namespace.
+ * @param modifiers The node modifiers.
+ * @param t The continuable target.
+ */
+ public ContinuableDeclaration(NodeModifiers modifiers, Target t) {
+ super(Namespace.CONTINUABLE, null, null, modifiers, t);
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableReference.java b/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableReference.java
new file mode 100644
index 0000000000..48c65cab81
--- /dev/null
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/ContinuableReference.java
@@ -0,0 +1,18 @@
+package com.laytonsmith.core.compiler.analysis;
+
+import com.laytonsmith.core.constructs.Target;
+
+/**
+ * Represents a reference to a continuable (by the continue()) function in a scope graph.
+ * @author P.J.S. Kools
+ */
+public class ContinuableReference extends Reference {
+
+ /**
+ * Creates a new {@link ContinuableReference} in the {@link Namespace#CONTINUABLE} scope.
+ * @param t - The target of the reference.
+ */
+ public ContinuableReference(Target t) {
+ super(Namespace.CONTINUABLE, null, t);
+ }
+}
diff --git a/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java b/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java
index fcf9c47bac..92f5e3738f 100644
--- a/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java
+++ b/src/main/java/com/laytonsmith/core/compiler/analysis/Namespace.java
@@ -10,5 +10,7 @@ public enum Namespace {
IVARIABLE_ASSIGN,
PROCEDURE,
INCLUDE,
- RETURNABLE
+ RETURNABLE,
+ BREAKABLE,
+ CONTINUABLE
}
diff --git a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java
index cdd3919f79..9a75e0ab30 100644
--- a/src/main/java/com/laytonsmith/core/functions/ControlFlow.java
+++ b/src/main/java/com/laytonsmith/core/functions/ControlFlow.java
@@ -23,7 +23,13 @@
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.compiler.SelfStatement;
import com.laytonsmith.core.compiler.VariableScope;
+import com.laytonsmith.core.compiler.analysis.BreakableReference;
+import com.laytonsmith.core.compiler.analysis.ContinuableBoundDeclaration;
+import com.laytonsmith.core.compiler.analysis.ContinuableDeclaration;
+import com.laytonsmith.core.compiler.analysis.ContinuableReference;
import com.laytonsmith.core.compiler.analysis.Declaration;
+import com.laytonsmith.core.compiler.analysis.BreakableBoundDeclaration;
+import com.laytonsmith.core.compiler.analysis.BreakableDeclaration;
import com.laytonsmith.core.compiler.analysis.Namespace;
import com.laytonsmith.core.compiler.analysis.ReturnableReference;
import com.laytonsmith.core.compiler.analysis.Scope;
@@ -720,26 +726,41 @@ public FunctionSignatures getSignatures() {
@Override
public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
ParseTree ast, Environment env, Set exceptions) {
+ if(ast.numberOfChildren() == 0) {
+ return super.linkScope(analysis, parentScope, ast, env, exceptions);
+ }
+
+ // Handle value in parent scope.
+ ParseTree val = ast.getChildAt(0);
+ Scope valScope = analysis.linkScope(parentScope, val, env, exceptions);
+
+ // Add breakable declaration in case parent scope.
+ Scope caseParentScope = analysis.createNewScope(valScope);
+ caseParentScope.addDeclaration(
+ new BreakableDeclaration(parentScope, ast.getNodeModifiers(), ast.getTarget()));
+ analysis.setTermScope(ast, caseParentScope);
+
+ // Handle switch cases scoping.
List children = ast.getChildren();
- for(int i = 0; i <= children.size() - 2; i += 2) {
+ for(int i = 1; i <= children.size() - 2; i += 2) {
ParseTree cond = children.get(i);
ParseTree code = children.get(i + 1);
// Handle condition in parent scope.
- Scope condScope = analysis.linkScope(parentScope, cond, env, exceptions);
+ Scope condScope = analysis.linkScope(caseParentScope, cond, env, exceptions);
// Handle code branches in separate scope.
analysis.linkScope(analysis.createNewScope(condScope), code, env, exceptions);
}
// Handle optional default branch in separate scope.
- if((children.size() & 0x01) == 0x01) { // (size % 2) == 1.
+ if((children.size() & 0x01) == 0x00) { // (size % 2) == 0.
analysis.linkScope(
- analysis.createNewScope(parentScope), children.get(children.size() - 1), env, exceptions);
+ analysis.createNewScope(caseParentScope), children.get(children.size() - 1), env, exceptions);
}
- // Return the parent scope.
- return parentScope;
+ // Return the switch scope.
+ return caseParentScope;
}
@Override
@@ -1103,7 +1124,33 @@ public Class extends CREThrowable>[] thrown() {
@Override
public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
ParseTree ast, Environment env, Set exceptions) {
- super.linkScope(analysis, parentScope, ast, env, exceptions);
+
+ // Fall back to default behavior for invalid usage.
+ if(ast.numberOfChildren() != 4) {
+ return super.linkScope(analysis, parentScope, ast, env, exceptions);
+ }
+
+ // Link child scopes.
+ ParseTree assign = ast.getChildAt(0);
+ ParseTree cond = ast.getChildAt(1);
+ ParseTree exp = ast.getChildAt(2);
+ ParseTree code = ast.getChildAt(3);
+
+ // Link scopes in order: assign -> cond -> (code -> exp -> cond)*.
+ Scope assignScope = analysis.linkScope(parentScope, assign, env, exceptions);
+ Scope condScope = analysis.linkScope(assignScope, cond, env, exceptions);
+ Scope codeParentScope = analysis.createNewScope(condScope);
+ Scope codeScope = analysis.linkScope(codeParentScope, code, env, exceptions);
+ analysis.linkScope(codeScope, exp, env, exceptions);
+
+ // Add breakable and continuable declaration in loop code parent scope.
+ codeParentScope.addDeclaration(
+ new BreakableDeclaration(parentScope, ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(
+ new ContinuableDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ analysis.setTermScope(ast, codeParentScope);
+
+ // Return parent scope.
return parentScope;
}
@@ -1345,11 +1392,19 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
// Order: assign -> cond -> (code -> exp -> cond)* -> elseCode?.
Scope assignScope = analysis.linkScope(parentScope, assign, env, exceptions);
Scope condScope = analysis.linkScope(assignScope, cond, env, exceptions);
- Scope codeScope = analysis.linkScope(condScope, code, env, exceptions);
+ Scope codeParentScope = analysis.createNewScope(condScope);
+ Scope codeScope = analysis.linkScope(codeParentScope, code, env, exceptions);
analysis.linkScope(codeScope, exp, env, exceptions);
if(elseCode != null) {
analysis.linkScope(condScope, code, env, exceptions);
}
+
+ // Add breakable and continuable declaration in loop code parent scope.
+ codeParentScope.addDeclaration(
+ new BreakableDeclaration(parentScope, ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(
+ new ContinuableDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ analysis.setTermScope(ast, codeParentScope);
}
return parentScope;
}
@@ -1594,7 +1649,15 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
}
Scope[] scopes = analysis.linkParamScope(keyParamScope, keyValScope, val, env, exceptions);
Scope valScope = scopes[0]; // paramScope.
- analysis.linkScope(valScope, code, env, exceptions);
+ Scope codeParentScope = analysis.createNewScope(valScope);
+ analysis.linkScope(codeParentScope, code, env, exceptions);
+
+ // Add breakable and continuable declaration in loop code parent scope.
+ codeParentScope.addDeclaration(
+ new BreakableDeclaration(parentScope, ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(
+ new ContinuableDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ analysis.setTermScope(ast, codeParentScope);
}
return parentScope;
}
@@ -1885,8 +1948,16 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
}
Scope[] scopes = analysis.linkParamScope(keyParamScope, keyValScope, val, env, exceptions);
Scope valScope = scopes[0]; // paramScope.
- analysis.linkScope(valScope, code, env, exceptions);
+ Scope codeParentScope = analysis.createNewScope(valScope);
+ analysis.linkScope(codeParentScope, code, env, exceptions);
analysis.linkScope(arrayScope, elseCode, env, exceptions);
+
+ // Add breakable and continuable declaration in loop code parent scope.
+ codeParentScope.addDeclaration(
+ new BreakableDeclaration(parentScope, ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(
+ new ContinuableDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ analysis.setTermScope(ast, codeParentScope);
}
return parentScope;
}
@@ -2033,7 +2104,29 @@ public Mixed exec(Target t, Environment environment, Mixed... args) throws Confi
@Override
public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
ParseTree ast, Environment env, Set exceptions) {
- super.linkScope(analysis, parentScope, ast, env, exceptions);
+
+ // Fall back to default behavior for invalid usage.
+ if(ast.numberOfChildren() != 2) {
+ return super.linkScope(analysis, parentScope, ast, env, exceptions);
+ }
+
+ // Link child scopes.
+ ParseTree cond = ast.getChildAt(0);
+ ParseTree code = ast.getChildAt(1);
+
+ // Link scopes in order: cond -> (code -> cond)*.
+ Scope condScope = analysis.linkScope(parentScope, cond, env, exceptions);
+ Scope codeParentScope = analysis.createNewScope(condScope);
+ analysis.linkScope(codeParentScope, code, env, exceptions);
+
+ // Add breakable and continuable declaration in loop code parent scope.
+ codeParentScope.addDeclaration(
+ new BreakableDeclaration(parentScope, ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(
+ new ContinuableDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ analysis.setTermScope(ast, codeParentScope);
+
+ // Return parent scope.
return parentScope;
}
@@ -2167,7 +2260,29 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes)
@Override
public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
ParseTree ast, Environment env, Set exceptions) {
- super.linkScope(analysis, parentScope, ast, env, exceptions);
+
+ // Fall back to default behavior for invalid usage.
+ if(ast.numberOfChildren() != 2) {
+ return super.linkScope(analysis, parentScope, ast, env, exceptions);
+ }
+
+ // Link child scopes.
+ ParseTree code = ast.getChildAt(0);
+ ParseTree cond = ast.getChildAt(1);
+
+ // Link scopes in order: code -> (cond -> code)*.
+ Scope codeParentScope = analysis.createNewScope(parentScope);
+ analysis.linkScope(codeParentScope, code, env, exceptions);
+ analysis.linkScope(parentScope, cond, env, exceptions);
+
+ // Add breakable and continuable declaration in loop code parent scope.
+ codeParentScope.addDeclaration(
+ new BreakableDeclaration(parentScope, ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(
+ new ContinuableDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ analysis.setTermScope(ast, codeParentScope);
+
+ // Return parent scope.
return parentScope;
}
@@ -2235,11 +2350,83 @@ public String docs() {
+ " or a compile error will occur. An integer must be hard coded into the function.";
}
+ @Override
+ public CClassType typecheck(StaticAnalysis analysis,
+ ParseTree ast, Environment env, Set exceptions) {
+
+ // Typecheck function.
+ super.typecheck(analysis, ast, env, exceptions);
+
+ // Get break count.
+ long breakCount;
+ if(ast.numberOfChildren() >= 1 && ast.getChildAt(0).getData() instanceof CInt breakCountInt) {
+ breakCount = breakCountInt.getInt();
+ if(breakCount <= 0) {
+ breakCount = 1;
+ }
+ } else {
+ breakCount = 1;
+ }
+
+ // Resolve this breakable reference to a breakable declaration to validate usage of break in this context.
+ Scope scope = analysis.getTermScope(ast);
+ if(scope != null) {
+ long remainingBreakCount = breakCount;
+ Set decls = scope.getDeclarations(Namespace.BREAKABLE, null);
+ while(decls.size() != 0) {
+ assert decls.size() == 1 : "Break reference resolves to multiple breakables at once.";
+ assert decls.iterator().next() instanceof BreakableDeclaration : "Unsupported breakable declaration.";
+ Declaration decl = decls.iterator().next();
+ if(decl instanceof BreakableDeclaration loopDecl) {
+ if(--remainingBreakCount == 0) {
+ break;
+ }
+ decls = loopDecl.getParentScope().getDeclarations(Namespace.BREAKABLE, null);
+ } else if(decl instanceof BreakableBoundDeclaration) {
+ break;
+ } else {
+ throw new Error("Unsupported " + Namespace.BREAKABLE + " declaration.");
+ }
+ }
+ if(remainingBreakCount != 0) {
+ if(breakCount == remainingBreakCount) {
+ exceptions.add(new ConfigCompileException(
+ "Break is not valid in this context.", ast.getTarget()));
+ } else {
+ long breaksAvailable = breakCount - remainingBreakCount;
+ exceptions.add(new ConfigCompileException(
+ "Cannot break from " + breakCount + " breakables. Only " + breaksAvailable
+ + " nested breakable" + (breaksAvailable == 1 ? " is" : "s are")
+ + " available here.", ast.getTarget()));
+ }
+ }
+ }
+
+ // Return void.
+ return CVoid.TYPE;
+ }
+
@Override
public Class extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class};
}
+ @Override
+ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast,
+ Environment env, Set exceptions) {
+
+ // Handle children. These will execute before this break().
+ Scope scope = super.linkScope(analysis, parentScope, ast, env, exceptions);
+
+ // Add break loop reference in new scope.
+ scope = analysis.createNewScope(scope);
+ scope.addReference(new BreakableReference(ast.getTarget()));
+ analysis.setTermScope(ast, scope);
+
+ // Return scope.
+ return scope;
+ }
+
@Override
public boolean isRestricted() {
return false;
@@ -2339,11 +2526,55 @@ public String docs() {
+ " If int is set, it will skip 'int' repetitions. If no argument is specified, 1 is used.";
}
+ @Override
+ public CClassType typecheck(StaticAnalysis analysis,
+ ParseTree ast, Environment env, Set exceptions) {
+
+ // Typecheck function.
+ super.typecheck(analysis, ast, env, exceptions);
+
+ // Resolve this continuable reference to a continuable declaration to validate usage of continue in this context.
+ Scope scope = analysis.getTermScope(ast);
+ if(scope != null) {
+ Set decls = scope.getDeclarations(Namespace.CONTINUABLE, null);
+ if(decls.size() == 0) {
+ exceptions.add(new ConfigCompileException(
+ "Continue is not valid in this context.", ast.getTarget()));
+ } else {
+ for(Declaration decl : decls) {
+ if(decl instanceof ContinuableBoundDeclaration) {
+ exceptions.add(new ConfigCompileException(
+ "Continue is not valid in this context.", ast.getTarget()));
+ }
+ }
+ }
+ }
+
+ // Return void.
+ return CVoid.TYPE;
+ }
+
@Override
public Class extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class};
}
+ @Override
+ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast,
+ Environment env, Set exceptions) {
+
+ // Handle children. These will execute before this break().
+ Scope scope = super.linkScope(analysis, parentScope, ast, env, exceptions);
+
+ // Add break loop reference in new scope.
+ scope = analysis.createNewScope(scope);
+ scope.addReference(new ContinuableReference(ast.getTarget()));
+ analysis.setTermScope(ast, scope);
+
+ // Return scope.
+ return scope;
+ }
+
@Override
public boolean isRestricted() {
return false;
diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
index 993971e8e4..473dd74aa5 100644
--- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java
+++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java
@@ -32,6 +32,8 @@
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.compiler.SelfStatement;
import com.laytonsmith.core.compiler.VariableScope;
+import com.laytonsmith.core.compiler.analysis.BreakableBoundDeclaration;
+import com.laytonsmith.core.compiler.analysis.ContinuableBoundDeclaration;
import com.laytonsmith.core.compiler.analysis.Declaration;
import com.laytonsmith.core.compiler.analysis.IVariableAssignDeclaration;
import com.laytonsmith.core.compiler.analysis.IncludeReference;
@@ -2745,9 +2747,14 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope, ParseTree ast
// Create returnable declaration in the inner root scope.
paramScope.addDeclaration(new ReturnableDeclaration(retType, ast.getNodeModifiers(), ast.getTarget()));
+ // Create breakable and continuable bound declarations to prevent resolving to parent scope.
+ Scope codeParentScope = analysis.createNewScope(paramScope);
+ codeParentScope.addDeclaration(new BreakableBoundDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(new ContinuableBoundDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+
// Handle closure code.
ParseTree code = ast.getChildAt(ast.numberOfChildren() - 1);
- analysis.linkScope(paramScope, code, env, exceptions);
+ analysis.linkScope(codeParentScope, code, env, exceptions);
// Return the parent scope, as parameters and their default values are not accessible after the closure.
return parentScope;
diff --git a/src/main/java/com/laytonsmith/core/functions/EventBinding.java b/src/main/java/com/laytonsmith/core/functions/EventBinding.java
index f562c596a8..d3f4f1707b 100644
--- a/src/main/java/com/laytonsmith/core/functions/EventBinding.java
+++ b/src/main/java/com/laytonsmith/core/functions/EventBinding.java
@@ -18,6 +18,8 @@
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.compiler.SelfStatement;
import com.laytonsmith.core.compiler.VariableScope;
+import com.laytonsmith.core.compiler.analysis.BreakableBoundDeclaration;
+import com.laytonsmith.core.compiler.analysis.ContinuableBoundDeclaration;
import com.laytonsmith.core.compiler.analysis.Namespace;
import com.laytonsmith.core.compiler.analysis.ReturnableDeclaration;
import com.laytonsmith.core.compiler.analysis.Scope;
@@ -336,10 +338,15 @@ public Scope linkScope(StaticAnalysis analysis, Scope parentScope,
paramScope = scopes[0];
valScope = scopes[1];
}
- analysis.linkScope(paramScope, code, env, exceptions);
+ Scope codeParentScope = analysis.createNewScope(paramScope);
+ analysis.linkScope(codeParentScope, code, env, exceptions);
// Create returnable declaration in the inner root scope.
- paramScope.addDeclaration(new ReturnableDeclaration(CVoid.TYPE, ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(new ReturnableDeclaration(CVoid.TYPE, ast.getNodeModifiers(), ast.getTarget()));
+
+ // Create breakable and continuable bound declarations to prevent resolving to parent scope.
+ codeParentScope.addDeclaration(new BreakableBoundDeclaration(ast.getNodeModifiers(), ast.getTarget()));
+ codeParentScope.addDeclaration(new ContinuableBoundDeclaration(ast.getNodeModifiers(), ast.getTarget()));
// Allow code after bind() to access declarations in assigned values, but not parameters themselves.
return valScope;