From 33148d02c430a8b9a370fd059074a6406ec82f32 Mon Sep 17 00:00:00 2001 From: Chloe Magnier Date: Tue, 16 Jun 2026 18:17:36 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(compiler):=20@pure=20v=C3=A9rifie=20au?= =?UTF-8?q?ssi=20l'absence=20d'effets=20de=20bord?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'annotation @pure ne signalait jusqu'ici que le rejet du résultat (comme @nodiscard). Elle vérifie désormais que la fonction est réellement pure. Une fonction @pure est signalée (WARNING ANNOTATION_NOT_PURE) quand son corps : - écrit / incrémente une variable externe à la fonction (globale, champ, champ statique, this, ou variable capturée d'une portée englobante) ; - appelle une fonction système à effet de bord (debug*, mutateurs Array/Map/Set : push, pop, sort, mapPut, setRemove…) ; - appelle une fonction utilisateur qui n'est pas elle-même pure. La pureté des appels est vérifiée de façon transitive après l'analyse complète (le graphe d'appels n'est connu qu'à ce moment, à cause des références avant déclaration) : appeler une fonction non annotée mais réellement pure est donc autorisé, et la récursion / récursion mutuelle est gérée via les cycles. La détection se greffe sur la passe analyze() existante (pas de visiteur AST dédié) ; les effets directs sont signalés en ligne, les appels inter-fonctions sont résolus dans resolvePurity(). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/leekscript/common/Error.java | 1 + .../leekscript/compiler/PurityChecker.java | 116 ++++++++++++++++++ .../leekscript/compiler/WordCompiler.java | 77 ++++++++++++ .../compiler/expression/LeekExpression.java | 15 +++ .../compiler/expression/LeekFunctionCall.java | 19 +++ src/test/java/test/TestAnnotations.java | 61 +++++++++ 6 files changed, 289 insertions(+) create mode 100644 src/main/java/leekscript/compiler/PurityChecker.java diff --git a/src/main/java/leekscript/common/Error.java b/src/main/java/leekscript/common/Error.java index adebf8ca..ae1ba98b 100644 --- a/src/main/java/leekscript/common/Error.java +++ b/src/main/java/leekscript/common/Error.java @@ -161,4 +161,5 @@ public enum Error { ANNOTATION_TODO, // 157 ANNOTATION_OVERRIDE_NO_PARENT, // 158 OVERRIDDEN_METHOD_NARROWER_VISIBILITY, // 159 + ANNOTATION_NOT_PURE, // 160 } \ No newline at end of file diff --git a/src/main/java/leekscript/compiler/PurityChecker.java b/src/main/java/leekscript/compiler/PurityChecker.java new file mode 100644 index 00000000..35658741 --- /dev/null +++ b/src/main/java/leekscript/compiler/PurityChecker.java @@ -0,0 +1,116 @@ +package leekscript.compiler; + +import java.util.Set; + +import leekscript.common.Error; +import leekscript.compiler.AnalyzeError.AnalyzeErrorLevel; +import leekscript.compiler.bloc.AbstractLeekBlock; +import leekscript.compiler.exceptions.LeekCompilerException; +import leekscript.compiler.expression.Expression; +import leekscript.compiler.expression.LeekArrayAccess; +import leekscript.compiler.expression.LeekExpression; +import leekscript.compiler.expression.LeekFunctionCall; +import leekscript.compiler.expression.LeekObjectAccess; +import leekscript.compiler.expression.LeekParenthesis; +import leekscript.compiler.expression.LeekVariable; +import leekscript.compiler.expression.Operators; + +/** + * Static purity analysis for the {@code @pure} annotation. A function annotated + * {@code @pure} is expected to be free of observable side effects; this checker + * detects the side effects that can be determined statically and emits an + * {@link Error#ANNOTATION_NOT_PURE} warning otherwise. + * + * The checks are performed during the normal {@code analyze()} traversal: each + * relevant node asks {@link WordCompiler#getCurrentPureFunction()} whether it is + * inside a {@code @pure} function and, if so, reports any side effect it + * introduces. A function is considered impure when its body, directly: + * + * Side effects hidden behind dynamic/method calls or aliasing are not detected, + * so the check is intentionally conservative (it may miss impurities but does not + * flag pure code). + */ +public class PurityChecker { + + // Built-in functions that mutate one of their arguments in place or perform + // I/O. LeekFunctions carries no purity metadata, so the side-effecting + // builtins are listed explicitly here (return type is not a reliable signal: + // e.g. pop/setPut mutate but return a value). + private static final Set IMPURE_SYSTEM_FUNCTIONS = Set.of( + // I/O + "debug", "debugW", "debugE", "debugC", + // Array mutators + "push", "pushAll", "pop", "shift", "unshift", "insert", + "remove", "removeElement", "removeKey", "fill", "sort", "arraySort", + "shuffle", "reverse", "arrayClear", "arrayRemoveAll", + "assocSort", "assocReverse", "keySort", + // Map mutators + "mapClear", "mapPut", "mapPutAll", "mapRemove", "mapRemoveAll", + "mapReplace", "mapReplaceAll", "mapFill", "mapMerge", + // Set mutators + "setPut", "setRemove", "setClear" + ); + + public static boolean isImpureSystemFunction(String name) { + return IMPURE_SYSTEM_FUNCTIONS.contains(name); + } + + /** + * If an assignment or increment whose target is {@code target} mutates state + * visible outside {@code pureFunction}, returns the name of the mutated + * symbol; otherwise returns null. Mutating the function's own locals or + * parameters is pure and returns null. + */ + public static String externalMutation(Expression target, AbstractLeekBlock pureFunction) { + var root = rootVariable(target); + if (root == null) return null; + switch (root.getVariableType()) { + case GLOBAL: + case FIELD: + case STATIC_FIELD: + case THIS: + return root.getName(); + case LOCAL: + case ARGUMENT: + case ITERATOR: + // Pure if the variable belongs to this function; impure if it is + // captured from an enclosing scope (incl. top-level variables). + var declaration = root.getDeclaration(); + if (declaration != null && declaration.getFunction() != pureFunction) { + return root.getName(); + } + return null; + default: + return null; + } + } + + /** + * Walks an l-value down to the variable whose state it ultimately mutates: + * {@code arr[i]} → {@code arr}, {@code obj.field} → {@code obj}, etc. Returns + * null when no underlying variable can be determined. + */ + private static LeekVariable rootVariable(Expression e) { + while (true) { + if (e instanceof LeekVariable v) return v; + if (e instanceof LeekParenthesis p) { e = p.getExpression(); continue; } + if (e instanceof LeekArrayAccess a) { e = a.getTabular(); continue; } + if (e instanceof LeekObjectAccess o) { e = o.getObject(); continue; } + if (e instanceof LeekFunctionCall fc) { e = fc.getCallExpression(); continue; } + if (e instanceof LeekExpression x && x.getOperator() == Operators.NON_NULL_ASSERTION) { e = x.getExpression2(); continue; } + return null; + } + } + + public static void reportNotPure(WordCompiler compiler, Location location, String detail) throws LeekCompilerException { + compiler.addError(new AnalyzeError(location, AnalyzeErrorLevel.WARNING, Error.ANNOTATION_NOT_PURE, new String[] { detail })); + } +} diff --git a/src/main/java/leekscript/compiler/WordCompiler.java b/src/main/java/leekscript/compiler/WordCompiler.java index 4a74db39..75aac9d4 100644 --- a/src/main/java/leekscript/compiler/WordCompiler.java +++ b/src/main/java/leekscript/compiler/WordCompiler.java @@ -2,8 +2,11 @@ import java.math.BigInteger; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import leekscript.common.Annotation; import leekscript.common.AccessLevel; @@ -65,6 +68,14 @@ public class WordCompiler { private AbstractLeekBlock mCurrentFunction; private ClassDeclarationInstruction mCurrentClass; private LexicalParserTokenStream mTokens; + + // Purity tracking for @pure verification. Side effects and call edges are + // recorded per function during analyze(), then resolved transitively after + // the whole program is analyzed (a callee may be analyzed after its caller). + private final Set mImpureFunctions = new HashSet<>(); + private final Map> mFunctionCallees = new HashMap<>(); + private final ArrayList mDeferredPureCalls = new ArrayList<>(); + private record PureCall(AbstractLeekBlock callee, Location location) {} private int mLine; private AIFile mAI = null; private final int version; @@ -356,6 +367,7 @@ public void analyze() throws LeekCompilerException { setCurrentFunction(mMain); mMain.preAnalyze(this); mMain.analyze(this); + resolvePurity(); } private void compileWord() throws LeekCompilerException { @@ -2471,6 +2483,71 @@ public AbstractLeekBlock getCurrentFunction() { return mCurrentFunction; } + /** + * Returns the function/method currently being analyzed if it is annotated + * {@code @pure}, otherwise null. Used to verify that the body of a {@code @pure} + * function does not perform side effects. Nested anonymous functions reset the + * current function, so side effects inside a lambda defined within a {@code @pure} + * function are not attributed to it. + */ + public AbstractLeekBlock getCurrentPureFunction() { + if (mCurrentFunction instanceof FunctionBlock fb && fb.hasAnnotation(Annotation.PURE)) return fb; + if (mCurrentFunction instanceof ClassMethodBlock cm && cm.hasAnnotation(Annotation.PURE)) return cm; + return null; + } + + /** Marks the function currently being analyzed as having a direct side effect. */ + public void recordSideEffect() { + mImpureFunctions.add(mCurrentFunction); + } + + /** Records that the function currently being analyzed calls {@code callee}. */ + public void recordCallee(AbstractLeekBlock callee) { + mFunctionCallees.computeIfAbsent(mCurrentFunction, k -> new HashSet<>()).add(callee); + } + + /** + * Defers a purity check for a call made from a {@code @pure} function: after the + * whole program is analyzed, {@code callee} is checked for transitive purity and + * a warning is emitted at {@code location} if it turns out to be impure. + */ + public void recordPureCall(AbstractLeekBlock callee, Location location) { + mDeferredPureCalls.add(new PureCall(callee, location)); + } + + /** + * Resolves deferred {@code @pure} call checks once the full call graph is known. + * A call from a pure function is only flagged when the callee is transitively + * impure, so calling an unannotated-but-actually-pure function is allowed. + */ + private void resolvePurity() throws LeekCompilerException { + if (mDeferredPureCalls.isEmpty()) return; + var memo = new HashMap(); + for (var call : mDeferredPureCalls) { + if (isTransitivelyImpure(call.callee(), memo, new HashSet<>())) { + var name = call.callee() instanceof FunctionBlock fb ? fb.getName() : "?"; + PurityChecker.reportNotPure(this, call.location(), name); + } + } + } + + private boolean isTransitivelyImpure(AbstractLeekBlock f, Map memo, Set visiting) { + var cached = memo.get(f); + if (cached != null) return cached; + if (mImpureFunctions.contains(f)) return true; // direct side effect + if (!visiting.add(f)) return false; // recursion cycle: assume pure + boolean impure = false; + var callees = mFunctionCallees.get(f); + if (callees != null) { + for (var callee : callees) { + if (isTransitivelyImpure(callee, memo, visiting)) { impure = true; break; } + } + } + visiting.remove(f); + memo.put(f, impure); + return impure; + } + public void addError(AnalyzeError analyzeError) throws LeekCompilerException { this.mAI.getErrors().add(analyzeError); if (this.mAI.getErrors().size() > 10000) { diff --git a/src/main/java/leekscript/compiler/expression/LeekExpression.java b/src/main/java/leekscript/compiler/expression/LeekExpression.java index 2f69383c..6127877a 100644 --- a/src/main/java/leekscript/compiler/expression/LeekExpression.java +++ b/src/main/java/leekscript/compiler/expression/LeekExpression.java @@ -2,6 +2,7 @@ import leekscript.compiler.AnalyzeError; import leekscript.compiler.NarrowingInfo; +import leekscript.compiler.PurityChecker; import leekscript.compiler.Token; import leekscript.compiler.JavaWriter; import leekscript.compiler.Location; @@ -1167,6 +1168,13 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { if (mExpression1 instanceof LeekArrayAccess) ((LeekArrayAccess) mExpression1).setLeftValue(true); + + // @pure : assigning to state outside the function is a side effect + var mutated = PurityChecker.externalMutation(mExpression1, compiler.getCurrentFunction()); + if (mutated != null) { + compiler.recordSideEffect(); + if (compiler.getCurrentPureFunction() != null) PurityChecker.reportNotPure(compiler, getLocation(), mutated); + } } if (Operators.isIncrement(mOperator)) { @@ -1191,6 +1199,13 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { mExpression2.getType().toString() })); } + + // @pure : incrementing state outside the function is a side effect + var mutated = PurityChecker.externalMutation(mExpression2, compiler.getCurrentFunction()); + if (mutated != null) { + compiler.recordSideEffect(); + if (compiler.getCurrentPureFunction() != null) PurityChecker.reportNotPure(compiler, getLocation(), mutated); + } } if (mOperator == Operators.ASSIGN) { diff --git a/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java b/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java index 2d509afe..f2161bed 100644 --- a/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java +++ b/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java @@ -10,6 +10,7 @@ import leekscript.compiler.Token; import leekscript.compiler.JavaWriter; import leekscript.compiler.Location; +import leekscript.compiler.PurityChecker; import leekscript.compiler.WordCompiler; import leekscript.compiler.AnalyzeError.AnalyzeErrorLevel; import leekscript.compiler.bloc.ClassMethodBlock; @@ -720,6 +721,24 @@ else if (!exprType.canBeCallable()) { })); } } + + // @pure : record side effects and call edges for purity analysis. Calling a + // side-effecting builtin is a direct side effect; calling another user + // function is resolved transitively after the whole program is analyzed, so + // an unannotated-but-actually-pure callee does not make the caller impure. + if (system_function != null) { + if (PurityChecker.isImpureSystemFunction(system_function.getName())) { + compiler.recordSideEffect(); + if (compiler.getCurrentPureFunction() != null) { + PurityChecker.reportNotPure(compiler, getLocation(), system_function.getName()); + } + } + } else if (resolvedFunction != null) { + compiler.recordCallee(resolvedFunction); + if (compiler.getCurrentPureFunction() != null) { + compiler.recordPureCall(resolvedFunction, getLocation()); + } + } } private void verifySystemFunction(WordCompiler compiler, LeekVariable v, LeekFunctions f) throws LeekCompilerException { diff --git a/src/test/java/test/TestAnnotations.java b/src/test/java/test/TestAnnotations.java index 9b85f018..0584cec2 100644 --- a/src/test/java/test/TestAnnotations.java +++ b/src/test/java/test/TestAnnotations.java @@ -66,6 +66,67 @@ public void testPure() throws Exception { // @pure is visible by @unused so both can be combined code_strict_v4_("@pure @unused function helper() { return 0; } return 0").noWarning(); + + // --- Purity checking --- + + // Pure function using only its own locals/parameters → no warning + code_v4_("@pure function f() { var local = 0; local = 1; return local } return f()").noWarning(); + code_v4_("@pure function f() { var local = 0; local = 1; return local } return f()").equals("1"); + + // Mutating a local array/map of its own is pure + code_v4_("@pure function f() { var a = [1, 2]; a[0] = 5; return a } return f()").noWarning(); + + // Writing to a global is a side effect → not pure + code_v4_("global g = 0; @pure function f() { g = 1; return g } return f()").warning(Error.ANNOTATION_NOT_PURE); + + // Incrementing a global is a side effect → not pure + code_v4_("global g = 0; @pure function f() { g++; return g } return f()").warning(Error.ANNOTATION_NOT_PURE); + + // Calling a side-effecting builtin (I/O) → not pure + code_v4_("@pure function f(a) { debug(a); return a } return f(1)").warning(Error.ANNOTATION_NOT_PURE); + + // Calling an array-mutating builtin → not pure + code_v4_("@pure function f(arr) { push(arr, 1); return arr } return f([])").warning(Error.ANNOTATION_NOT_PURE); + + // Calling a pure builtin (no mutation) → still pure + code_v4_("@pure function f(a) { return abs(a) } return f(-3)").noWarning(); + code_v4_("@pure function f(a) { return abs(a) } return f(-3)").equals("3"); + + // Calling another @pure function is fine + code_v4_("@pure function helper(x) { return x * 2 } @pure function f(x) { return helper(x) } return f(2)").noWarning(); + code_v4_("@pure function helper(x) { return x * 2 } @pure function f(x) { return helper(x) } return f(2)").equals("4"); + + // Calling an unannotated but actually-pure function is fine: purity is verified + // transitively, not by requiring the @pure annotation everywhere + code_v4_("function helper(x) { return x * 2 } @pure function f(x) { return helper(x) } return f(2)").noWarning(); + code_v4_("function helper(x) { return x * 2 } @pure function f(x) { return helper(x) } return f(2)").equals("4"); + + // ...even through a chain of unannotated pure functions + code_v4_("function a(x) { return b(x) } function b(x) { return x + 1 } @pure function f(x) { return a(x) } return f(1)").noWarning(); + code_v4_("function a(x) { return b(x) } function b(x) { return x + 1 } @pure function f(x) { return a(x) } return f(1)").equals("2"); + + // Calling an unannotated function that IS impure → warning + code_v4_("global g = 0; function bump() { g = 1; return g } @pure function f() { return bump() } return f()").warning(Error.ANNOTATION_NOT_PURE); + + // ...including impurity reached transitively through another function + code_v4_("function a() { return b() } function b() { debug(1); return 0 } @pure function f() { return a() } return f()").warning(Error.ANNOTATION_NOT_PURE); + + // Recursion on a @pure function is allowed (it calls itself, which is @pure) + code_v4_("@pure function fact(n) { return n <= 1 ? 1 : n * fact(n - 1) } return fact(5)").noWarning(); + code_v4_("@pure function fact(n) { return n <= 1 ? 1 : n * fact(n - 1) } return fact(5)").equals("120"); + + // Side effects inside a lambda defined within a @pure function are NOT attributed + // to it (the lambda is a separate scope): defining it is pure. + code_v4_("global g = 0; @pure function f() { var fn = function() { g = 1 }; return 0 } return f()").noWarning(); + + // @pure on a method: mutating the object (this) is a side effect → not pure + code_v4_(""" + class A { + public integer x = 0 + @pure setX() { this.x = 1; return this.x } + } + return new A().setX() + """).warning(Error.ANNOTATION_NOT_PURE); } @Test From 3ab253d9cebd0842df7477ca3c8b87cf7ce622cd Mon Sep 17 00:00:00 2001 From: Chloe Magnier Date: Tue, 16 Jun 2026 18:41:13 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(compiler):=20expose=20la=20puret=C3=A9?= =?UTF-8?q?=20des=20fonctions=20syst=C3=A8me=20au=20check=20@pure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le check @pure ne connaissait que les fonctions à effet de bord de la bibliothèque standard (liste IMPURE_SYSTEM_FUNCTIONS). Les fonctions fournies par un hôte (ex. le générateur Leek Wars) étaient toutes considérées pures, donc appelables depuis une fonction @pure sans avertissement, même lorsqu'elles agissent sur l'état. LeekFunctions porte désormais un drapeau de pureté (pur par défaut, setImpure() pour s'en exclure) : un hôte peut déclarer la pureté de chacune de ses fonctions directement sur l'objet. isImpureSystemFunction() consulte ce drapeau en plus de la liste standard, et LeekFunctionCall lui passe la fonction plutôt que son seul nom. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../leekscript/compiler/PurityChecker.java | 24 ++++++++++++++----- .../compiler/expression/LeekFunctionCall.java | 2 +- .../java/leekscript/runner/LeekFunctions.java | 23 ++++++++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/main/java/leekscript/compiler/PurityChecker.java b/src/main/java/leekscript/compiler/PurityChecker.java index 35658741..801d03a7 100644 --- a/src/main/java/leekscript/compiler/PurityChecker.java +++ b/src/main/java/leekscript/compiler/PurityChecker.java @@ -14,6 +14,7 @@ import leekscript.compiler.expression.LeekParenthesis; import leekscript.compiler.expression.LeekVariable; import leekscript.compiler.expression.Operators; +import leekscript.runner.LeekFunctions; /** * Static purity analysis for the {@code @pure} annotation. A function annotated @@ -40,10 +41,12 @@ */ public class PurityChecker { - // Built-in functions that mutate one of their arguments in place or perform - // I/O. LeekFunctions carries no purity metadata, so the side-effecting - // builtins are listed explicitly here (return type is not a reliable signal: - // e.g. pop/setPut mutate but return a value). + // Standard-library builtins that mutate one of their arguments in place or + // perform I/O. These are listed explicitly here because the standard library + // registers them without purity metadata (return type is not a reliable signal: + // e.g. pop/setPut mutate but return a value). Extra functions provided by a host + // (e.g. the Leek Wars generator) instead declare their purity directly on the + // LeekFunctions object via setImpure(); see isImpureSystemFunction below. private static final Set IMPURE_SYSTEM_FUNCTIONS = Set.of( // I/O "debug", "debugW", "debugE", "debugC", @@ -59,8 +62,17 @@ public class PurityChecker { "setPut", "setRemove", "setClear" ); - public static boolean isImpureSystemFunction(String name) { - return IMPURE_SYSTEM_FUNCTIONS.contains(name); + /** + * Whether calling the builtin {@code function} introduces a side effect, making + * a {@code @pure} caller impure. A builtin is impure when it has explicitly + * declared itself so ({@link LeekFunctions#isPure()} is false — how host-provided + * functions such as the generator's game actions opt in) or when it is one of the + * standard-library mutators/I/O functions listed in {@link #IMPURE_SYSTEM_FUNCTIONS}. + */ + public static boolean isImpureSystemFunction(LeekFunctions function) { + if (function == null) return false; + if (!function.isPure()) return true; + return IMPURE_SYSTEM_FUNCTIONS.contains(function.getName()); } /** diff --git a/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java b/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java index f2161bed..02f5ec29 100644 --- a/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java +++ b/src/main/java/leekscript/compiler/expression/LeekFunctionCall.java @@ -727,7 +727,7 @@ else if (!exprType.canBeCallable()) { // function is resolved transitively after the whole program is analyzed, so // an unannotated-but-actually-pure callee does not make the caller impure. if (system_function != null) { - if (PurityChecker.isImpureSystemFunction(system_function.getName())) { + if (PurityChecker.isImpureSystemFunction(system_function)) { compiler.recordSideEffect(); if (compiler.getCurrentPureFunction() != null) { PurityChecker.reportNotPure(compiler, getLocation(), system_function.getName()); diff --git a/src/main/java/leekscript/runner/LeekFunctions.java b/src/main/java/leekscript/runner/LeekFunctions.java index 630e6dd7..76c0a45e 100644 --- a/src/main/java/leekscript/runner/LeekFunctions.java +++ b/src/main/java/leekscript/runner/LeekFunctions.java @@ -421,6 +421,12 @@ private static LeekFunctions method(String name, String clazz, int operations, b private int minVersion = 1; private int maxVersion = LeekScript.LATEST_VERSION; private String replacement = null; + // Whether calling this builtin produces no observable side effect. Consulted by + // the @pure annotation check. Defaults to true (most builtins only compute or + // read state); side-effecting builtins (I/O, in-place mutators, and — for the + // generator — game actions such as moveToward/useChip/say) opt out via + // setImpure(). + private boolean pure = true; public LeekFunctions(String standardClass, String name, int operations, boolean isStatic, Type return_type, Type[] arguments) { this(standardClass, name, operations, isStatic, new CallableVersion[] { new CallableVersion(return_type, arguments) }); @@ -543,6 +549,23 @@ public void addOperations(AI leekIA, LeekFunctions function, Object parameters[] public boolean isStatic() { return isStatic; } + + /** Whether this builtin is free of observable side effects (see {@link #pure}). */ + public boolean isPure() { + return pure; + } + + /** + * Declares this builtin as having an observable side effect (I/O, in-place + * mutation of an argument, or — for generator/extra functions — an action on the + * fight state). Calling it from a {@code @pure} function is then reported. + * Fluent so it can be chained on a {@code method(...)} registration. + */ + public LeekFunctions setImpure() { + this.pure = false; + return this; + } + public String getName() { return name; }