Skip to content

Commit 75ebff2

Browse files
author
jacklau
committed
Pin the variable-expansion iteration cap with an oscillating cycle
ExpandTest covered the self-referential cycle (terminates via the next==result fixpoint break), but not an oscillating two-variable cycle (%a% to %b% to %a%), which never reaches a fixpoint and instead relies on the MAX_ITERATIONS cap to terminate. Host-supplied values (chat text read as a variable) can be adversarially self-referential, so guard that the cap path does not hang. Test-only, jars unchanged. Doc test count to 362.
1 parent e34036f commit 75ebff2

5 files changed

Lines changed: 16 additions & 5 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Use `./gradlew` (the wrapper) — do not rely on a system Gradle.
2828
The engine is a normal single Kotlin module. Develop and test it without touching Fabric:
2929

3030
```bash
31-
./gradlew :engine:test # 361 unit tests
31+
./gradlew :engine:test # 362 unit tests
3232
./gradlew :engine:build
3333
```
3434

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ first-class support for **23 Minecraft versions**
2323
own algorithm** through the [Pathfinder SPI](docs/guide/pathfinding.md#write-your-own-pathfinder).
2424
- **Auto-reconnect** — opt-in: rejoin the last server automatically after a disconnect (toggle
2525
via the in-game module GUI or a keybind).
26-
- **Tested** — the engine ships a real unit-test suite (361 tests): lexer, parser, runtime
26+
- **Tested** — the engine ships a real unit-test suite (362 tests): lexer, parser, runtime
2727
VM, variables, expressions, actions, pathfinding.
2828

2929
---
@@ -170,7 +170,7 @@ the full workflow.
170170
```
171171
mc_macromod/
172172
├─ engine/ Pure-JVM scripting engine (DSL lexer → compiler → VM, pathfinding,
173-
│ modules). No Minecraft deps. 361 unit tests.
173+
│ modules). No Minecraft deps. 362 unit tests.
174174
├─ fabric/ Multi-version Fabric mod (Stonecutter). Keybinds, MC actions, GUI,
175175
│ events, navigation. Shades :engine via Jar-in-Jar.
176176
│ └─ versions/ Per-Minecraft-version gradle.properties (deps + Java level).

‎docs/VERSIONS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ auto-provisioned by Foojay** on first build.
6868
> the Java-16 (1.17.1) and Java-8 (1.16.5) variants that shade the engine (JIJ). The engine uses
6969
> only Kotlin stdlib (`kotlin.collections.ArrayDeque.addFirst` etc.) with no APIs above Java 8,
7070
> so targeting 8 is behavior-neutral and the Java-8 (major-version-52) bytecode runs on every
71-
> newer-JVM variant too. `./gradlew :engine:test` stays green at 361 tests.
71+
> newer-JVM variant too. `./gradlew :engine:test` stays green at 362 tests.
7272
7373
## Supported versions
7474

‎docs/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ first-class **multi-version** support.
3636

3737
---
3838

39-
The engine ships a real unit-test suite (361 tests): lexer, parser, runtime VM,
39+
The engine ships a real unit-test suite (362 tests): lexer, parser, runtime VM,
4040
variables, expressions, actions, pathfinding.
4141

4242
</div>

‎engine/src/test/kotlin/dev/macromod/engine/param/ExpandTest.kt‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import dev.macromod.engine.value.Value
44
import dev.macromod.engine.variable.VariableRegistry
55
import kotlin.test.Test
66
import kotlin.test.assertEquals
7+
import kotlin.test.assertTrue
78

89
class ExpandTest {
910
private fun expander(vararg pairs: Pair<String, Value>): VariableExpander {
@@ -47,6 +48,16 @@ class ExpandTest {
4748
assertEquals("%&a%", e.expand("%&a%"))
4849
}
4950

51+
@Test fun `an oscillating two-variable cycle terminates at the iteration cap`() {
52+
// %a% -> "%b%" -> "%a%" -> ... never reaches a fixpoint (the value keeps flipping), so unlike the
53+
// self-ref case above it never hits the next==result break — it must terminate via the
54+
// MAX_ITERATIONS cap instead. Host-supplied values (e.g. chat text read as a variable) can be
55+
// adversarially self-referential, so this path must not hang. The test completing proves it.
56+
val e = expander("&a" to Value.Str("%&b%"), "&b" to Value.Str("%&a%"))
57+
val out = e.expand("%&a%")
58+
assertTrue(out == "%&a%" || out == "%&b%", "expected a residual ref after the cap, got: $out")
59+
}
60+
5061
@Test fun `a literal percent sign is left untouched`() {
5162
assertEquals("100% done", expander().expand("100% done"))
5263
// the bare "50%" stays literal (no name follows the %); the real ref still expands

0 commit comments

Comments
 (0)