diff --git a/README.md b/README.md index 99c1a82..c0b43a6 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ RandoLogicScript -t soh -o out/soh/ src/ Multiple transpilers can be specified, each with their own output directory: ``` -RandoLogicScript -t soh -o out/soh/ -t ap -o out/ap/ src/ extra.rls +RandoLogicScript -t soh -o out/soh/ -t soh_ap -o out/ap/ src/ extra.rls ``` Input paths can be individual `.rls` files or directories (which are recursively scanned for `.rls` files). @@ -35,7 +35,7 @@ Input paths can be individual `.rls` files or directories (which are recursively | Name | Target | | ------------ | ---------------------- | | `soh` | C++ for Shipwright | -| `ap` | Python for Archipelago | +| `soh_ap` | Python for Archipelago | ## Docs diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt index f844ae7..cc53293 100644 --- a/console/CMakeLists.txt +++ b/console/CMakeLists.txt @@ -2,14 +2,14 @@ add_executable(RandoLogicScript main.cpp ) -target_link_libraries(RandoLogicScript PRIVATE ast parser sema soh ap) +target_link_libraries(RandoLogicScript PRIVATE ast parser sema soh soh_ap) if(BUILD_TESTING) rls_add_gtest(console_acceptance_tests tests/acceptance_soh_tests.cpp tests/acceptance_ap_tests.cpp ) - target_link_libraries(console_acceptance_tests PRIVATE ast parser sema soh ap) + target_link_libraries(console_acceptance_tests PRIVATE ast parser sema soh soh_ap) target_compile_definitions(console_acceptance_tests PRIVATE RLS_REPO_ROOT="${CMAKE_SOURCE_DIR}" ) diff --git a/console/main.cpp b/console/main.cpp index 392607f..d83b849 100644 --- a/console/main.cpp +++ b/console/main.cpp @@ -9,7 +9,7 @@ #include "output.h" #include "parser.h" #include "sema.h" -#include "ap.h" +#include "soh_ap.h" #include "soh.h" namespace fs = std::filesystem; @@ -24,7 +24,7 @@ static void printUsage(const char* program) { << "Options:\n" << " -t, --transpiler -o, --output \n" << " Transpiler and output directory pair (may be repeated).\n" - << " Available transpilers: soh, ap\n" + << " Available transpilers: soh, soh_ap\n" << " -h, --help Show this help message.\n"; } @@ -74,7 +74,7 @@ struct TranspilerConfig { }; static bool runTranspiler(const TranspilerConfig& config, const rls::ast::Project& project) { - if (config.name != "soh" && config.name != "ap") { + if (config.name != "soh" && config.name != "soh_ap") { std::cerr << "error: unknown transpiler '" << config.name << "'\n"; return false; } @@ -93,8 +93,26 @@ static bool runTranspiler(const TranspilerConfig& config, const rls::ast::Projec std::cerr << "aborting due to SoH transpiler errors\n"; return false; } + } else if (config.name == "soh_ap") { + rls::transpilers::soh_ap::SohApTranspiler transpiler(project); + transpiler.Transpile(writer); + + // Surface constructs the RuleBuilder target cannot represent (negating a rule, + // a rule-conditioned ternary with no known complement, a runtime value combined + // with a rule). Abort rather than ship Python that raises at world-load. + bool hasErrors = false; + for (const auto& d : transpiler.Diagnostics()) { + printDiagnostic(d); + if (d.level == rls::ast::DiagnosticLevel::Error) + hasErrors = true; + } + if (hasErrors) { + std::cerr << "aborting: '" << config.name << "' could not represent some rules\n"; + return false; + } } else { - rls::transpilers::ap::Transpile(project, writer); + std::cerr << "error: unknown transpiler '" << config.name << "'\n"; + return false; } return true; diff --git a/console/tests/acceptance_ap_tests.cpp b/console/tests/acceptance_ap_tests.cpp index 1da955b..2e204bf 100644 --- a/console/tests/acceptance_ap_tests.cpp +++ b/console/tests/acceptance_ap_tests.cpp @@ -7,14 +7,14 @@ TEST(AcceptanceAp, ExamplesRlsMatchesGolden) { const auto project = parseAndAnalyzeProject(repoPath("examples/rls"), errors); ASSERT_TRUE(errors.empty()) << joinLines(errors); - TempDirectory outputDir("ap"); + TempDirectory outputDir("soh_ap"); { DirectoryWriter writer(outputDir.path()); - rls::transpilers::ap::Transpile(project, writer); + rls::transpilers::soh_ap::SohApTranspiler(project).Transpile(writer); } expectDirectoryMatchesGolden( outputDir.path(), - repoPath("examples/ap"), - R"(.\build\console\RandoLogicScript.exe -t ap -o .\examples\ap .\examples\rls)"); + repoPath("examples/soh_ap"), + R"(.\build\console\RandoLogicScript.exe -t soh_ap -o .\examples\soh_ap .\examples\rls)"); } diff --git a/console/tests/acceptance_helpers.h b/console/tests/acceptance_helpers.h index df1923b..2de7199 100644 --- a/console/tests/acceptance_helpers.h +++ b/console/tests/acceptance_helpers.h @@ -12,7 +12,7 @@ #include #include -#include "ap.h" +#include "soh_ap.h" #include "ast.h" #include "output.h" #include "parser.h" diff --git a/docs/AP-Function-Generation.md b/docs/AP-Function-Generation.md new file mode 100644 index 0000000..df34a30 --- /dev/null +++ b/docs/AP-Function-Generation.md @@ -0,0 +1,345 @@ +# AP Function Generation + +Scope: `transpilers/ap` (generic) + `transpilers/soh_ap` (SoH) + +How RLS `define` functions become Python functions for the Archipelago +RuleBuilder target. The C++ `soh` target generates functions trivially; the AP +target cannot, because a `Bool` is not a uniform type there. This document +specifies why that is hard and how generation handles it. `SohApTranspiler::Transpile` +emits `functions.gen.py`; the section references below point at the code and +tests that realize each piece. + +--- + +## 1. The problem in one sentence + +In the C++ target every RLS `Bool` is a uniform C++ `bool`, so `&&`, `!`, and +the ternary operator just work. In the AP target a `Bool` is **either** a +runtime `Rule` object **or** a build-time Python `bool`, the two cannot be freely +combined, and the RLS surface syntax does not tell them apart. Function +generation is hard because it has to recover that distinction and bridge it. + +--- + +## 2. The target model (RuleBuilder), and its hard constraints + +Access logic in the AP world is built from `Rule` objects +(`rule_builder/rules.py`): `True_()`, `False_()`, `Has(...)`, `And`, `Or`, +`Filtered` (option filters), etc. A rule is bound to a region/location as a +callback `Callable[[bundle], Rule]` — `lambda bundle: ` — where +`bundle` is the `(parent_region, world)` tuple. The framework calls the lambda +with the bundle and `resolve()`s the resulting rule against collection state. + +Three constraints make the model strict. All are load-bearing for this design: + +1. **A `Rule` is not a Python boolean.** `Rule.__bool__` raises: + ```python + # rule_builder/rules.py:187 + raise TypeError("Use & or | to combine rules, or use `is not None` for boolean tests") + ``` + ⇒ a `Rule` can never appear as the condition of a Python `if`, ternary, + `and`, `or`, or `not`. + +2. **`&` / `|` combine `Rule` with `Rule` only.** `Rule.__and__` /`__or__` + accept `Rule | OptionFilter | Iterable[OptionFilter]`. A plain `bool` falls + through to `self.options == other.options` and raises `AttributeError` + (`bool` has no `.options`). ⇒ you cannot splice a Python `bool` into a rule + expression with `&` / `|`. + +3. **There is no runtime negation.** No `__invert__`, no `Not` rule class, so a + *collection-state* rule (`has`, `can_use`, …) cannot be negated. A rule built + only from `setting(...)` is the exception: it resolves at build time against + `world.options`, so the transpiler negates it *structurally* via De Morgan — + see §4.2. In the reference world `not` likewise appears only on `world.options`, + never on a collection rule. + +--- + +## 3. The central distinction: three value classes + +A rule callback `lambda bundle: ` runs **once** to build a `Rule` tree; +only the resulting `Rule` re-evaluates against collection state at solve time +(`Rule.resolve(world)` → `Resolved.__call__(state)`). So the question that +decides how an expression lowers is *when its value is known*, and there are +**three** answers, not two: + +| Class | Lowers to | Known when | Examples | +|------|-----------|------------|----------| +| **R** (rule) | a `Rule` object | re-evaluated each solve step | `has(X)`, `can_use(X)`, `trick(X)`, `flag(X)`, `can_kill(...)`, **`setting(K)` / `setting(K) is V`** (→ OptionFilter rule), any user define that is R, `true`/`false`/`always`/`never` → `True_()`/`False_()` | +| **V** (build-time value) | a Python `int` / `bool` | frozen when the lambda runs | int/enum literals, arithmetic over V, **`Bool`/`Int` parameters** (bound to literals/config at the call that builds the rule), value-defines like `distance_to_int`, comparisons over V operands (`distance_to_int(d) <= N`) | +| **RV** (runtime non-rule value) | *nothing directly* | depends on collection state, but is **not** a `Rule` | `bottle_count()`, `collected_triforce_pieces()`, and any comparison over them (`bottle_count() >= 1`) | + +The RLS *type* does not decide this — `has(X)`, `wall_or_floor` (a `Bool` param), +and `bottle_count() >= 1` are all `Bool`, yet they are R, V, and RV respectively. + +> **Definition.** Classify bottom-up. A host call returning `Bool` is **R**; a +> host call returning anything else (e.g. `Int`) is **RV** — it reads collection +> state but is not a rule. Literals, enum values, and parameters are **V**. A +> user define takes its body's class (params treated as V). Operators fold their +> operands worse-of: **RV** dominates **R** dominates **V** (`and`/`or`, +> comparisons); a ternary/match is R if any branch/arm is R, else V if condition +> and branches are all V, else RV. +> +> **Why RV is its own class — the trap.** `bottle_count() >= 1` is *not* +> build-time: bottles are collected during the solve. Folding it as a ternary +> condition (`has(X) if bottle_count() >= 1 else False_()`) freezes it to the +> **initial, empty** collection state — a silent miscompile. It is also not a +> `Rule`, so it cannot combine with `&`/`|`. So the transpiler refuses to emit an +> RV value: it raises a diagnostic (§6.4) pointing the author at a host rule. The +> correct representation is a dedicated host rule — e.g. the reference world's +> `has_bottle_count(1)`, a custom `Rule` that counts at resolve time. The one RV +> define in the stdlib, `has_bottle`, is host-provided and skipped (§6.5); the +> transpiler does **not** synthesize host rules itself. +> +> **Note on settings.** Unlike the upstream reference world — where settings are +> build-time `world.options` and `not` applies to them directly — this transpiler +> emits `setting(K)` as `True_(options=[OptionFilter(K, …)])`, an atomic Rule. So +> settings are **R**, and `not setting(K)` is `OptionFilter(K, False)`, not a +> Python `not`. + +Classification is the keystone the rest of the design rests on. It is +implemented as `ApTranspiler::ClassifyExpression` → `{Rule, BuildTime, Runtime}` +(`classify_expression.cpp`), pure analysis over the resolved AST, and pinned by +`ApClassify.*`. + +--- + +## 4. Bridging the two worlds + +Boolean operators in RLS are uniform; in AP their lowering depends on the +classes of their operands. + +### 4.1 `and` / `or` + +| Operands | Lowering | Why | +|----------|----------|-----| +| `R and R` | `R & R` | rule conjunction | +| `R or R` | `R \| R` | rule disjunction | +| `V and V` | `V and V` (Python) | both build-time | +| `V or V` | `V or V` (Python) | both build-time | +| `V and R` | `R if V else False_()` | build-time short-circuit | +| `V or R` | `True_() if V else R` | build-time short-circuit | +| `R and V` | `R if V else False_()` | (commute) | +| `R or V` | `True_() if V else R` | (commute) | +| `RV` involved | **unsupported → diagnostic** | the build-time condition would freeze a runtime value | + +The mixed `V`/`R` cases are legal Python because the *condition* of the emitted +ternary is `V` — a value genuinely frozen when the lambda runs (a literal, a +parameter bound at the build call) — and only the branches are rules. This is the +only sound way to fold a build-time fact into a runtime rule. + +The crucial guard is that the condition must be **V, not RV**. `wall_or_floor and +can_use(X)` is fine (`wall_or_floor` is a `Bool` parameter, frozen at build). +`bottle_count() >= 1 and has(X)` is **not** — `bottle_count() >= 1` is RV, and +freezing it as the condition is the miscompile of §3. Such expressions are +`Unrepresentable` in `ClassifyAndOr`: they raise a diagnostic (§6.4) and emit a +best-effort rule-op fallback. Lowering an RV to a host rule is left to the author +(the transpiler does not synthesize one). Pinned by `ApBridging.*`. + +### 4.2 `not` + +| Operand | Lowering | +|---------|----------| +| `not V` (build-time) | `not V` (Python) — a build-time bool, e.g. `not wall_or_floor` (a `Bool` param) | +| `not ` | **De Morgan negation** — push `not` through `and`/`or` (swapping them), flip each `setting(K) is V` leaf to its `"ne"` form, and inline a no-arg pure-setting define's body. Covers `not setting(K)` (→ `OptionFilter(K, False)`), `not (setting(K) is V)` (→ `OptionFilter(K, V, "ne")`), negated membership (`not (A or B or C)` → AND of `"ne"` filters), and `not is_fire_loop_locked()`. Sound because settings resolve at build time. | +| `not R` (collection rule) | **unsupported → diagnostic** (no negation for a collection-state rule) | +| `not RV` | **unsupported → diagnostic** | + +A "pure option-filter rule" is one built entirely from `setting(...)` comparisons, +bool literals, and `and`/`or`/`not` of those (or a no-arg define whose body is one). +The `not` over anything containing a collection rule (`has`, `can_use`, …) is impure +and stays a diagnostic. Implemented by `IsPureOptionFilterRule` + +`GenerateNegatedOptionFilterRule` (`generate_expression.cpp` / `classify_expression.cpp`), +pinned by `ApNegation.*` and `SohApRendering.NotFireLoopLockedNegatesKeysanityMembership`. + +### 4.3 Ternary `cond ? a : b` + +| Condition | Lowering | +|-----------|----------| +| `cond` is **V** | `a if cond else b` (legal; `a`/`b` may be R or V as long as they agree) | +| `cond` is **R**, rule branches | `(cond & a) \| b` — the then-branch is gated by the condition, the else-branch is unconditional. No rule negation is needed and no complement of `cond` is synthesized (the source never wrote one). More permissive than a strict ternary, but **monotonic**, which is what access logic wants: gaining `cond` never removes the else-branch's access. | +| `cond` is **R**, a value branch (or **RV** condition) | **unsupported → diagnostic** (a Rule cannot be a Python `if`, and `cond & ` is ill-typed) | + +The rule-conditioned rule-branch case is selected by `isRuleConditionedRuleTernary` +(`generate_expression.cpp`), pinned by `ApTernary.*` (generic) and +`SohApHostRewrites.AgeConditional*` (SoH bundle/enum rendering). + +--- + +## 5. Worked examples + +### 5.1 Pure-R function + +``` +# RLS +define has_explosives(): + can_use(RG_BOMB_BAG) or can_use(RG_BOMBCHU_5) +``` +```python +# AP (bundle-first; -> bool is the body's RLS type — see the §6.3 limitation) +def has_explosives(bundle) -> bool: + return can_use(bundle, Items.RG_BOMB_BAG) | can_use(bundle, Items.RG_BOMBCHU_5) +``` + +### 5.2 Pure-V function — plain Python + +``` +# RLS +define distance_to_int(distance): + match distance { ED_CLOSE: 0 ED_SHORT_JUMPSLASH: 1 ... } +``` +```python +# AP — value match: returns an int, no rules involved. `rls_match_value`'s first +# arg is the type-appropriate default (0 here); each arm is (condition, body, fallthrough). +def distance_to_int(bundle, distance: EnemyDistance) -> int: + return rls_match_value(0, (lambda distance=distance: distance == EnemyDistance.ED_CLOSE), (lambda: 0), False, ...) +``` + +### 5.3 Mixed V/R — the hard case + +``` +# RLS (from can_get_drop) +can_kill(e, distance) and + (distance_to_int(distance) <= distance_to_int(ED_MASTER_SWORD_JUMPSLASH) or match e { ... }) +``` +- `can_kill(e, distance)` → **R** +- `distance_to_int(distance) <= …` → **V** (both sides build-time ints) +- `match e { … }` (rule bodies) → **R** + +So the structure is `R and (V or R)`, lowering to (schematically): +```python +can_kill(...) & (True_() if distance_to_int(bundle, distance) <= distance_to_int(bundle, EnemyDistance.ED_MASTER_SWORD_JUMPSLASH) else rls_match_rule(...)) +``` + +### 5.4 Unrepresentable — `Int` that depends on a runtime rule + +``` +# RLS +define wallet_capacity(): + has(RG_TYCOON_WALLET) ? 999 : has(RG_GIANT_WALLET) ? 500 : ... : 0 +``` +The ternary condition is **R** and the result is an `Int`. There is no way to +produce a state-dependent integer in this model, so `wallet_capacity` is **not +generated** — it is host-provided and skipped via `isHostProvidedDefine` (§6.5). +Its only consumer, `check_price(x) <= wallet_capacity()`, is special-cased at the +call site to `can_afford_slot(x)` (`soh_expression.cpp` `renderBinarySpecialCase`). +Same story for the triforce comparison → `CanWinTriforceHunt()`. + +--- + +## 6. Cross-cutting behavior + +### 6.1 `match` over rules + +`match` with `or`-fallthrough accumulates bodies. Because `bool(Rule)` raises +(§2 constraint 1), the fallthrough path can never test a rule body with +`if bool(body())`. Two transpile-time-selected helpers in +`transpilers/soh_ap/src/rls_match.py` handle the two cases, chosen by the arm +bodies' value class (`generate_expression.cpp`, pinned by `ApMatch.*`): +- **value match** (build-time bodies) → `rls_match_value(default, …)` returns the + selected value; the codegen passes `0` or `False` as the type-appropriate + default; +- **rule match** (rule bodies) → `rls_match_rule(…)` `|`-combines the matched arm + with the arms it falls through into, defaulting to `False_()`. It never calls + `bool()` on a rule. + +A match whose bodies are **runtime non-rule** values (RV) is diagnosed (§6.4). + +### 6.2 Receiver position: bundle-first + +`bundle` is the **first** parameter everywhere — both in the host-call rewrites +(`has_item(bundle, X)`) and in generated function signatures +(`is_child(bundle)`, `can_kill_enemy(bundle, …)`). The reference `LogicHelpers` +is itself inconsistent about where `bundle` goes (`can_use(item, bundle)` puts it +last, `can_kill_enemy(bundle, …)` puts it first), so there is no single upstream +convention to be drop-in compatible with; bundle-first is the self-consistent +choice and owns a divergent set of host helpers deliberately. +`GenerateFunctionDefinitionsSource` prepends the `ruleContextParam()` receiver. + +### 6.3 Signatures and types + +- `pythonTypeName` (`soh_functions.cpp`) is derived from the same `enumClassName` + table that backs `renderEnumValue` (`soh_expression.cpp`), so a parameter + holding `Items.RG_FOO` annotates as `Items` and the two cannot drift. Enum + types with no dedicated reference class (Scene/Dungeon/Area, which + `renderEnumValue` renders bare) fall to `unsupported_type`. + `Condition`/`Callable` map to `Callable[[tuple[Regions, "SohWorld"]], Rule]` (a + thunk taking the bundle and returning a Rule). Pinned by + `SohApFunctionSignatures.*`. +- `Condition` parameters lower as a thunk on the way in and `cond(bundle)` on + invoke (`ApCallables` / `SohApCallables`). +- Default-valued params land in the right class: the default binds like a call + argument, so it is rendered via `GenerateCallArgument` — a value (Bool) default + emits Python `True`/`False` (not the `True_()`/`False_()` rule literals), an enum + default carries its `renderEnumValue` prefix, and a Condition default is thunked. + Pinned by `SohApFunctionSignatures.{BoolParamDefaultUsesPythonLiteral, + EnumParamDefaultIsPrefixed}`. +- **Limitation:** the return annotation is `pythonTypeName(bodyType)`, so a + rule-valued body whose RLS type is `Bool` annotates as `-> bool` rather than + `-> Rule`. Harmless at runtime (Python does not enforce annotations) but + inaccurate; tightening it would mean annotating from the body's value class. + +### 6.4 Diagnose, don't miscompile + +Some RLS that *type-checks* cannot be expressed (`not R`, a rule-conditioned +*value* ternary/match, a state-dependent `Int`). The transpiler emits a precise +diagnostic at the offending node rather than generate code that throws +`TypeError` at world-load — an error, not silent wrong output. `Diagnose` + +`Diagnostics()` (`ap_transpiler.cpp`) accumulate these; `runTranspiler` +(`console/main.cpp`) prints them and aborts with a non-zero exit. Pinned by +`ApDiagnostics.*`. + +### 6.5 Host-provided defines + +A game can declare that certain `define`s are supplied natively by the world and +must **not** be generated, via the `isHostProvidedDefine` hook (base default: +generate everything). Generating them would either shadow a hand-written host +helper or emit an unrepresentable body. SoH names two: +- `has_bottle` — a hand-written host rule (RV; §3), referenced from the region rules; +- `wallet_capacity` — a state-dependent `Int` (§5.4), collapsed away at its only + call site by `renderBinarySpecialCase`. + +Pinned by `SohApFunctionSignatures.HostProvidedDefinesAreSkipped`. + +--- + +## 7. Code and test map + +| Concept | Code | Tests | +|---------|------|-------| +| Classification (R/V/RV) | `classify_expression.cpp` (`ClassifyExpression`) | `ApClassify.*` | +| `and`/`or` bridging (§4.1) | `generate_expression.cpp` (`ClassifyAndOr`) | `ApBridging.*` | +| `not` / De Morgan negation (§4.2) | `classify_expression.cpp` (`IsPureOptionFilterRule`) + `generate_expression.cpp` (`GenerateNegatedOptionFilterRule`) | `ApNegation.*`, `SohApRendering.NotFireLoopLockedNegatesKeysanityMembership` | +| Ternary (§4.3) | `generate_expression.cpp` (`isRuleConditionedRuleTernary`) | `ApTernary.*`, `SohApHostRewrites.AgeConditional*` | +| `match` (§6.1) | `rls_match.py` (`rls_match_value`/`rls_match_rule`) + `generate_expression.cpp` | `ApMatch.*`, `SohApHostRewrites.RuleMatch*` | +| Diagnostics (§6.4) | `ap_transpiler.cpp` (`Diagnose`/`Diagnostics`), `console/main.cpp` (`runTranspiler`) | `ApDiagnostics.*` | +| Signatures & types (§6.3) | `soh_functions.cpp` (`pythonTypeName`) + `soh_expression.cpp` (`enumClassName`/`renderEnumValue`) | `SohApFunctionSignatures.*` | +| Host-provided defines (§6.5) | `isHostProvidedDefine` hook + `soh_expression.cpp` | `SohApFunctionSignatures.HostProvidedDefinesAreSkipped` | +| Emission | `SohApTranspiler::Transpile` → `GenerateFunctionDefinitionsSource` (emits `functions.gen.py`) | `AcceptanceSoh` (byte-for-byte golden) | + +The generated `functions.gen.py` preamble imports the host primitives +(`from .Rules import *` — the host rules, enum classes, and the `Callable`/`Rule` +names the annotations use) plus the match helpers; the regions file imports the +generated functions (`from .functions.gen import *`). + +--- + +## 8. Why the C++ transpiler is not a reference here + +`transpilers/soh/src/generate_functions.cpp` gives the *structure* (iterate +`DefineDecls`, build a signature, `return `), which is mirrored. But it +offers **no guidance on the V/R split**, because C++ has none: `has()` returns +`bool`, `&&`/`!`/ternary operate uniformly, and `wallet_capacity` is just a +function returning `int`. The entire difficulty in this document is specific to +the RuleBuilder target. Mirroring the C++ approach naively is exactly the trap. + +--- + +## 9. Design decisions + +1. **Receiver position** — **bundle-first** everywhere, owning a divergent set of + host helpers (§6.2). +2. **Scope** — the **whole `stdlib`** transpiles, with host-provided defines + skipped via `isHostProvidedDefine` (§6.5). +3. **Classification home** — kept **local to `ap`** (`classify_expression.cpp`), + not promoted to sema; revisit only if a second target needs the R/V/RV split. diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 156a427..95da5f6 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -138,7 +138,7 @@ Acceptance tests are included in `console_acceptance_tests` and run the end-to-e pipeline over `examples/rls`. - SOH acceptance golden files: `examples/soh/*.gen.{h,cpp}` -- AP acceptance golden file: `examples/ap/ap.py` +- SOH AP acceptance golden files: `examples/soh_ap/*.gen.py` To run only acceptance tests: diff --git a/examples/ap/ap.py b/examples/ap/ap.py deleted file mode 100644 index cb054a1..0000000 --- a/examples/ap/ap.py +++ /dev/null @@ -1 +0,0 @@ -# Generated by RLS ap transpiler diff --git a/examples/soh_ap/enums.gen.py b/examples/soh_ap/enums.gen.py new file mode 100644 index 0000000..511bff3 --- /dev/null +++ b/examples/soh_ap/enums.gen.py @@ -0,0 +1,192 @@ +# Generated by RLS soh_ap transpiler +from enum import StrEnum, IntEnum, auto + +class EventLocations(StrEnum): + @staticmethod + def _generate_next_value_(name, start, count, last_values): + return name.replace("RR_", "").replace("_", " ").title() + RR_KF_OUTSIDE_DEKU_TREE_LOGIC_STICK_ACCESS = auto() + RR_KF_OUTSIDE_DEKU_TREE_LOGIC_NUT_ACCESS = auto() + RR_KF_OUTSIDE_DEKU_TREE_LOGIC_FAIRY_ACCESS = auto() + RR_KF_OUTSIDE_DEKU_TREE_LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD = auto() + RR_KF_STORMS_GROTTO_LOGIC_FAIRY_ACCESS = auto() + RR_KF_STORMS_GROTTO_LOGIC_BUG_ACCESS = auto() + RR_KF_STORMS_GROTTO_LOGIC_FISH_ACCESS = auto() + RR_KOKIRI_FOREST_LOGIC_FAIRY_ACCESS = auto() + RR_KOKIRI_FOREST_LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD = auto() + RR_ROOT_LOGIC_KAKARIKO_GATE_OPEN = auto() + RR_ROOT_LOGIC_TH_COULD_FREE_1_TORCH_CARPENTER = auto() + RR_ROOT_LOGIC_TH_COULD_FREE_DOUBLE_CELL_CARPENTER = auto() + RR_ROOT_LOGIC_TH_COULD_FREE_DEAD_END_CARPENTER = auto() + RR_ROOT_LOGIC_TH_COULD_FREE_SLOPE_CARPENTER = auto() + RR_ROOT_LOGIC_TH_RESCUED_ALL_CARPENTERS = auto() + RR_ROOT_LOGIC_FREED_EPONA = auto() + +class Events(StrEnum): + @staticmethod + def _generate_next_value_(name, start, count, last_values): + return name.replace("LOGIC_", "").replace("_", " ").title() + LOGIC_BUG_ACCESS = auto() + LOGIC_FAIRY_ACCESS = auto() + LOGIC_FISH_ACCESS = auto() + LOGIC_FREED_EPONA = auto() + LOGIC_KAKARIKO_GATE_OPEN = auto() + LOGIC_NUT_ACCESS = auto() + LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD = auto() + LOGIC_STICK_ACCESS = auto() + LOGIC_TH_COULD_FREE_1_TORCH_CARPENTER = auto() + LOGIC_TH_COULD_FREE_DEAD_END_CARPENTER = auto() + LOGIC_TH_COULD_FREE_DOUBLE_CELL_CARPENTER = auto() + LOGIC_TH_COULD_FREE_SLOPE_CARPENTER = auto() + LOGIC_TH_RESCUED_ALL_CARPENTERS = auto() + +class Regions(StrEnum): + RR_ADULT_SPAWN = "Adult Spawn" + RR_BOLERO_OF_FIRE_WARP = "Bolero of Fire Warp" + RR_CHILD_SPAWN = "Child Spawn" + RR_KF_BOULDER_LOOP = "KF Boulder Loop" + RR_KF_HOUSE_OF_TWINS = "KF House of Twins" + RR_KF_KNOW_IT_ALL_HOUSE = "KF Know It All House" + RR_KF_KOKIRI_SHOP = "KF Kokiri Shop" + RR_KF_LINKS_HOUSE = "KF Link's House" + RR_KF_LINKS_PORCH = "KF Link's Porch" + RR_KF_MIDOS_HOUSE = "KF Mido's House" + RR_KF_OUTSIDE_DEKU_TREE = "KF Outside Deku Tree" + RR_KF_OUTSIDE_LOST_WOODS = "KF Outside Lost Woods" + RR_KF_RUPEE_ALCOVE = "KF Alcove" + RR_KF_SARIAS_HOUSE = "KF Saria's House" + RR_KF_STORMS_GROTTO = "KF Storms Grotto" + RR_KOKIRI_FOREST = "Kokiri Forest" + RR_MINUET_OF_FOREST_WARP = "Minuet of Forest Warp" + RR_NOCTURNE_OF_SHADOW_WARP = "Nocturne of Shadow Warp" + RR_PRELUDE_OF_LIGHT_WARP = "Prelude of Light Warp" + RR_REQUIEM_OF_SPIRIT_WARP = "Requiem of Spirit Warp" + RR_ROOT = "Root" + RR_ROOT_EXITS = "Root Exits" + RR_SERENADE_OF_WATER_WARP = "Serenade of Water Warp" + +class Locations(StrEnum): + @staticmethod + def _generate_next_value_(name, start, count, last_values): + return name.replace("RC_", "").replace("_", " ").title() + RC_HC_MALON_EGG = auto() + RC_HC_ZELDAS_LETTER = auto() + RC_KF_ADULT_GRASS_1 = auto() + RC_KF_ADULT_GRASS_10 = auto() + RC_KF_ADULT_GRASS_11 = auto() + RC_KF_ADULT_GRASS_12 = auto() + RC_KF_ADULT_GRASS_13 = auto() + RC_KF_ADULT_GRASS_14 = auto() + RC_KF_ADULT_GRASS_15 = auto() + RC_KF_ADULT_GRASS_16 = auto() + RC_KF_ADULT_GRASS_17 = auto() + RC_KF_ADULT_GRASS_18 = auto() + RC_KF_ADULT_GRASS_19 = auto() + RC_KF_ADULT_GRASS_2 = auto() + RC_KF_ADULT_GRASS_20 = auto() + RC_KF_ADULT_GRASS_3 = auto() + RC_KF_ADULT_GRASS_4 = auto() + RC_KF_ADULT_GRASS_5 = auto() + RC_KF_ADULT_GRASS_6 = auto() + RC_KF_ADULT_GRASS_7 = auto() + RC_KF_ADULT_GRASS_8 = auto() + RC_KF_ADULT_GRASS_9 = auto() + RC_KF_BEAN_RED_RUPEE = auto() + RC_KF_BEAN_RUPEE_1 = auto() + RC_KF_BEAN_RUPEE_2 = auto() + RC_KF_BEAN_RUPEE_3 = auto() + RC_KF_BEAN_RUPEE_4 = auto() + RC_KF_BEAN_RUPEE_5 = auto() + RC_KF_BEAN_RUPEE_6 = auto() + RC_KF_BEAN_SPROUT_FAIRY_1 = auto() + RC_KF_BEAN_SPROUT_FAIRY_2 = auto() + RC_KF_BEAN_SPROUT_FAIRY_3 = auto() + RC_KF_BEHIND_MIDOS_RUPEE = auto() + RC_KF_BOULDER_RUPEE_1 = auto() + RC_KF_BOULDER_RUPEE_2 = auto() + RC_KF_BRIDGE_RUPEE = auto() + RC_KF_BROTHERS_HOUSE_POT_1 = auto() + RC_KF_BROTHERS_HOUSE_POT_2 = auto() + RC_KF_CHILD_GRASS_1 = auto() + RC_KF_CHILD_GRASS_10 = auto() + RC_KF_CHILD_GRASS_11 = auto() + RC_KF_CHILD_GRASS_12 = auto() + RC_KF_CHILD_GRASS_2 = auto() + RC_KF_CHILD_GRASS_3 = auto() + RC_KF_CHILD_GRASS_4 = auto() + RC_KF_CHILD_GRASS_5 = auto() + RC_KF_CHILD_GRASS_6 = auto() + RC_KF_CHILD_GRASS_7 = auto() + RC_KF_CHILD_GRASS_8 = auto() + RC_KF_CHILD_GRASS_9 = auto() + RC_KF_CHILD_GRASS_MAZE_1 = auto() + RC_KF_CHILD_GRASS_MAZE_2 = auto() + RC_KF_CHILD_GRASS_MAZE_3 = auto() + RC_KF_DEKU_TREE_LEFT_GOSSIP_STONE = auto() + RC_KF_DEKU_TREE_LEFT_GOSSIP_STONE_FAIRY = auto() + RC_KF_DEKU_TREE_LEFT_GOSSIP_STONE_FAIRY_BIG = auto() + RC_KF_DEKU_TREE_RIGHT_GOSSIP_STONE = auto() + RC_KF_DEKU_TREE_RIGHT_GOSSIP_STONE_FAIRY = auto() + RC_KF_DEKU_TREE_RIGHT_GOSSIP_STONE_FAIRY_BIG = auto() + RC_KF_GOSSIP_STONE = auto() + RC_KF_GOSSIP_STONE_FAIRY = auto() + RC_KF_GOSSIP_STONE_FAIRY_BIG = auto() + RC_KF_GS_BEAN_PATCH = auto() + RC_KF_GS_HOUSE_OF_TWINS = auto() + RC_KF_GS_KNOW_IT_ALL_HOUSE = auto() + RC_KF_KOKIRI_SWORD_CHEST = auto() + RC_KF_LINKS_HOUSE_COW = auto() + RC_KF_LINKS_HOUSE_POT = auto() + RC_KF_MIDOS_BOTTOM_LEFT_CHEST = auto() + RC_KF_MIDOS_BOTTOM_RIGHT_CHEST = auto() + RC_KF_MIDOS_TOP_LEFT_CHEST = auto() + RC_KF_MIDOS_TOP_RIGHT_CHEST = auto() + RC_KF_NORTH_GRASS_EAST_RUPEE = auto() + RC_KF_NORTH_GRASS_WEST_RUPEE = auto() + RC_KF_SARIAS_BOTTOM_LEFT_HEART = auto() + RC_KF_SARIAS_BOTTOM_RIGHT_HEART = auto() + RC_KF_SARIAS_ROOF_EAST_HEART = auto() + RC_KF_SARIAS_ROOF_NORTH_HEART = auto() + RC_KF_SARIAS_ROOF_WEST_HEART = auto() + RC_KF_SARIAS_TOP_LEFT_HEART = auto() + RC_KF_SARIAS_TOP_RIGHT_HEART = auto() + RC_KF_SHOP_ITEM_1 = auto() + RC_KF_SHOP_ITEM_2 = auto() + RC_KF_SHOP_ITEM_3 = auto() + RC_KF_SHOP_ITEM_4 = auto() + RC_KF_SHOP_ITEM_5 = auto() + RC_KF_SHOP_ITEM_6 = auto() + RC_KF_SHOP_ITEM_7 = auto() + RC_KF_SHOP_ITEM_8 = auto() + RC_KF_SOUTH_GRASS_EAST_RUPEE = auto() + RC_KF_SOUTH_GRASS_WEST_RUPEE = auto() + RC_KF_STORMS_GROTTO_BEEHIVE_LEFT = auto() + RC_KF_STORMS_GROTTO_BEEHIVE_RIGHT = auto() + RC_KF_STORMS_GROTTO_CHEST = auto() + RC_KF_STORMS_GROTTO_FISH = auto() + RC_KF_STORMS_GROTTO_GOSSIP_STONE = auto() + RC_KF_STORMS_GROTTO_GOSSIP_STONE_FAIRY = auto() + RC_KF_STORMS_GROTTO_GOSSIP_STONE_FAIRY_BIG = auto() + RC_KF_STORMS_GROTTO_GRASS_1 = auto() + RC_KF_STORMS_GROTTO_GRASS_2 = auto() + RC_KF_STORMS_GROTTO_GRASS_3 = auto() + RC_KF_STORMS_GROTTO_GRASS_4 = auto() + RC_KF_TWINS_HOUSE_POT_1 = auto() + RC_KF_TWINS_HOUSE_POT_2 = auto() + RC_LINKS_POCKET = auto() + RC_SARIA_SONG_HINT = auto() + RC_SONG_FROM_IMPA = auto() + RC_TOT_MASTER_SWORD = auto() + RC_TRIFORCE_COMPLETED = auto() + +class TimePasses(IntEnum): + Auto = 0 + Yes = 1 + No = 2 + +class WaterLevel(IntEnum): + WL_LOW = 0 + WL_MID = 1 + WL_HIGH = 2 + WL_LOW_OR_MID = 3 + WL_HIGH_OR_MID = 4 diff --git a/examples/soh_ap/functions.gen.py b/examples/soh_ap/functions.gen.py new file mode 100644 index 0000000..5aec9eb --- /dev/null +++ b/examples/soh_ap/functions.gen.py @@ -0,0 +1,76 @@ +# Generated by RLS soh_ap transpiler +from .Rules import * +from .rls_match import rls_match_rule, rls_match_value +from .rls_conditional import rls_conditional + +if TYPE_CHECKING: + from ... import SohWorld + +def _can_get_drop_gold_skulltula(bundle, distance: EnemyDistance) -> bool: + return rls_match_rule((lambda distance=distance: distance == EnemyDistance.ED_CLOSE or distance == EnemyDistance.ED_SHORT_JUMPSLASH or distance == EnemyDistance.ED_MASTER_SWORD_JUMPSLASH or distance == EnemyDistance.ED_LONG_JUMPSLASH or distance == EnemyDistance.ED_BOMB_THROW or distance == EnemyDistance.ED_BOOMERANG), (lambda: can_use(bundle, Items.RG_BOOMERANG)), True, (lambda distance=distance: distance == EnemyDistance.ED_HOOKSHOT), (lambda: can_use(bundle, Items.RG_HOOKSHOT)), True, (lambda distance=distance: distance == EnemyDistance.ED_LONGSHOT), (lambda: can_use(bundle, Items.RG_LONGSHOT)), False) + +def _can_kill_gold_skulltula(bundle, distance: EnemyDistance, wall_or_floor: bool) -> bool: + return rls_match_rule((lambda distance=distance: distance == EnemyDistance.ED_CLOSE), (lambda: can_use(bundle, Items.RG_MEGATON_HAMMER)), True, (lambda distance=distance: distance == EnemyDistance.ED_SHORT_JUMPSLASH), (lambda: can_use(bundle, Items.RG_KOKIRI_SWORD)), True, (lambda distance=distance: distance == EnemyDistance.ED_MASTER_SWORD_JUMPSLASH), (lambda: can_use(bundle, Items.RG_MASTER_SWORD)), True, (lambda distance=distance: distance == EnemyDistance.ED_LONG_JUMPSLASH), (lambda: can_use(bundle, Items.RG_BIGGORON_SWORD) | can_use(bundle, Items.RG_STICKS)), True, (lambda distance=distance: distance == EnemyDistance.ED_BOMB_THROW), (lambda: can_use(bundle, Items.RG_BOMB_BAG)), True, (lambda distance=distance: distance == EnemyDistance.ED_BOOMERANG), (lambda: can_use(bundle, Items.RG_BOOMERANG) | can_use(bundle, Items.RG_DINS_FIRE)), True, (lambda distance=distance: distance == EnemyDistance.ED_HOOKSHOT), (lambda: can_use(bundle, Items.RG_HOOKSHOT)), True, (lambda distance=distance: distance == EnemyDistance.ED_LONGSHOT), (lambda: can_use(bundle, Items.RG_LONGSHOT) | (can_use(bundle, Items.RG_BOMBCHU_5) if wall_or_floor else False_())), True, (lambda distance=distance: distance == EnemyDistance.ED_FAR), (lambda: can_use(bundle, Items.RG_FAIRY_SLINGSHOT) | can_use(bundle, Items.RG_FAIRY_BOW)), False) + +def call_gossip_fairy(bundle) -> bool: + return call_gossip_fairy_except_suns(bundle) | can_use(bundle, Items.RG_SUNS_SONG) + +def call_gossip_fairy_except_suns(bundle) -> bool: + return can_use(bundle, Items.RG_ZELDAS_LULLABY) | can_use(bundle, Items.RG_EPONAS_SONG) | can_use(bundle, Items.RG_SONG_OF_TIME) + +def can_avoid(bundle, e: Enemies, grounded: bool = False, quantity: int = 1) -> bool: + return can_kill(bundle, e, EnemyDistance.ED_CLOSE, True, quantity, False, False) | rls_match_rule((lambda e=e: e == Enemies.RE_GOLD_SKULLTULA), (lambda: True_()), False) + +def can_break_lower_beehives(bundle) -> bool: + return can_break_upper_beehives(bundle) | can_use(bundle, Items.RG_BOMB_BAG) + +def can_break_upper_beehives(bundle) -> bool: + return hookshot_or_boomerang(bundle) | can_do_trick(bundle, Tricks.RT_BOMBCHU_BEEHIVES) & can_use(bundle, Items.RG_BOMBCHU_5) | True_(options=[OptionFilter(RSK_SLINGBOW_BREAK_BEEHIVES, True)]) & (can_use(bundle, Items.RG_FAIRY_BOW) | can_use(bundle, Items.RG_FAIRY_SLINGSHOT)) + +def can_climb_ladder(bundle) -> bool: + return has_item(bundle, Items.RG_CLIMB) | can_do_trick(bundle, Tricks.RT_HOOKSHOT_LADDERS) & can_use(bundle, Items.RG_HOOKSHOT) + +def can_cut_shrubs(bundle) -> bool: + return can_use(bundle, Items.RG_KOKIRI_SWORD) | can_use(bundle, Items.RG_BOOMERANG) | has_explosives(bundle) | has_item(bundle, Items.RG_GORONS_BRACELET) | can_use(bundle, Items.RG_MASTER_SWORD) | can_use(bundle, Items.RG_MEGATON_HAMMER) | can_use(bundle, Items.RG_BIGGORON_SWORD) | can_use(bundle, Items.RG_GIANTS_KNIFE) + +def can_get_deku_baba_nuts(bundle) -> bool: + return can_jumpslash(bundle) | can_use(bundle, Items.RG_FAIRY_SLINGSHOT) | can_use(bundle, Items.RG_FAIRY_BOW) | has_explosives(bundle) | can_use(bundle, Items.RG_DINS_FIRE) + +def can_get_deku_baba_sticks(bundle) -> bool: + return can_use_sword(bundle) | can_use(bundle, Items.RG_BOOMERANG) + +def can_get_drop(bundle, e: Enemies, distance: EnemyDistance = EnemyDistance.ED_CLOSE, above_link: bool = False) -> bool: + return can_kill(bundle, e, distance, True, 1, False, False) & (True_() if distance_to_int(bundle, distance) <= distance_to_int(bundle, EnemyDistance.ED_MASTER_SWORD_JUMPSLASH) else rls_match_rule((lambda e=e: e == Enemies.RE_GOLD_SKULLTULA), (lambda: _can_get_drop_gold_skulltula(bundle, distance)), False, (lambda e=e: e == Enemies.RE_KEESE or e == Enemies.RE_FIRE_KEESE or e == Enemies.RE_GUAY), (lambda: True_()), False, (lambda: True), (lambda: True_() if above_link else can_use(bundle, Items.RG_BOOMERANG) if distance_to_int(bundle, distance) <= distance_to_int(bundle, EnemyDistance.ED_BOOMERANG) else False_()), False)) + +def can_get_night_time_gs(bundle) -> bool: + return at_night(bundle) & (can_use(bundle, Items.RG_SUNS_SONG) | True_(options=[OptionFilter(RSK_SKULLS_SUNS_SONG, False)])) + +def can_jumpslash(bundle) -> bool: + return can_jumpslash_except_hammer(bundle) | can_use(bundle, Items.RG_MEGATON_HAMMER) + +def can_jumpslash_except_hammer(bundle) -> bool: + return can_use(bundle, Items.RG_STICKS) | can_use_sword(bundle) + +def can_kill(bundle, e: Enemies, distance: EnemyDistance = EnemyDistance.ED_CLOSE, wall_or_floor: bool = True, quantity: int = 1, timer: bool = False, in_water: bool = False) -> bool: + return rls_match_rule((lambda e=e: e == Enemies.RE_GOLD_SKULLTULA), (lambda: _can_kill_gold_skulltula(bundle, distance, wall_or_floor)), False) + +def can_open_storms_grotto(bundle) -> bool: + return can_use(bundle, Items.RG_SONG_OF_STORMS) & (has_item(bundle, Items.RG_STONE_OF_AGONY) | can_do_trick(bundle, Tricks.RT_GROTTOS_WITHOUT_AGONY)) + +def can_pass(bundle, e: Enemies, distance: EnemyDistance = EnemyDistance.ED_CLOSE, wall_or_floor: bool = True) -> bool: + return can_kill(bundle, e, distance, wall_or_floor, 1, False, False) | rls_match_rule((lambda e=e: e == Enemies.RE_GOLD_SKULLTULA), (lambda: True_()), False) + +def can_spawn_soil_skull(bundle, bean: Items) -> bool: + return is_child(bundle) & can_use(bundle, Items.RG_BOTTLE_WITH_BUGS) & has_item(bundle, bean) + +def can_use_sword(bundle) -> bool: + return can_use(bundle, Items.RG_KOKIRI_SWORD) | can_use(bundle, Items.RG_MASTER_SWORD) | can_use(bundle, Items.RG_BIGGORON_SWORD) + +def distance_to_int(bundle, distance: EnemyDistance) -> int: + return rls_match_value(0, (lambda distance=distance: distance == EnemyDistance.ED_CLOSE), (lambda: 0), False, (lambda distance=distance: distance == EnemyDistance.ED_SHORT_JUMPSLASH), (lambda: 1), False, (lambda distance=distance: distance == EnemyDistance.ED_MASTER_SWORD_JUMPSLASH), (lambda: 2), False, (lambda distance=distance: distance == EnemyDistance.ED_LONG_JUMPSLASH), (lambda: 3), False, (lambda distance=distance: distance == EnemyDistance.ED_BOMB_THROW), (lambda: 4), False, (lambda distance=distance: distance == EnemyDistance.ED_BOOMERANG), (lambda: 5), False, (lambda distance=distance: distance == EnemyDistance.ED_HOOKSHOT), (lambda: 6), False, (lambda distance=distance: distance == EnemyDistance.ED_LONGSHOT), (lambda: 7), False, (lambda distance=distance: distance == EnemyDistance.ED_FAR), (lambda: 8), False) + +def has_explosives(bundle) -> bool: + return can_use(bundle, Items.RG_BOMB_BAG) | can_use(bundle, Items.RG_BOMBCHU_5) + +def hookshot_or_boomerang(bundle) -> bool: + return can_use(bundle, Items.RG_HOOKSHOT) | can_use(bundle, Items.RG_BOOMERANG) diff --git a/examples/soh_ap/regions.gen.py b/examples/soh_ap/regions.gen.py new file mode 100644 index 0000000..c8e384d --- /dev/null +++ b/examples/soh_ap/regions.gen.py @@ -0,0 +1,357 @@ +# Generated by RLS soh_ap transpiler +from .functions.gen import * + +if TYPE_CHECKING: + from ... import SohWorld + +def set_region_rules(world: "SohWorld") -> None: + # Adult Spawn + # Exits + connect_regions(Regions.RR_ADULT_SPAWN, world, [ + (Regions.RR_TEMPLE_OF_TIME, lambda bundle: True_()), + ]) + + # Bolero of Fire Warp + # Exits + connect_regions(Regions.RR_BOLERO_OF_FIRE_WARP, world, [ + (Regions.RR_DMC_PAD_ENTRY, lambda bundle: True_()), + ]) + + # Child Spawn + # Exits + connect_regions(Regions.RR_CHILD_SPAWN, world, [ + (Regions.RR_KF_LINKS_HOUSE, lambda bundle: True_()), + ]) + + # KF Boulder Loop + # Locations + add_locations(Regions.RR_KF_BOULDER_LOOP, world, [ + (Locations.RC_KF_KOKIRI_SWORD_CHEST, lambda bundle: is_child(bundle) & has_item(bundle, Items.RG_OPEN_CHEST)), + (Locations.RC_KF_BOULDER_RUPEE_1, lambda bundle: is_child(bundle)), + (Locations.RC_KF_BOULDER_RUPEE_2, lambda bundle: is_child(bundle)), + (Locations.RC_KF_CHILD_GRASS_MAZE_1, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_MAZE_2, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_MAZE_3, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + ]) + # Exits + connect_regions(Regions.RR_KF_BOULDER_LOOP, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: can_use(bundle, Items.RG_CRAWL)), + ]) + + # KF House of Twins + # Locations + add_locations(Regions.RR_KF_HOUSE_OF_TWINS, world, [ + (Locations.RC_KF_TWINS_HOUSE_POT_1, lambda bundle: has_item(bundle, Items.RG_POWER_BRACELET)), + (Locations.RC_KF_TWINS_HOUSE_POT_2, lambda bundle: has_item(bundle, Items.RG_POWER_BRACELET)), + ]) + # Exits + connect_regions(Regions.RR_KF_HOUSE_OF_TWINS, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + ]) + + # KF Know It All House + # Locations + add_locations(Regions.RR_KF_KNOW_IT_ALL_HOUSE, world, [ + (Locations.RC_KF_BROTHERS_HOUSE_POT_1, lambda bundle: has_item(bundle, Items.RG_POWER_BRACELET)), + (Locations.RC_KF_BROTHERS_HOUSE_POT_2, lambda bundle: has_item(bundle, Items.RG_POWER_BRACELET)), + ]) + # Exits + connect_regions(Regions.RR_KF_KNOW_IT_ALL_HOUSE, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + ]) + + # KF Kokiri Shop + # Locations + add_locations(Regions.RR_KF_KOKIRI_SHOP, world, [ + (Locations.RC_KF_SHOP_ITEM_1, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_1)), + (Locations.RC_KF_SHOP_ITEM_2, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_2)), + (Locations.RC_KF_SHOP_ITEM_3, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_3)), + (Locations.RC_KF_SHOP_ITEM_4, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_4)), + (Locations.RC_KF_SHOP_ITEM_5, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_5)), + (Locations.RC_KF_SHOP_ITEM_6, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_6)), + (Locations.RC_KF_SHOP_ITEM_7, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_7)), + (Locations.RC_KF_SHOP_ITEM_8, lambda bundle: has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_afford_slot(Locations.RC_KF_SHOP_ITEM_8)), + ]) + # Exits + connect_regions(Regions.RR_KF_KOKIRI_SHOP, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + ]) + + # KF Link's House + # Locations + add_locations(Regions.RR_KF_LINKS_HOUSE, world, [ + (Locations.RC_KF_LINKS_HOUSE_COW, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_EPONAS_SONG) & has_item(bundle, Events.LOGIC_LINKS_COW)), + (Locations.RC_KF_LINKS_HOUSE_POT, lambda bundle: has_item(bundle, Items.RG_POWER_BRACELET)), + ]) + # Exits + connect_regions(Regions.RR_KF_LINKS_HOUSE, world, [ + (Regions.RR_KF_LINKS_PORCH, lambda bundle: True_()), + ]) + + # KF Link's Porch + # Exits + connect_regions(Regions.RR_KF_LINKS_PORCH, world, [ + (Regions.RR_KF_LINKS_HOUSE, lambda bundle: True_()), + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + ]) + + # KF Mido's House + # Locations + add_locations(Regions.RR_KF_MIDOS_HOUSE, world, [ + (Locations.RC_KF_MIDOS_TOP_LEFT_CHEST, lambda bundle: has_item(bundle, Items.RG_OPEN_CHEST)), + (Locations.RC_KF_MIDOS_TOP_RIGHT_CHEST, lambda bundle: has_item(bundle, Items.RG_OPEN_CHEST)), + (Locations.RC_KF_MIDOS_BOTTOM_LEFT_CHEST, lambda bundle: has_item(bundle, Items.RG_OPEN_CHEST)), + (Locations.RC_KF_MIDOS_BOTTOM_RIGHT_CHEST, lambda bundle: has_item(bundle, Items.RG_OPEN_CHEST)), + ]) + # Exits + connect_regions(Regions.RR_KF_MIDOS_HOUSE, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + ]) + + # KF Outside Deku Tree + # Events + add_events(Regions.RR_KF_OUTSIDE_DEKU_TREE, world, [ + (EventLocations.RR_KF_OUTSIDE_DEKU_TREE_LOGIC_STICK_ACCESS, Events.LOGIC_STICK_ACCESS, lambda bundle: can_get_deku_baba_sticks(bundle)), + (EventLocations.RR_KF_OUTSIDE_DEKU_TREE_LOGIC_NUT_ACCESS, Events.LOGIC_NUT_ACCESS, lambda bundle: can_get_deku_baba_nuts(bundle)), + (EventLocations.RR_KF_OUTSIDE_DEKU_TREE_LOGIC_FAIRY_ACCESS, Events.LOGIC_FAIRY_ACCESS, lambda bundle: call_gossip_fairy_except_suns(bundle)), + (EventLocations.RR_KF_OUTSIDE_DEKU_TREE_LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD, Events.LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD, lambda bundle: is_child(bundle) & has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_use(bundle, Items.RG_KOKIRI_SWORD) & can_use(bundle, Items.RG_DEKU_SHIELD)), + ]) + # Locations + add_locations(Regions.RR_KF_OUTSIDE_DEKU_TREE, world, [ + (Locations.RC_KF_DEKU_TREE_LEFT_GOSSIP_STONE_FAIRY, lambda bundle: call_gossip_fairy_except_suns(bundle)), + (Locations.RC_KF_DEKU_TREE_LEFT_GOSSIP_STONE_FAIRY_BIG, lambda bundle: can_use(bundle, Items.RG_SONG_OF_STORMS)), + (Locations.RC_KF_DEKU_TREE_RIGHT_GOSSIP_STONE_FAIRY, lambda bundle: call_gossip_fairy_except_suns(bundle)), + (Locations.RC_KF_DEKU_TREE_RIGHT_GOSSIP_STONE_FAIRY_BIG, lambda bundle: can_use(bundle, Items.RG_SONG_OF_STORMS)), + (Locations.RC_KF_DEKU_TREE_LEFT_GOSSIP_STONE, lambda bundle: True_()), + (Locations.RC_KF_DEKU_TREE_RIGHT_GOSSIP_STONE, lambda bundle: True_()), + ]) + # Exits + connect_regions(Regions.RR_KF_OUTSIDE_DEKU_TREE, world, [ + (Regions.RR_DEKU_TREE_ENTRYWAY, lambda bundle: is_child(bundle) | True_(options=[OptionFilter(RSK_SHUFFLE_DUNGEON_ENTRANCES, RandomizerSettingKey.RO_DUNGEON_ENTRANCE_SHUFFLE_OFF, "ne")]) & (True_(options=[OptionFilter(RSK_FOREST, RandomizerSettingKey.RO_CLOSED_FOREST_OFF)]) | has_item(bundle, Events.LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD))), + (Regions.RR_KOKIRI_FOREST, lambda bundle: is_adult(bundle) & (can_pass(bundle, Enemies.RE_BIG_SKULLTULA, EnemyDistance.ED_CLOSE, True) | has_item(bundle, Events.LOGIC_DEKU_TREE_CLEAR)) | True_(options=[OptionFilter(RSK_FOREST, RandomizerSettingKey.RO_CLOSED_FOREST_OFF)]) | has_item(bundle, Events.LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD)), + ]) + + # KF Outside Lost Woods + # Locations + add_locations(Regions.RR_KF_OUTSIDE_LOST_WOODS, world, [ + (Locations.RC_KF_GOSSIP_STONE_FAIRY, lambda bundle: call_gossip_fairy_except_suns(bundle)), + (Locations.RC_KF_GOSSIP_STONE_FAIRY_BIG, lambda bundle: can_use(bundle, Items.RG_SONG_OF_STORMS)), + (Locations.RC_KF_BEAN_RUPEE_1, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_BOOMERANG)), + (Locations.RC_KF_BEAN_RUPEE_2, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_BOOMERANG)), + (Locations.RC_KF_BEAN_RUPEE_3, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_BOOMERANG)), + (Locations.RC_KF_BEAN_RUPEE_4, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_BOOMERANG)), + (Locations.RC_KF_BEAN_RUPEE_5, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_BOOMERANG)), + (Locations.RC_KF_BEAN_RUPEE_6, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_BOOMERANG)), + (Locations.RC_KF_BEAN_RED_RUPEE, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_BOOMERANG)), + (Locations.RC_KF_GOSSIP_STONE, lambda bundle: True_()), + ]) + # Exits + connect_regions(Regions.RR_KF_OUTSIDE_LOST_WOODS, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + (Regions.RR_THE_LOST_WOODS, lambda bundle: True_()), + (Regions.RR_KF_RUPEE_ALCOVE, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS))), + (Regions.RR_KF_STORMS_GROTTO, lambda bundle: can_open_storms_grotto(bundle)), + ]) + + # KF Alcove + # Locations + add_locations(Regions.RR_KF_RUPEE_ALCOVE, world, [ + (Locations.RC_KF_BEAN_RUPEE_1, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_HOVER_BOOTS)), + (Locations.RC_KF_BEAN_RUPEE_2, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_HOVER_BOOTS)), + (Locations.RC_KF_BEAN_RUPEE_3, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_HOVER_BOOTS)), + (Locations.RC_KF_BEAN_RUPEE_4, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_HOVER_BOOTS)), + (Locations.RC_KF_BEAN_RUPEE_5, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_HOVER_BOOTS)), + (Locations.RC_KF_BEAN_RUPEE_6, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_HOVER_BOOTS)), + (Locations.RC_KF_BEAN_RED_RUPEE, lambda bundle: is_adult(bundle) & can_use(bundle, Items.RG_HOVER_BOOTS)), + ]) + # Exits + connect_regions(Regions.RR_KF_RUPEE_ALCOVE, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + ]) + + # KF Saria's House + # Locations + add_locations(Regions.RR_KF_SARIAS_HOUSE, world, [ + (Locations.RC_KF_SARIAS_TOP_LEFT_HEART, lambda bundle: True_()), + (Locations.RC_KF_SARIAS_TOP_RIGHT_HEART, lambda bundle: True_()), + (Locations.RC_KF_SARIAS_BOTTOM_LEFT_HEART, lambda bundle: True_()), + (Locations.RC_KF_SARIAS_BOTTOM_RIGHT_HEART, lambda bundle: True_()), + ]) + # Exits + connect_regions(Regions.RR_KF_SARIAS_HOUSE, world, [ + (Regions.RR_KOKIRI_FOREST, lambda bundle: True_()), + ]) + + # KF Storms Grotto + # Events + add_events(Regions.RR_KF_STORMS_GROTTO, world, [ + (EventLocations.RR_KF_STORMS_GROTTO_LOGIC_FAIRY_ACCESS, Events.LOGIC_FAIRY_ACCESS, lambda bundle: call_gossip_fairy(bundle) | can_use(bundle, Items.RG_STICKS)), + (EventLocations.RR_KF_STORMS_GROTTO_LOGIC_BUG_ACCESS, Events.LOGIC_BUG_ACCESS, lambda bundle: can_cut_shrubs(bundle)), + (EventLocations.RR_KF_STORMS_GROTTO_LOGIC_FISH_ACCESS, Events.LOGIC_FISH_ACCESS, lambda bundle: True_()), + ]) + # Locations + add_locations(Regions.RR_KF_STORMS_GROTTO, world, [ + (Locations.RC_KF_STORMS_GROTTO_CHEST, lambda bundle: has_item(bundle, Items.RG_OPEN_CHEST)), + (Locations.RC_KF_STORMS_GROTTO_BEEHIVE_LEFT, lambda bundle: can_break_lower_beehives(bundle)), + (Locations.RC_KF_STORMS_GROTTO_BEEHIVE_RIGHT, lambda bundle: can_break_lower_beehives(bundle)), + (Locations.RC_KF_STORMS_GROTTO_GOSSIP_STONE_FAIRY, lambda bundle: call_gossip_fairy(bundle)), + (Locations.RC_KF_STORMS_GROTTO_GOSSIP_STONE_FAIRY_BIG, lambda bundle: can_use(bundle, Items.RG_SONG_OF_STORMS)), + (Locations.RC_KF_STORMS_GROTTO_FISH, lambda bundle: has_bottle(bundle)), + (Locations.RC_KF_STORMS_GROTTO_GRASS_1, lambda bundle: can_cut_shrubs(bundle)), + (Locations.RC_KF_STORMS_GROTTO_GRASS_2, lambda bundle: can_cut_shrubs(bundle)), + (Locations.RC_KF_STORMS_GROTTO_GRASS_3, lambda bundle: can_cut_shrubs(bundle)), + (Locations.RC_KF_STORMS_GROTTO_GRASS_4, lambda bundle: can_cut_shrubs(bundle)), + (Locations.RC_KF_STORMS_GROTTO_GOSSIP_STONE, lambda bundle: True_()), + ]) + # Exits + connect_regions(Regions.RR_KF_STORMS_GROTTO, world, [ + (Regions.RR_KF_OUTSIDE_LOST_WOODS, lambda bundle: True_()), + ]) + + # Kokiri Forest + # Events + add_events(Regions.RR_KOKIRI_FOREST, world, [ + (EventLocations.RR_KOKIRI_FOREST_LOGIC_FAIRY_ACCESS, Events.LOGIC_FAIRY_ACCESS, lambda bundle: call_gossip_fairy_except_suns(bundle) | is_child(bundle) & can_use(bundle, Items.RG_MAGIC_BEAN) & has_item(bundle, Items.RG_KOKIRI_FOREST_BEAN_SOUL) & can_use(bundle, Items.RG_SONG_OF_STORMS)), + (EventLocations.RR_KOKIRI_FOREST_LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD, Events.LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD, lambda bundle: is_child(bundle) & has_item(bundle, Items.RG_SPEAK_KOKIRI) & can_use(bundle, Items.RG_KOKIRI_SWORD) & can_use(bundle, Items.RG_DEKU_SHIELD)), + ]) + # Locations + add_locations(Regions.RR_KOKIRI_FOREST, world, [ + (Locations.RC_KF_BEAN_SPROUT_FAIRY_1, lambda bundle: is_child(bundle) & can_use(bundle, Items.RG_MAGIC_BEAN) & has_item(bundle, Items.RG_KOKIRI_FOREST_BEAN_SOUL) & can_use(bundle, Items.RG_SONG_OF_STORMS)), + (Locations.RC_KF_BEAN_SPROUT_FAIRY_2, lambda bundle: is_child(bundle) & can_use(bundle, Items.RG_MAGIC_BEAN) & has_item(bundle, Items.RG_KOKIRI_FOREST_BEAN_SOUL) & can_use(bundle, Items.RG_SONG_OF_STORMS)), + (Locations.RC_KF_BEAN_SPROUT_FAIRY_3, lambda bundle: is_child(bundle) & can_use(bundle, Items.RG_MAGIC_BEAN) & has_item(bundle, Items.RG_KOKIRI_FOREST_BEAN_SOUL) & can_use(bundle, Items.RG_SONG_OF_STORMS)), + (Locations.RC_KF_BRIDGE_RUPEE, lambda bundle: is_child(bundle)), + (Locations.RC_KF_BEHIND_MIDOS_RUPEE, lambda bundle: is_child(bundle)), + (Locations.RC_KF_SOUTH_GRASS_WEST_RUPEE, lambda bundle: is_child(bundle)), + (Locations.RC_KF_SOUTH_GRASS_EAST_RUPEE, lambda bundle: is_child(bundle)), + (Locations.RC_KF_NORTH_GRASS_WEST_RUPEE, lambda bundle: is_child(bundle)), + (Locations.RC_KF_NORTH_GRASS_EAST_RUPEE, lambda bundle: is_child(bundle)), + (Locations.RC_KF_SARIAS_ROOF_WEST_HEART, lambda bundle: is_child(bundle)), + (Locations.RC_KF_SARIAS_ROOF_EAST_HEART, lambda bundle: is_child(bundle)), + (Locations.RC_KF_SARIAS_ROOF_NORTH_HEART, lambda bundle: is_child(bundle)), + (Locations.RC_KF_BEAN_RUPEE_1, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS) | can_use(bundle, Items.RG_BOOMERANG))), + (Locations.RC_KF_BEAN_RUPEE_2, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS) | can_use(bundle, Items.RG_BOOMERANG))), + (Locations.RC_KF_BEAN_RUPEE_3, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS) | can_use(bundle, Items.RG_BOOMERANG))), + (Locations.RC_KF_BEAN_RUPEE_4, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS) | can_use(bundle, Items.RG_BOOMERANG))), + (Locations.RC_KF_BEAN_RUPEE_5, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS) | can_use(bundle, Items.RG_BOOMERANG))), + (Locations.RC_KF_BEAN_RUPEE_6, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS) | can_use(bundle, Items.RG_BOOMERANG))), + (Locations.RC_KF_BEAN_RED_RUPEE, lambda bundle: is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_use(bundle, Items.RG_HOVER_BOOTS) | can_use(bundle, Items.RG_BOOMERANG))), + (Locations.RC_KF_GS_KNOW_IT_ALL_HOUSE, lambda bundle: is_child(bundle) & can_kill(bundle, Enemies.RE_GOLD_SKULLTULA, EnemyDistance.ED_CLOSE, True, 1, False, False) & can_get_night_time_gs(bundle)), + (Locations.RC_KF_GS_BEAN_PATCH, lambda bundle: can_spawn_soil_skull(bundle, Items.RG_KOKIRI_FOREST_BEAN_SOUL) & can_kill(bundle, Enemies.RE_GOLD_SKULLTULA, EnemyDistance.ED_CLOSE, True, 1, False, False)), + (Locations.RC_KF_GS_HOUSE_OF_TWINS, lambda bundle: is_adult(bundle) & can_get_night_time_gs(bundle) & (can_get_drop(bundle, Enemies.RE_GOLD_SKULLTULA, EnemyDistance.ED_BOOMERANG, False) | can_do_trick(bundle, Tricks.RT_KF_ADULT_GS) & can_use(bundle, Items.RG_HOVER_BOOTS) & can_kill(bundle, Enemies.RE_GOLD_SKULLTULA, EnemyDistance.ED_SHORT_JUMPSLASH, True, 1, False, False))), + (Locations.RC_KF_CHILD_GRASS_1, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_2, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_3, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_4, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_5, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_6, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_7, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_8, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_9, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_10, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_11, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_CHILD_GRASS_12, lambda bundle: is_child(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_1, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_2, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_3, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_4, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_5, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_6, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_7, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_8, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_9, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_10, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_11, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_12, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_13, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_14, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_15, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_16, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_17, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_18, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_19, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + (Locations.RC_KF_ADULT_GRASS_20, lambda bundle: is_adult(bundle) & can_cut_shrubs(bundle)), + ]) + # Exits + connect_regions(Regions.RR_KOKIRI_FOREST, world, [ + (Regions.RR_KF_BOULDER_LOOP, lambda bundle: can_use(bundle, Items.RG_CRAWL)), + (Regions.RR_KF_LINKS_PORCH, lambda bundle: (is_child(bundle) & can_climb_ladder(bundle)) | (has_item(bundle, Items.RG_CLIMB) | can_use(bundle, Items.RG_HOVER_BOOTS))), + (Regions.RR_KF_MIDOS_HOUSE, lambda bundle: True_()), + (Regions.RR_KF_SARIAS_HOUSE, lambda bundle: True_()), + (Regions.RR_KF_HOUSE_OF_TWINS, lambda bundle: True_()), + (Regions.RR_KF_KNOW_IT_ALL_HOUSE, lambda bundle: True_()), + (Regions.RR_KF_KOKIRI_SHOP, lambda bundle: True_()), + (Regions.RR_KF_OUTSIDE_DEKU_TREE, lambda bundle: has_item(bundle, Events.LOGIC_SHOWED_MIDO_SWORD_AND_SHIELD) | True_(options=[OptionFilter(RSK_FOREST, RandomizerSettingKey.RO_CLOSED_FOREST_OFF)]) | is_adult(bundle) & (can_pass(bundle, Enemies.RE_BIG_SKULLTULA, EnemyDistance.ED_CLOSE, True) | has_item(bundle, Events.LOGIC_FOREST_TEMPLE_CLEAR))), + (Regions.RR_KF_OUTSIDE_LOST_WOODS, lambda bundle: has_item(bundle, Items.RG_CLIMB) | can_use(bundle, Items.RG_HOOKSHOT) | is_adult(bundle) & (can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL) | can_do_trick(bundle, Tricks.RT_UNINTUITIVE_JUMPS))), + (Regions.RR_KF_RUPEE_ALCOVE, lambda bundle: is_adult(bundle) & can_plant_bean(bundle, Regions.RR_KOKIRI_FOREST, Items.RG_KOKIRI_FOREST_BEAN_SOUL)), + (Regions.RR_LW_BRIDGE_FROM_FOREST, lambda bundle: is_adult(bundle) | True_(options=[OptionFilter(RSK_FOREST, RandomizerSettingKey.RO_CLOSED_FOREST_ON, "ne")]) | has_item(bundle, Events.LOGIC_DEKU_TREE_CLEAR)), + ]) + + # Minuet of Forest Warp + # Exits + connect_regions(Regions.RR_MINUET_OF_FOREST_WARP, world, [ + (Regions.RR_SACRED_FOREST_MEADOW, lambda bundle: True_()), + ]) + + # Nocturne of Shadow Warp + # Exits + connect_regions(Regions.RR_NOCTURNE_OF_SHADOW_WARP, world, [ + (Regions.RR_GRAVEYARD_WARP_PAD_REGION, lambda bundle: True_()), + ]) + + # Prelude of Light Warp + # Exits + connect_regions(Regions.RR_PRELUDE_OF_LIGHT_WARP, world, [ + (Regions.RR_TEMPLE_OF_TIME, lambda bundle: True_()), + ]) + + # Requiem of Spirit Warp + # Exits + connect_regions(Regions.RR_REQUIEM_OF_SPIRIT_WARP, world, [ + (Regions.RR_DESERT_COLOSSUS, lambda bundle: True_()), + ]) + + # Root + # Events + add_events(Regions.RR_ROOT, world, [ + (EventLocations.RR_ROOT_LOGIC_KAKARIKO_GATE_OPEN, Events.LOGIC_KAKARIKO_GATE_OPEN, lambda bundle: True_(options=[OptionFilter(RSK_KAK_GATE, RandomizerSettingKey.RO_KAK_GATE_OPEN)])), + (EventLocations.RR_ROOT_LOGIC_TH_COULD_FREE_1_TORCH_CARPENTER, Events.LOGIC_TH_COULD_FREE_1_TORCH_CARPENTER, lambda bundle: True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FREE)])), + (EventLocations.RR_ROOT_LOGIC_TH_COULD_FREE_DOUBLE_CELL_CARPENTER, Events.LOGIC_TH_COULD_FREE_DOUBLE_CELL_CARPENTER, lambda bundle: True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FREE)]) | True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FAST)])), + (EventLocations.RR_ROOT_LOGIC_TH_COULD_FREE_DEAD_END_CARPENTER, Events.LOGIC_TH_COULD_FREE_DEAD_END_CARPENTER, lambda bundle: True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FREE)]) | True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FAST)])), + (EventLocations.RR_ROOT_LOGIC_TH_COULD_FREE_SLOPE_CARPENTER, Events.LOGIC_TH_COULD_FREE_SLOPE_CARPENTER, lambda bundle: True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FREE)]) | True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FAST)])), + (EventLocations.RR_ROOT_LOGIC_TH_RESCUED_ALL_CARPENTERS, Events.LOGIC_TH_RESCUED_ALL_CARPENTERS, lambda bundle: True_(options=[OptionFilter(RSK_GERUDO_FORTRESS, RandomizerSettingKey.RO_GF_CARPENTERS_FREE)])), + (EventLocations.RR_ROOT_LOGIC_FREED_EPONA, Events.LOGIC_FREED_EPONA, lambda bundle: True_(options=[OptionFilter(RSK_SKIP_EPONA_RACE, True)])), + ]) + # Locations + add_locations(Regions.RR_ROOT, world, [ + (Locations.RC_LINKS_POCKET, lambda bundle: True_()), + (Locations.RC_TRIFORCE_COMPLETED, lambda bundle: CanWinTriforceHunt()), + (Locations.RC_SARIA_SONG_HINT, lambda bundle: can_use(bundle, Items.RG_SARIAS_SONG)), + (Locations.RC_SONG_FROM_IMPA, lambda bundle: True_(options=[OptionFilter(RSK_SKIP_CHILD_ZELDA, True)])), + (Locations.RC_HC_MALON_EGG, lambda bundle: True_(options=[OptionFilter(RSK_SKIP_CHILD_ZELDA, True)])), + (Locations.RC_HC_ZELDAS_LETTER, lambda bundle: True_(options=[OptionFilter(RSK_SKIP_CHILD_ZELDA, True)])), + (Locations.RC_TOT_MASTER_SWORD, lambda bundle: True_(options=[OptionFilter(RSK_SELECTED_STARTING_AGE, RandomizerSettingKey.RO_AGE_ADULT)])), + ]) + # Exits + connect_regions(Regions.RR_ROOT, world, [ + (Regions.RR_ROOT_EXITS, lambda bundle: True_()), + ]) + + # Root Exits + # Exits + connect_regions(Regions.RR_ROOT_EXITS, world, [ + (Regions.RR_CHILD_SPAWN, lambda bundle: is_child(bundle)), + (Regions.RR_ADULT_SPAWN, lambda bundle: is_adult(bundle)), + (Regions.RR_MINUET_OF_FOREST_WARP, lambda bundle: can_use(bundle, Items.RG_MINUET_OF_FOREST)), + (Regions.RR_BOLERO_OF_FIRE_WARP, lambda bundle: can_use(bundle, Items.RG_BOLERO_OF_FIRE)), + (Regions.RR_SERENADE_OF_WATER_WARP, lambda bundle: can_use(bundle, Items.RG_SERENADE_OF_WATER)), + (Regions.RR_NOCTURNE_OF_SHADOW_WARP, lambda bundle: can_use(bundle, Items.RG_NOCTURNE_OF_SHADOW)), + (Regions.RR_REQUIEM_OF_SPIRIT_WARP, lambda bundle: can_use(bundle, Items.RG_REQUIEM_OF_SPIRIT)), + (Regions.RR_PRELUDE_OF_LIGHT_WARP, lambda bundle: can_use(bundle, Items.RG_PRELUDE_OF_LIGHT)), + ]) + + # Serenade of Water Warp + # Exits + connect_regions(Regions.RR_SERENADE_OF_WATER_WARP, world, [ + (Regions.RR_LAKE_HYLIA, lambda bundle: True_()), + ]) + diff --git a/sema/tests/validate_declarations_tests.cpp b/sema/tests/validate_declarations_tests.cpp index f606fdd..27e0b85 100644 --- a/sema/tests/validate_declarations_tests.cpp +++ b/sema/tests/validate_declarations_tests.cpp @@ -1,5 +1,7 @@ #include +#include + #include "ast.h" #include "parser.h" #include "sema.h" diff --git a/transpilers/CMakeLists.txt b/transpilers/CMakeLists.txt index 02bb10f..ae061b2 100644 --- a/transpilers/CMakeLists.txt +++ b/transpilers/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(soh) add_subdirectory(ap) +add_subdirectory(soh_ap) diff --git a/transpilers/ap/CMakeLists.txt b/transpilers/ap/CMakeLists.txt index f649679..8f52622 100644 --- a/transpilers/ap/CMakeLists.txt +++ b/transpilers/ap/CMakeLists.txt @@ -1,17 +1,17 @@ -file(GLOB ap_sources CONFIGURE_DEPENDS +file(GLOB ap_transpiler_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" ) -add_library(ap STATIC ${ap_sources}) +add_library(ap_transpiler STATIC ${ap_transpiler_sources}) -target_include_directories(ap PUBLIC include) -target_link_libraries(ap PUBLIC ast) +target_include_directories(ap_transpiler PUBLIC include) +target_link_libraries(ap_transpiler PUBLIC ast) if(BUILD_TESTING) - file(GLOB ap_test_sources CONFIGURE_DEPENDS + file(GLOB ap_transpiler_test_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp" ) - rls_add_gtest(ap_tests ${ap_test_sources}) - target_link_libraries(ap_tests PRIVATE ap) + rls_add_gtest(ap_transpiler_tests ${ap_transpiler_test_sources}) + target_link_libraries(ap_transpiler_tests PRIVATE ap_transpiler parser sema) endif() diff --git a/transpilers/ap/include/ap.h b/transpilers/ap/include/ap.h deleted file mode 100644 index 6f0055c..0000000 --- a/transpilers/ap/include/ap.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "ast.h" -#include "output.h" - -namespace rls::transpilers::ap { - -void Transpile(const rls::ast::Project& project, rls::OutputWriter& out); - -} // namespace rls::transpilers::ap diff --git a/transpilers/ap/include/ap_transpiler.h b/transpilers/ap/include/ap_transpiler.h new file mode 100644 index 0000000..bf926d1 --- /dev/null +++ b/transpilers/ap/include/ap_transpiler.h @@ -0,0 +1,329 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "ast.h" +#include "output.h" + +namespace rls::transpilers::ap { + +// Generic RLS -> Archipelago RuleBuilder transpiler. +// +// This base class owns everything that is true of *any* AP world: the expression +// walk, Python operator precedence/parentheses, OptionFilter wrapping of setting +// comparisons, True_/False_ rule literals, lambda composition, and the region/enum/ +// function file structure. +// +// Every game-specific decision is a protected virtual hook. Hooks with a sensible +// generic default are implemented here; hooks that are pure scaffolding (region +// helper names, enum classes, Python type names) are pure virtual, which makes this +// class abstract. A concrete game transpiler derives from it and overrides the hooks +// it needs -- see SohApTranspiler. +class ApTranspiler { +public: + virtual ~ApTranspiler() = default; + + // Emit the source files for this transpiler. Derived classes choose which of + // the Generate*Source building blocks to call (e.g. SoH emits regions + enums). + virtual void Transpile(rls::OutputWriter& out) const = 0; + + std::string GenerateExpression(const rls::ast::ExprPtr& expr) const; + std::string GenerateExpression(const rls::ast::Expr::Variant& node) const; + void SetCurrentLocation(std::optional location) const; + + // True if the expression lowers to a runtime Rule object (see ValueClass::Rule). + // Thin wrapper over ClassifyExpression for callers that only care about rule-ness. + bool ExpressionIsRule(const rls::ast::ExprPtr& expr) const; + + // Diagnostics raised while generating expressions: constructs that type-check in RLS + // but cannot be expressed in the RuleBuilder target (negating a rule, a rule-valued + // ternary condition, combining a runtime non-rule value). Accumulated across a + // Transpile() so the driver can report them and abort rather than emit code that + // raises at world-load. See docs/AP-Function-Generation.md §6.4. + const std::vector& Diagnostics() const; + +protected: + explicit ApTranspiler(const rls::ast::Project& project); + + // Building blocks a derived Transpile() can choose from. + void GenerateFunctionDefinitionsSource(rls::OutputWriter& out) const; + void GenerateRegionsSource(rls::OutputWriter& out) const; + void GenerateEnumsSource(rls::OutputWriter& out) const; + + // == Game-specific hooks ================================================== + // Hooks with a generic default are defined in the base; override to change. + + // Name of the implicit first parameter threaded through generated rule + // lambdas, calls and function signatures (SoH uses "bundle"). Default: empty, + // meaning calls/signatures take no implicit receiver. + virtual std::string ruleContextParam() const; + + // Python expression for the world's options dataclass, reached from a rule lambda's + // receiver -- passed to OptionFilter.check() to evaluate a setting comparison as a + // build-time bool. How a lambda reaches world.options is world-specific (it depends on the + // receiver's shape), so the default is empty, which disables the build-time-setting lowering + // (a setting-conditioned ternary then falls back to a diagnostic). A world overrides this to + // enable it -- e.g. SoH's bundle is `(region, world)`, so it returns "bundle[1].options". + virtual std::string ruleContextOptions() const; + + // Render an enum-value identifier (e.g. RG_HOOKSHOT) to its Python form, given the + // name of the enum it belongs to (e.g. "Item") and the value name. Enums are keyed by + // name rather than by ast::Type: every enum shares the single Type::Enum, and the + // identity lives beside it in project.getEnumType(node). + // Default: the bare value name. Override to add world enum-class prefixes + // and value overrides (e.g. RO_GENERIC_YES -> "True"). + virtual std::string renderEnumValue(std::string_view enumName, const std::string& value) const; + + // Rewrite a host/builtin call (has, flag, trick, ...) to Python. Default: + // std::nullopt, so the core emits the default call form `callee(args...)` + // (with the ruleContextParam() receiver prepended when one is set). + // `overrideIdx`/`overrideExpr` let the ternary distribution re-render the call with one + // argument replaced (see renderCall); when overrideIdx is std::string::npos there is no + // override and every argument comes from the resolved call args as usual. + virtual std::optional renderHostCall(const rls::ast::CallExpr& node, + size_t overrideIdx = std::string::npos, const rls::ast::Expr* overrideExpr = nullptr) const; + + // Rewrite a world-specific binary special case. Default: std::nullopt + // (normal binary-operator handling). + virtual std::optional renderBinarySpecialCase(const rls::ast::BinaryExpr& node) const; + + // True if a user `define` of this name is supplied natively by the host world and so + // must NOT be emitted as a generated function. The canonical cases are defines whose AP + // lowering is a hand-written host rule (SoH's `has_bottle`) or that exist only to be + // folded away at their call sites (SoH's `wallet_capacity`, collapsed into + // `can_afford_slot` by renderBinarySpecialCase) -- emitting them would shadow the host + // helper or produce an unrepresentable body. Default: false (every define is emitted). + virtual bool isHostProvidedDefine(const std::string& name) const; + + // == Game-specific scaffolding (pure virtual: no generic AP default) ====== + + // Preamble for the regions file, ending with the rule-setup def line. + virtual std::string regionsPreamble() const = 0; + // Leading args of a region's helper call: e.g. `Regions., world, [\n`. + virtual std::string regionCreationArgs(const std::string& regionKey) const = 0; + // Names of the per-region helper calls. + virtual std::string addEventsFn() const = 0; + virtual std::string addLocationsFn() const = 0; + virtual std::string connectRegionsFn() const = 0; + // Per-entry tuple lines emitted inside each helper call. + virtual std::string eventEntryLine( + const std::string& regionKey, const std::string& entryName, const std::string& rule) const = 0; + virtual std::string locationEntryLine(const std::string& entryName, const std::string& rule) const = 0; + virtual std::string exitEntryLine(const std::string& entryName, const std::string& rule) const = 0; + // Emit the entire enums file (world enum-class scaffolding). + virtual void writeEnums(rls::OutputWriter& out) const = 0; + // Preamble for the functions file (header comment + imports). + virtual std::string functionsPreamble() const = 0; + // Python type name for an RLS type, used in generated function signatures. For + // Type::Enum, `enumName` carries which enum it is (from project.getEnumType); it is + // std::nullopt for every other type. + virtual std::string pythonTypeName( + rls::ast::Type type, std::optional enumName) const = 0; + + const rls::ast::Project& project; + mutable std::optional currentLocationName; + + // Record an error diagnostic for an unrepresentable construct at `span`. + void Diagnose(const rls::ast::Span& span, std::string message) const; + +private: + mutable std::vector diagnostics; + + // How a Bool/Int expression lowers to the RuleBuilder target. The keystone of + // function generation (see docs/AP-Function-Generation.md). The rule lambda + // `lambda bundle: ` runs ONCE to build a Rule tree; only the resulting + // Rule re-evaluates against collection state. So an expression's class is about + // *when* its value is known: + // - Rule: lowers to a Rule object whose truth is re-evaluated at solve time + // (has(X), can_use(X), setting comparisons, a define that is a Rule). + // - BuildTime: a plain Python value fixed when the lambda runs -- int/enum + // literals, parameters (bound to literals/config at the call that + // builds the rule), value comparisons over build-time operands, a + // value-define like distance_to_int. Safe to use as a ternary + // condition because it is frozen at build time. + // - Runtime: a non-rule value that depends on collection state, so it is NOT + // fixed at build time -- e.g. bottle_count() or a comparison over it. + // It cannot be a Rule *or* a build-time condition; it must be lowered + // to a host rule (like has_bottle_count) or rejected with a + // diagnostic. Folding it as a build-time condition would freeze it to + // its value in the initial (empty) collection state -- a miscompile. + // The RLS type alone does not decide this: has(X) and bottle_count() >= 1 are both + // Bool, but the first is Rule and the second is Runtime. + enum class ValueClass { Rule, BuildTime, Runtime }; + ValueClass ClassifyExpression(const rls::ast::Expr* expr) const; + ValueClass ClassifyExpression(const rls::ast::ExprPtr& expr) const; + + // The value class of a call, by its callee's return semantics (setting/define/extern). + // Shared by ClassifyExpression and the ternary distribution guard so both agree on which + // calls produce a Rule. + ValueClass ClassifyCall(const rls::ast::CallExpr& call) const; + + // Combine two operand classes for an operator that folds its operands: Runtime + // dominates Rule dominates BuildTime. + static ValueClass JoinClass(ValueClass a, ValueClass b); + + // A user define's class, computed from its body (with parameters treated as + // BuildTime). Memoized because the same define is queried repeatedly; the + // in-progress set breaks recursion cycles conservatively (a cycle is a Rule). + ValueClass DefineClass(const rls::ast::DefineDecl* decl) const; + mutable std::map defineClassCache; + mutable std::set defineClassInProgress; + + // How a Bool `and`/`or` lowers, given its operands' classes (§4.1 of + // docs/AP-Function-Generation.md): + // - RuleOp: both operands are rules -> `L & R` / `L | R` + // - PythonOp: both operands are build-time -> `L and R` / `L or R` + // - MixedTernary: one rule, one build-time -> `R if V else False_()` (and) / + // `True_() if V else R` (or) + // - Unrepresentable: a Runtime operand is involved -- cannot be expressed without a + // host rule; a Phase 2 diagnostic will reject it. + // GenerateExpression and GetPythonPrecedence both dispatch on this so the emitted + // form and its parenthesization stay in sync. + enum class AndOrLowering { RuleOp, PythonOp, MixedTernary, Unrepresentable }; + AndOrLowering ClassifyAndOr(const rls::ast::BinaryExpr& node) const; + + // True if `node` is a rule-conditioned ternary with rule branches, which lowers to the + // `(C & a) | b` rule idiom rather than a Python `if`. Shared by GenerateExpression and + // GetPythonPrecedence so the emitted form and its precedence stay in sync. + bool isRuleConditionedRuleTernary(const rls::ast::TernaryExpr& node) const; + + int GetPythonPrecedence(const rls::ast::ExprPtr& expr) const; + std::string GenerateChildExpression(const rls::ast::ExprPtr& expr, int parentPrec, bool isRightChild = false) const; + std::string GenerateExpression(const rls::ast::BoolLiteral& node) const; + std::string GenerateExpression(const rls::ast::IntLiteral& node) const; + std::string GenerateExpression(const rls::ast::StringLiteral& node) const; + std::string GenerateExpression(const rls::ast::ListExpr& node) const; + std::string GenerateExpression(const rls::ast::Identifier& node) const; + // `EnumName.ValueName` -- the dotted form disambiguating a value shared by two enums. + std::string GenerateExpression(const rls::ast::MemberExpr& node) const; + std::string GenerateExpression(const rls::ast::UnaryExpr& node) const; + std::string GenerateExpression(const rls::ast::BinaryExpr& node) const; + std::string GenerateExpression(const rls::ast::TernaryExpr& node) const; + std::string GenerateExpression(const rls::ast::CallExpr& node) const; + std::string GenerateExpression(const rls::ast::InvokeExpr& node) const; + + // The declared type of parameter `index` of the function `node` calls, looked up from + // the extern/define decl. std::nullopt if the callee or parameter cannot be resolved. + std::optional ResolveCallParamType(const rls::ast::CallExpr& node, size_t index) const; + + // Render one call argument, accounting for Condition parameters: a non-Condition argument + // bound to a Condition parameter is wrapped in a `(lambda : )` thunk so it is + // evaluated lazily; an argument already of Condition type is passed through unchanged. + std::string GenerateCallArgument(const rls::ast::Expr* argExpr, std::optional paramType) const; + + // Dispatch a call to its emitted form: the setting truthiness check, a world host-call + // rewrite (renderHostCall), or the default `(, ...)` form. `overrideIdx`/ + // `overrideExpr` replace one resolved argument (used when distributing a call over a ternary's + // branches -- each branch re-renders through this same dispatch so host rewrites still apply). + // Precondition: node has resolved call args. + std::string renderCall(const rls::ast::CallExpr& node, size_t overrideIdx, + const rls::ast::Expr* overrideExpr) const; + + // Render a plain function call `(, ...)`, threading the rule-context + // receiver. If `overrideIdx` is a valid argument index, `overrideExpr` is generated in place + // of the resolved argument there -- used to distribute a call over a ternary's branches. + // Precondition: node has resolved call args. + std::string renderDefaultCall(const rls::ast::CallExpr& node, size_t overrideIdx, + const rls::ast::Expr* overrideExpr) const; + + // If a call argument is a rule-conditioned ternary whose branches are (non-rule) build-time + // values -- e.g. `can_use(is_adult() ? RG_HOOKSHOT : RG_LONGSHOT)` -- the branches cannot be + // &/|-combined with the rule condition, so the ternary is not directly representable. Instead + // we lift the call over the ternary: `f(.., C ? A : B, ..)` becomes + // `rls_conditional(, C, f(..,A,..), f(..,B,..))`, a solve-time pick between the two rules + // that mirrors the source ternary exactly (see renderConditionalRule). Returns the distributed + // call when such an argument exists, else std::nullopt (the caller renders the call normally). + // A build-time or pure-setting condition is left alone (it stays an ordinary Python `if`), as + // is a runtime non-rule condition (which remains a diagnosed, unrepresentable value). + std::optional tryDistributeTernaryArg(const rls::ast::CallExpr& node) const; + + // Render a conditional (if-then-else) rule `rls_conditional(, , , )`: a + // host rule that evaluates at solve time and picks or accordingly. This + // is the faithful lowering of a rule-conditioned ternary -- unlike the `(C & a) | b` idiom it + // does not ungate the else-branch, and it needs no rule negation. + std::string renderConditionalRule(const std::string& cond, const std::string& thenExpr, + const std::string& elseExpr) const; + + std::string GenerateExpression(const rls::ast::HereRef& node) const; + std::string GenerateExpression(const rls::ast::MatchExpr& node) const; + + // True if this binary expression is a `setting(KEY) == VALUE` / `!= VALUE` comparison, + // which is emitted as an atomic OptionFilter rule rather than a Python comparison. + bool IsSettingComparison(const rls::ast::BinaryExpr& node) const; + + // Convert setting(KEY) == VALUE expressions to OptionFilter(...) form for RuleBuilder. + // Returns empty string if not a setting comparison; caller uses the fallback. + std::string TryGenerateOptionFilter(const rls::ast::BinaryExpr& node) const; + + // Render a setting comparison as an OptionFilter rule (precondition: IsSettingComparison). + // When `negate`, renders its negation by flipping eq <-> "ne". Shared by the positive + // path (TryGenerateOptionFilter) and the De Morgan negation. + std::string renderSettingOptionFilter(const rls::ast::BinaryExpr& node, bool negate) const; + + // The `, [, "ne"]` argument list for an OptionFilter (precondition: + // IsSettingComparison). `negate` flips eq <-> "ne". Shared by the rule-wrapping form + // (renderSettingOptionFilter) and the build-time .check() form (renderSettingCheck). + std::string optionFilterArgs(const rls::ast::BinaryExpr& node, bool negate) const; + + // Render a setting comparison as a build-time bool: `OptionFilter().check()`. + // Precondition: IsSettingComparison(node) and a non-empty ruleContextOptions(). Unlike the + // rule form, this yields a plain Python bool usable as a ternary condition. + std::string renderSettingCheck(const rls::ast::BinaryExpr& node) const; + + // Wraps an OptionFilter argument list as a standalone RuleBuilder rule: + // `True_(options=[OptionFilter()])`. A bare OptionFilter is not a Rule and cannot + // combine with another OptionFilter via & / |, so every setting comparison is wrapped in + // its own rule. This keeps each comparison a valid standalone rule that composes normally. + std::string WrapOptionFilter(const std::string& optionFilterArgs) const; + + // == Negation of pure option-filter rules ================================= + // The RuleBuilder has no rule negation in general, but a rule built entirely from + // `setting(...)` comparisons resolves at *build time* against `world.options`, so it can + // be negated soundly by pushing `not` down via De Morgan and flipping each leaf + // (eq <-> "ne"). This is what lets `not is_fire_loop_locked()` lower without the source + // having to reverse the logic by hand. Collection rules (has/can_use/...) are never pure, + // so `not` over them stays a diagnostic. + + // True iff `expr` resolves entirely from build-time settings: a setting comparison, a + // bool literal, `and`/`or`/`not` of such, or a call to a define whose body is such. + // Memoized over defines with a cycle guard. Anything touching a collection/host rule is + // impure (returns false). + bool IsPureOptionFilterRule(const rls::ast::Expr* expr) const; + bool IsPureOptionFilterRule(const rls::ast::ExprPtr& expr) const; + mutable std::map pureOptionFilterCache; + mutable std::set pureOptionFilterInProgress; + + // Renders the negation of a pure option-filter rule (precondition: + // IsPureOptionFilterRule(expr)). De Morgan dual: `and`->`|` of negations, `or`->`&` of + // negations, a setting leaf flips eq<->"ne", `not x` returns x's positive rendering, and + // a define call inlines its negated body. + std::string GenerateNegatedOptionFilterRule(const rls::ast::ExprPtr& expr) const; + // Precedence of the form GenerateNegatedOptionFilterRule emits (the De Morgan dual swaps + // and/or), so parenthesization of a negated rule stays in sync with its rendering. + int NegatedPrecedence(const rls::ast::ExprPtr& expr) const; + + // == Build-time evaluation of settings ==================================== + // A pure option-filter expression resolves entirely from `world.options`, which is frozen + // at generation. So in a *build-time boolean* position (a ternary condition, where a Rule + // cannot go because `bool(rule)` raises) it lowers to plain Python over + // `OptionFilter(...).check()` calls, rather than the OptionFilter-attached rule + // form used when a setting comparison combines *with* rules via & / |. + + // True iff `cond` can be evaluated as a build-time bool here: it is a pure option-filter + // expression and the world exposes an options accessor (ruleContextOptions() non-empty). + bool isBuildTimeSettingCondition(const rls::ast::ExprPtr& cond) const; + + // Render a pure option-filter expression as a build-time Python bool (precondition: + // isBuildTimeSettingCondition(expr)): setting leaves become OptionFilter(...).check(...), + // and `and`/`or`/`not`/pure-define nodes become the matching Python boolean operators. + std::string GenerateBuildTimeSettingCondition(const rls::ast::ExprPtr& expr) const; + // As GenerateChildExpression, but for a negated child: wraps using NegatedPrecedence. + std::string GenerateNegatedChild(const rls::ast::ExprPtr& expr, int parentPrec, bool isRightChild = false) const; +}; + +} // namespace rls::transpilers::ap diff --git a/transpilers/ap/include/section_walk.h b/transpilers/ap/include/section_walk.h new file mode 100644 index 0000000..62ef751 --- /dev/null +++ b/transpilers/ap/include/section_walk.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include + +#include "ast.h" + +namespace rls::transpilers::ap { + +using EntryWriter = std::function; + +// Invoke `writer` for every entry in every section of the given kind. +inline void WriteEntries( + const std::vector& sections, + rls::ast::SectionKind sectionKind, + const EntryWriter& writer) +{ + for (const auto& section : sections) { + if (section.kind == sectionKind) { + for (const auto& entry : section.entries) { + writer(entry); + } + } + } +} + +// A region's human-readable display name: the `name: "..."` data entry. Region bodies +// carry arbitrary key/value data, so the key is looked up rather than being a fixed field; +// a missing or non-string `name` yields "" (sema/the target's Validate() reports it). +inline std::string RegionDisplayName(const rls::ast::RegionDecl& region) { + const auto* entry = region.body.findData("name"); + if (entry == nullptr) { + return ""; + } + const auto* literal = std::get_if(&entry->value->node); + return literal != nullptr ? literal->value : ""; +} + +// Collect the names of every entry in every section of the given kind. +inline void InsertToSet( + const std::vector& sections, + rls::ast::SectionKind sectionKind, + std::set& emittedValues) +{ + for (const auto& section : sections) { + if (section.kind == sectionKind) { + for (const auto& entry : section.entries) { + emittedValues.insert(entry.name.text); + } + } + } +} + +} // namespace rls::transpilers::ap diff --git a/transpilers/ap/src/ap.cpp b/transpilers/ap/src/ap.cpp deleted file mode 100644 index 826b152..0000000 --- a/transpilers/ap/src/ap.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "ap.h" - -namespace rls::transpilers::ap { - -void Transpile(const rls::ast::Project& project, rls::OutputWriter& out) { - out.open("ap.py") << "# Generated by RLS ap transpiler\n"; -} - -} // namespace rls::transpilers::ap diff --git a/transpilers/ap/src/ap_transpiler.cpp b/transpilers/ap/src/ap_transpiler.cpp new file mode 100644 index 0000000..dcab9a1 --- /dev/null +++ b/transpilers/ap/src/ap_transpiler.cpp @@ -0,0 +1,55 @@ +#include "ap_transpiler.h" + +namespace rls::transpilers::ap { + +ApTranspiler::ApTranspiler(const rls::ast::Project& project) + : project(project) {} + +void ApTranspiler::GenerateEnumsSource(rls::OutputWriter& out) const { + writeEnums(out); +} + +void ApTranspiler::SetCurrentLocation(std::optional location) const { + currentLocationName = std::move(location); +} + +const std::vector& ApTranspiler::Diagnostics() const { + return diagnostics; +} + +void ApTranspiler::Diagnose(const rls::ast::Span& span, std::string message) const { + diagnostics.push_back({rls::ast::DiagnosticLevel::Error, std::move(message), span}); +} + +// == Default hook implementations ============================================= +// Generic AP behavior; SoH (and other games) override as needed. + +std::string ApTranspiler::ruleContextParam() const { + return ""; +} + +std::string ApTranspiler::ruleContextOptions() const { + // No generic accessor: how a rule lambda reaches world.options is world-specific (it depends + // on the shape of the rule-context receiver). Empty disables the build-time-setting lowering; + // a world overrides this to enable it. + return ""; +} + +std::string ApTranspiler::renderEnumValue(std::string_view, const std::string& value) const { + return value; +} + +std::optional ApTranspiler::renderHostCall(const rls::ast::CallExpr&, size_t, + const rls::ast::Expr*) const { + return std::nullopt; +} + +std::optional ApTranspiler::renderBinarySpecialCase(const rls::ast::BinaryExpr&) const { + return std::nullopt; +} + +bool ApTranspiler::isHostProvidedDefine(const std::string&) const { + return false; +} + +} // namespace rls::transpilers::ap diff --git a/transpilers/ap/src/classify_expression.cpp b/transpilers/ap/src/classify_expression.cpp new file mode 100644 index 0000000..3cb56e6 --- /dev/null +++ b/transpilers/ap/src/classify_expression.cpp @@ -0,0 +1,262 @@ +// Value classification: decide whether an RLS expression lowers to a runtime Rule +// object, a build-time Python value, or a runtime non-rule value in the Archipelago +// RuleBuilder target. This is pure analysis over the resolved AST -- it produces no +// output -- and is the keystone the function-generation bridging (and/or/not/ternary) +// builds on. See docs/AP-Function-Generation.md for the model and bridging tables. +// +// The distinction is about *when* a value is known. The rule lambda runs once to build +// a Rule tree; a build-time value is frozen then, a Rule re-evaluates at solve time, and +// a runtime non-rule value (e.g. bottle_count()) is collection-state dependent yet not a +// Rule -- so it can only be represented via a dedicated host rule. +#include "ap_transpiler.h" + +namespace rls::transpilers::ap { + +// Worse-of, where Runtime dominates Rule dominates BuildTime. Used to combine operand +// classes for operators that fold their operands (and/or, comparisons, calls): a Runtime +// operand makes the whole thing Runtime; otherwise a Rule operand makes it a Rule. +ApTranspiler::ValueClass ApTranspiler::JoinClass(ValueClass a, ValueClass b) { + if (a == ValueClass::Runtime || b == ValueClass::Runtime) { + return ValueClass::Runtime; + } + if (a == ValueClass::Rule || b == ValueClass::Rule) { + return ValueClass::Rule; + } + return ValueClass::BuildTime; +} + +ApTranspiler::ValueClass ApTranspiler::ClassifyExpression(const rls::ast::ExprPtr& expr) const { + return ClassifyExpression(expr.get()); +} + +ApTranspiler::ValueClass ApTranspiler::ClassifyExpression(const rls::ast::Expr* expr) const { + if (!expr) { + return ValueClass::BuildTime; + } + const auto& node = expr->node; + + // true/false/always/never lower to the rule literals True_()/False_(). + if (std::holds_alternative(node)) { + return ValueClass::Rule; + } + // Integer literals are build-time. + if (std::holds_alternative(node)) { + return ValueClass::BuildTime; + } + // Identifiers are build-time: an enum value is a Python constant, a parameter carries + // a build-time value (a deferred rule is passed as a Condition, not a bare Bool), and + // a FunctionRef is a callable object. + if (std::holds_alternative(node)) { + return ValueClass::BuildTime; + } + // Invoking a Condition yields a Rule. + if (std::holds_alternative(node)) { + return ValueClass::Rule; + } + // `here` resolves to a region name -- a build-time enum constant, like any other + // Region-typed identifier. + if (std::holds_alternative(node)) { + return ValueClass::BuildTime; + } + + // `not` preserves class: `not setting(...)` is still an OptionFilter rule, while + // `not ` stays a build-time bool. + if (auto* unary = std::get_if(&node)) { + return ClassifyExpression(unary->operand); + } + + // A ternary is a Rule if either branch is a Rule (the build-time condition selects + // between them). If both branches are build-time but the condition is not, the result + // is a runtime value (e.g. wallet_capacity: has(X) ? 999 : ...). + if (auto* ternary = std::get_if(&node)) { + ValueClass thenClass = ClassifyExpression(ternary->thenBranch); + ValueClass elseClass = ClassifyExpression(ternary->elseBranch); + if (thenClass == ValueClass::Rule || elseClass == ValueClass::Rule) { + return ValueClass::Rule; + } + // A pure setting condition is evaluated at build time (OptionFilter.check()), so a + // ternary with build-time branches over one stays build-time -- not a runtime value. + ValueClass condClass = ClassifyExpression(ternary->condition); + bool condBuildTime = condClass == ValueClass::BuildTime || isBuildTimeSettingCondition(ternary->condition); + return condBuildTime ? ValueClass::BuildTime : ValueClass::Runtime; + } + + // A match mirrors the ternary: a Rule if any arm body is a Rule; build-time only if + // the discriminant and every body are build-time; otherwise a runtime value. + if (auto* match = std::get_if(&node)) { + ValueClass combined = ClassifyExpression(match->discriminant); + bool anyRule = false; + for (const auto& arm : match->arms) { + ValueClass bodyClass = ClassifyExpression(arm.body); + anyRule = anyRule || bodyClass == ValueClass::Rule; + combined = JoinClass(combined, bodyClass); + } + if (anyRule) { + return ValueClass::Rule; + } + return combined == ValueClass::BuildTime ? ValueClass::BuildTime : ValueClass::Runtime; + } + + if (auto* binary = std::get_if(&node)) { + // setting(KEY) == / != VALUE lowers to an OptionFilter rule. + if (IsSettingComparison(*binary)) { + return ValueClass::Rule; + } + // A game-specific rewrite (e.g. wallet capacity, triforce hunt) collapses the + // comparison to an atomic rule call. + if (renderBinarySpecialCase(*binary)) { + return ValueClass::Rule; + } + // Conjunction/disjunction and comparisons/arithmetic all fold their operands: a + // Runtime operand wins, then a Rule operand. (For `and`/`or` this yields the Rule + // vs build-time mix; for comparisons over a runtime quantity it yields Runtime -- + // e.g. bottle_count() >= 1.) + return JoinClass(ClassifyExpression(binary->left), ClassifyExpression(binary->right)); + } + + if (auto* call = std::get_if(&node)) { + return ClassifyCall(*call); + } + + return ValueClass::BuildTime; +} + +ApTranspiler::ValueClass ApTranspiler::ClassifyCall(const rls::ast::CallExpr& call) const { + // setting(KEY) used as a truthiness guard lowers to an OptionFilter rule. + if (call.callee.text == "setting") { + return ValueClass::Rule; + } + // A call into a user define takes the define's own class. A Rule define is a Rule + // regardless of its arguments; a value-define is build-time unless an argument is + // itself runtime (or a rule spliced into a value parameter), which it then is. + if (auto it = project.DefineDecls.find(call.callee.text); it != project.DefineDecls.end()) { + // A host-provided define (has_bottle, ...) is lowered as an opaque host call, not + // by inlining its RLS body, so its class is what the host rule returns -- exactly + // like an extern below. Classifying it by its body instead would read has_bottle's + // `bottle_count() >= 1` as a Runtime value and reject any rule combination. + if (isHostProvidedDefine(call.callee.text)) { + return project.getType(it->second->body.get()) == rls::ast::Type::Bool + ? ValueClass::Rule + : ValueClass::Runtime; + } + ValueClass defineClass = DefineClass(it->second); + if (defineClass == ValueClass::Rule) { + return ValueClass::Rule; + } + ValueClass combined = defineClass; + if (auto* args = project.getResolvedCallArgs(&call)) { + for (const rls::ast::Expr* arg : *args) { + combined = JoinClass(combined, ClassifyExpression(arg)); + } + } + return combined; + } + // A host/extern function returning Bool is a runtime rule (has, can_use, trick, + // is_child, ...). Any other host return -- notably Int (bottle_count, + // check_price, the triforce counts) -- is a runtime non-rule value: it depends on + // collection state but is not itself a Rule. + if (auto it = project.ExternDefineDecls.find(call.callee.text); it != project.ExternDefineDecls.end()) { + const bool returnsBool = it->second->returnType && it->second->returnType->name.text == "Bool"; + return returnsBool ? ValueClass::Rule : ValueClass::Runtime; + } + // Unknown callee (blocked earlier in sema): assume a host rule, conservatively. + return ValueClass::Rule; +} + +bool ApTranspiler::ExpressionIsRule(const rls::ast::ExprPtr& expr) const { + return ClassifyExpression(expr) == ValueClass::Rule; +} + +ApTranspiler::ValueClass ApTranspiler::DefineClass(const rls::ast::DefineDecl* decl) const { + if (auto it = defineClassCache.find(decl); it != defineClassCache.end()) { + return it->second; + } + // A define that (transitively) calls itself is treated as a Rule; logic helpers do + // not actually recurse, so this only guards against pathological input. + if (!defineClassInProgress.insert(decl).second) { + return ValueClass::Rule; + } + ValueClass result = ClassifyExpression(decl->body); + defineClassInProgress.erase(decl); + defineClassCache[decl] = result; + return result; +} + +bool ApTranspiler::IsPureOptionFilterRule(const rls::ast::ExprPtr& expr) const { + return IsPureOptionFilterRule(expr.get()); +} + +bool ApTranspiler::IsPureOptionFilterRule(const rls::ast::Expr* expr) const { + if (!expr) { + return false; + } + const auto& node = expr->node; + + // true/false/always/never -> True_()/False_(), trivially negatable. + if (std::holds_alternative(node)) { + return true; + } + // not is still a pure option-filter rule (negation of one stays one). + if (auto* unary = std::get_if(&node)) { + return IsPureOptionFilterRule(unary->operand); + } + if (auto* binary = std::get_if(&node)) { + // A setting comparison is the pure leaf. + if (IsSettingComparison(*binary)) { + return true; + } + // and/or of pure option-filter rules stays pure. Other binary ops (comparisons, + // arithmetic) are not option-filter rules. + if (binary->op == rls::ast::BinaryOp::And || binary->op == rls::ast::BinaryOp::Or) { + return IsPureOptionFilterRule(binary->left) && IsPureOptionFilterRule(binary->right); + } + return false; + } + if (auto* call = std::get_if(&node)) { + // Bare setting(K) truthiness guard. + if (call->callee.text == "setting") { + return true; + } + // A call into a user define is pure iff its body is. Only no-argument defines qualify: + // inlining the negated body has no parameter substitution, so a parameterized body + // (even a pure-setting one) is treated as impure rather than miscompiled. Memoized with + // a cycle guard; a (pathological) cycle is conservatively impure. + if (auto it = project.DefineDecls.find(call->callee.text); it != project.DefineDecls.end()) { + const rls::ast::DefineDecl* decl = it->second; + if (!decl->params.empty()) { + return false; + } + if (auto c = pureOptionFilterCache.find(decl); c != pureOptionFilterCache.end()) { + return c->second; + } + if (!pureOptionFilterInProgress.insert(decl).second) { + return false; + } + bool result = IsPureOptionFilterRule(decl->body); + pureOptionFilterInProgress.erase(decl); + pureOptionFilterCache[decl] = result; + return result; + } + // Host/extern calls (has, can_use, trick, flag, ...) are collection rules -- impure. + return false; + } + // Identifiers, ints, ternaries, matches, invokes, here refs: not option-filter rules. + return false; +} + +ApTranspiler::AndOrLowering ApTranspiler::ClassifyAndOr(const rls::ast::BinaryExpr& node) const { + const ValueClass left = ClassifyExpression(node.left); + const ValueClass right = ClassifyExpression(node.right); + if (left == ValueClass::Runtime || right == ValueClass::Runtime) { + return AndOrLowering::Unrepresentable; + } + if (left == ValueClass::Rule && right == ValueClass::Rule) { + return AndOrLowering::RuleOp; + } + if (left == ValueClass::BuildTime && right == ValueClass::BuildTime) { + return AndOrLowering::PythonOp; + } + return AndOrLowering::MixedTernary; +} + +} // namespace rls::transpilers::ap diff --git a/transpilers/ap/src/generate_expression.cpp b/transpilers/ap/src/generate_expression.cpp new file mode 100644 index 0000000..3ad8d79 --- /dev/null +++ b/transpilers/ap/src/generate_expression.cpp @@ -0,0 +1,738 @@ +#include "ap_transpiler.h" + +#include + +namespace rls::transpilers::ap { + +std::string ApTranspiler::WrapOptionFilter(const std::string& optionFilterArgs) const { + return "True_(options=[OptionFilter(" + optionFilterArgs + ")])"; +} + +// True if this binary expression is a `setting(KEY) == VALUE` / `!= VALUE` comparison. +bool ApTranspiler::IsSettingComparison(const rls::ast::BinaryExpr& node) const { + // Only == and != comparisons + if (node.op != rls::ast::BinaryOp::Eq && node.op != rls::ast::BinaryOp::NotEq) { + return false; + } + + // Left side must be a setting() call and right must be an identifier (the enum value) + auto* leftCall = std::get_if(&node.left->node); + auto* rightId = std::get_if(&node.right->node); + if (!leftCall || !rightId || leftCall->callee.text != "setting") { + return false; + } + + // The setting key argument (RSK_*) must resolve to an identifier + auto resolvedPtr = project.getResolvedCallArgs(leftCall); + if (!resolvedPtr || resolvedPtr->empty()) { + return false; + } + return std::get_if(&resolvedPtr->front()->node) != nullptr; +} + +// Try to generate an OptionFilter expression for setting comparisons. +// Returns empty string if not a setting comparison; caller should use standard binary expression. +std::string ApTranspiler::TryGenerateOptionFilter(const rls::ast::BinaryExpr& node) const { + if (!IsSettingComparison(node)) { + return ""; + } + return renderSettingOptionFilter(node, /*negate=*/false); +} + +std::string ApTranspiler::optionFilterArgs(const rls::ast::BinaryExpr& node, bool negate) const { + auto* rightId = std::get_if(&node.right->node); + auto* leftCall = std::get_if(&node.left->node); + auto* settingKeyId = std::get_if(&project.getResolvedCallArgs(leftCall)->front()->node); + + std::ostringstream args; + args << settingKeyId->name.text << ", " << GenerateExpression(*rightId); + // `!=` / `is not` is the "ne" operator; negation flips eq <-> ne. + bool ne = (node.op == rls::ast::BinaryOp::NotEq); + if (negate) { + ne = !ne; + } + if (ne) { + args << ", \"ne\""; + } + return args.str(); +} + +std::string ApTranspiler::renderSettingOptionFilter(const rls::ast::BinaryExpr& node, bool negate) const { + return WrapOptionFilter(optionFilterArgs(node, negate)); +} + +std::string ApTranspiler::renderSettingCheck(const rls::ast::BinaryExpr& node) const { + return "OptionFilter(" + optionFilterArgs(node, /*negate=*/false) + ").check(" + ruleContextOptions() + ")"; +} + +// Render the negation of a pure option-filter rule. Precondition: IsPureOptionFilterRule(expr). +std::string ApTranspiler::GenerateNegatedOptionFilterRule(const rls::ast::ExprPtr& expr) const { + const auto& node = expr->node; + + // Negate the rule literal: not True_() -> False_(), not False_() -> True_(). + if (auto* lit = std::get_if(&node)) { + return lit->value ? "False_()" : "True_()"; + } + + // not (not x) == x: emit x's positive rendering (x is itself a pure option-filter rule). + if (auto* unary = std::get_if(&node)) { + return GenerateExpression(unary->operand); + } + + if (auto* binary = std::get_if(&node)) { + // Setting comparison leaf: flip eq <-> "ne". + if (IsSettingComparison(*binary)) { + return renderSettingOptionFilter(*binary, /*negate=*/true); + } + // De Morgan: not(a and b) = not a | not b; not(a or b) = not a & not b. + if (binary->op == rls::ast::BinaryOp::And) { + return GenerateNegatedChild(binary->left, 11) + " | " + + GenerateNegatedChild(binary->right, 11, /*isRightChild=*/true); + } + if (binary->op == rls::ast::BinaryOp::Or) { + return GenerateNegatedChild(binary->left, 9) + " & " + + GenerateNegatedChild(binary->right, 9, /*isRightChild=*/true); + } + } + + if (auto* call = std::get_if(&node)) { + // Bare setting(K) truthiness: its negation is "the setting is off". + if (call->callee.text == "setting") { + auto resolvedPtr = project.getResolvedCallArgs(call); + if (resolvedPtr && !resolvedPtr->empty()) { + if (auto* keyId = std::get_if(&resolvedPtr->front()->node)) { + return WrapOptionFilter(keyId->name.text + ", False"); + } + } + } + // A call into a pure (no-arg) define: inline the negation of its body. + if (auto it = project.DefineDecls.find(call->callee.text); it != project.DefineDecls.end()) { + return GenerateNegatedOptionFilterRule(it->second->body); + } + } + + // Precondition violated (IsPureOptionFilterRule should have gated this) -- fall back to the + // positive rendering rather than emit malformed output. + return GenerateExpression(expr); +} + +// Precedence of the form GenerateNegatedOptionFilterRule emits (the De Morgan dual swaps the +// and/or operator at each level), keeping negated-rule parenthesization in sync. +int ApTranspiler::NegatedPrecedence(const rls::ast::ExprPtr& expr) const { + const auto& node = expr->node; + if (auto* unary = std::get_if(&node)) { + return GetPythonPrecedence(unary->operand); // negation cancels to the positive form + } + if (auto* binary = std::get_if(&node)) { + if (!IsSettingComparison(*binary)) { + if (binary->op == rls::ast::BinaryOp::And) { + return 11; // negates to `|` + } + if (binary->op == rls::ast::BinaryOp::Or) { + return 9; // negates to `&` + } + } + } + if (auto* call = std::get_if(&node)) { + if (call->callee.text != "setting") { + if (auto it = project.DefineDecls.find(call->callee.text); it != project.DefineDecls.end()) { + return NegatedPrecedence(it->second->body); + } + } + } + // Setting comparison/bare setting and bool literals lower to an atomic call (binds tightly). + return 0; +} + +std::string ApTranspiler::GenerateNegatedChild( + const rls::ast::ExprPtr& expr, int parentPrec, bool isRightChild) const { + std::string result = GenerateNegatedOptionFilterRule(expr); + int childPrec = NegatedPrecedence(expr); + if (childPrec > parentPrec || (isRightChild && childPrec == parentPrec)) { + return "(" + result + ")"; + } + return result; +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::BoolLiteral& node) const { + return node.value ? "True_()" : "False_()"; +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::IntLiteral& node) const { + return std::to_string(node.value); +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::StringLiteral& node) const { + return "\"" + node.value + "\""; +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::ListExpr& node) const { + // Lists only ever appear in region data (e.g. `areas: [RA_X, RA_Y]`), which this + // transpiler reads directly rather than through expression generation -- so a list + // reaching here is a list in a rule expression, which RuleBuilder has no form for. + // ListExpr carries no span of its own; the first element locates it well enough. + Diagnose(node.elements.empty() ? rls::ast::Span{} : node.elements.front()->span, + "a list is not representable in an Archipelago rule expression"); + return ""; +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::MemberExpr& node) const { + // `EnumName.ValueName`: the enum is named explicitly, so no type lookup is needed. + return renderEnumValue(node.object.text, node.member.text); +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::Identifier& node) const { + if (node.kind == rls::ast::IdentifierKind::EnumValue) { + auto enumName = project.getEnumType(&node); + if (!enumName.has_value()) { + return node.name.text; + } + return renderEnumValue(*enumName, node.name.text); + } else if (node.kind == rls::ast::IdentifierKind::Parameter) { + return node.name.text; + } else if (node.kind == rls::ast::IdentifierKind::FunctionRef) { + // Bare reference to a function used as a callable value: emit the name. + return node.name.text; + } else { + // Unresolved identifiers should have been blocked earlier in sema; emit empty as a defensive fallback. + return ""; + } +} + +// Returns the RuleBuilder operator precedence for an expression node. +// Precedence adjusted for RuleBuilder bitwise operators (&, |, ~) as used in Archipelago: +// - Arithmetic (*, /): 6 +// - Arithmetic (+, -): 7 +// - Bitwise AND (&): 9 +// - Bitwise OR (|): 11 +// - Comparisons (==, !=, <, etc.): 12 +// - Ternary: 16 +// Unary bitwise NOT (~) and function calls have precedence 3 (very tight). +int ApTranspiler::GetPythonPrecedence(const rls::ast::ExprPtr& expr) const { + if (auto* bin = std::get_if(&expr->node)) { + // Setting comparisons are emitted as an atomic OptionFilter rule call, not a + // Python comparison, so they bind as tightly as a call (no parentheses needed). + if (IsSettingComparison(*bin)) { + return 0; + } + // A game-specific rewrite (e.g. wallet capacity, triforce hunt) collapses the + // comparison to an atomic call, so it also binds as tightly as a call. + if (renderBinarySpecialCase(*bin)) { + return 0; + } + switch (bin->op) { + case rls::ast::BinaryOp::Mul: + case rls::ast::BinaryOp::Div: + return 6; + case rls::ast::BinaryOp::Add: + case rls::ast::BinaryOp::Sub: + return 7; + case rls::ast::BinaryOp::And: + switch (ClassifyAndOr(*bin)) { + case AndOrLowering::RuleOp: return 9; // Bitwise AND (&) + case AndOrLowering::PythonOp: return 13; // Python `and` + case AndOrLowering::MixedTernary: return 16; // emitted as a conditional + case AndOrLowering::Unrepresentable: return 9; // rule-op fallback + } + return 9; + case rls::ast::BinaryOp::Lt: + case rls::ast::BinaryOp::LtEq: + case rls::ast::BinaryOp::Gt: + case rls::ast::BinaryOp::GtEq: + case rls::ast::BinaryOp::Eq: + case rls::ast::BinaryOp::NotEq: + return 12; + case rls::ast::BinaryOp::Or: + switch (ClassifyAndOr(*bin)) { + case AndOrLowering::RuleOp: return 11; // Bitwise OR (|) + case AndOrLowering::PythonOp: return 14; // Python `or` + case AndOrLowering::MixedTernary: return 16; // emitted as a conditional + case AndOrLowering::Unrepresentable: return 11; // rule-op fallback + } + return 11; + default: return 0; + } + } + if (auto* tern = std::get_if(&expr->node)) { + // A rule-conditioned ternary is emitted as an or-expression `(C & a) | b` (precedence + // of `|`); a normal Python ternary binds loosest. + return isRuleConditionedRuleTernary(*tern) ? 11 : 16; + } + if (auto* unary = std::get_if(&expr->node); + unary && unary->op == rls::ast::UnaryOp::Not) { + // `not ` lowers to a Python `not` (precedence between comparison + // and `and`). + if (ClassifyExpression(unary->operand) == ValueClass::BuildTime) { + return 12; + } + // `not ` is emitted as its De Morgan dual, so its precedence + // is that of the negated form. + if (IsPureOptionFilterRule(unary->operand)) { + return NegatedPrecedence(unary->operand); + } + } + // A `not setting(...)` leaf and the diagnosed cases lower to an atomic call/operand, + // which binds tightly (0). + return 0; +} + +// Generates an expression, wrapping in parentheses when the child's Python +// precedence is looser than the parent's (or equal on the right side of +// a left-associative operator). +std::string ApTranspiler::GenerateChildExpression( + const rls::ast::ExprPtr& expr, int parentPrec, bool isRightChild) const +{ + auto result = GenerateExpression(expr); + int childPrec = GetPythonPrecedence(expr); + if (childPrec > parentPrec || (isRightChild && childPrec == parentPrec)) { + return "(" + result + ")"; + } + return result; +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::UnaryExpr& node) const { + switch (node.op) { + case rls::ast::UnaryOp::Not: { + // `not ` is an ordinary Python negation. + if (ClassifyExpression(node.operand) == ValueClass::BuildTime) { + return "not " + GenerateChildExpression(node.operand, 12); + } + // `not ` is representable: settings resolve at build time + // against world.options, so the negation is sound. Push `not` down via De Morgan and + // flip each setting leaf (eq <-> "ne"). This covers `not setting(...)` and negated + // membership such as `not is_fire_loop_locked()`. + if (IsPureOptionFilterRule(node.operand)) { + return GenerateNegatedOptionFilterRule(node.operand); + } + // `not ` / `not ` cannot be expressed: the RuleBuilder + // has no negation for a collection-state rule. Diagnose rather than silently drop the + // `not` and emit a rule with inverted meaning. + Diagnose(node.operand->span, + "cannot negate a rule: the Archipelago RuleBuilder has no rule negation; only " + "settings (setting(...) comparisons) can be negated"); + return GenerateExpression(node.operand); + } + default: + return ""; + } +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::BinaryExpr& node) const { + // Setting comparisons become atomic OptionFilter rules. + std::string optionFilter = TryGenerateOptionFilter(node); + if (!optionFilter.empty()) { + return optionFilter; + } + + // Game-specific binary rewrites (e.g. price <= wallet capacity, triforce hunt). + if (auto special = renderBinarySpecialCase(node)) { + return *special; + } + + switch (node.op) { + case rls::ast::BinaryOp::And: + switch (ClassifyAndOr(node)) { + case AndOrLowering::RuleOp: + return GenerateChildExpression(node.left, 9) + " & " + GenerateChildExpression(node.right, 9, true); + case AndOrLowering::PythonOp: + return GenerateChildExpression(node.left, 13) + " and " + GenerateChildExpression(node.right, 13, true); + case AndOrLowering::MixedTernary: { + // `V and R` short-circuits at build time: `R if V else False_()`. + const bool leftIsRule = ExpressionIsRule(node.left); + const auto& ruleExpr = leftIsRule ? node.left : node.right; + const auto& valueExpr = leftIsRule ? node.right : node.left; + return GenerateChildExpression(ruleExpr, 15) + " if " + + GenerateChildExpression(valueExpr, 15) + " else False_()"; + } + case AndOrLowering::Unrepresentable: + // A runtime non-rule operand (e.g. bottle_count() >= 1) cannot be combined here + // without a host rule. Diagnose; the rule-op form is a best-effort fallback. + Diagnose(node.left->span, + "cannot combine a runtime value (e.g. a count comparison like " + "bottle_count() >= 1) with a rule; it must be lowered to a host rule"); + return GenerateChildExpression(node.left, 9) + " & " + GenerateChildExpression(node.right, 9, true); + } + return ""; + case rls::ast::BinaryOp::Or: + switch (ClassifyAndOr(node)) { + case AndOrLowering::RuleOp: + return GenerateChildExpression(node.left, 11) + " | " + GenerateChildExpression(node.right, 11, true); + case AndOrLowering::PythonOp: + return GenerateChildExpression(node.left, 14) + " or " + GenerateChildExpression(node.right, 14, true); + case AndOrLowering::MixedTernary: { + // `V or R` short-circuits at build time: `True_() if V else R`. + const bool leftIsRule = ExpressionIsRule(node.left); + const auto& ruleExpr = leftIsRule ? node.left : node.right; + const auto& valueExpr = leftIsRule ? node.right : node.left; + return "True_() if " + GenerateChildExpression(valueExpr, 15) + " else " + + GenerateExpression(ruleExpr); + } + case AndOrLowering::Unrepresentable: + // See the `and` case. + Diagnose(node.left->span, + "cannot combine a runtime value (e.g. a count comparison like " + "bottle_count() >= 1) with a rule; it must be lowered to a host rule"); + return GenerateChildExpression(node.left, 11) + " | " + GenerateChildExpression(node.right, 11, true); + } + return ""; + case rls::ast::BinaryOp::Eq: + return GenerateChildExpression(node.left, 12) + " == " + GenerateChildExpression(node.right, 12, true); + case rls::ast::BinaryOp::NotEq: + return GenerateChildExpression(node.left, 12) + " != " + GenerateChildExpression(node.right, 12, true); + case rls::ast::BinaryOp::Lt: + return GenerateChildExpression(node.left, 12) + " < " + GenerateChildExpression(node.right, 12, true); + case rls::ast::BinaryOp::LtEq: + return GenerateChildExpression(node.left, 12) + " <= " + GenerateChildExpression(node.right, 12, true); + case rls::ast::BinaryOp::Gt: + return GenerateChildExpression(node.left, 12) + " > " + GenerateChildExpression(node.right, 12, true); + case rls::ast::BinaryOp::GtEq: + return GenerateChildExpression(node.left, 12) + " >= " + GenerateChildExpression(node.right, 12, true); + case rls::ast::BinaryOp::Add: + return GenerateChildExpression(node.left, 7) + " + " + GenerateChildExpression(node.right, 7, true); + case rls::ast::BinaryOp::Sub: + return GenerateChildExpression(node.left, 7) + " - " + GenerateChildExpression(node.right, 7, true); + case rls::ast::BinaryOp::Mul: + return GenerateChildExpression(node.left, 6) + " * " + GenerateChildExpression(node.right, 6, true); + case rls::ast::BinaryOp::Div: + return GenerateChildExpression(node.left, 6) + " / " + GenerateChildExpression(node.right, 6, true); + default: + return ""; + } +} + +bool ApTranspiler::isBuildTimeSettingCondition(const rls::ast::ExprPtr& cond) const { + // A pure option-filter expression is build-time only if we have somewhere to read the + // options from; without an accessor, .check() cannot be emitted. + return !ruleContextOptions().empty() && IsPureOptionFilterRule(cond); +} + +std::string ApTranspiler::GenerateBuildTimeSettingCondition(const rls::ast::ExprPtr& expr) const { + const auto& node = expr->node; + + // true/false/always/never -> plain Python bools. + if (auto* lit = std::get_if(&node)) { + return lit->value ? "True" : "False"; + } + // The only unary over a pure rule is `not`. Parenthesize to stay above `and`/`or`. + if (auto* unary = std::get_if(&node)) { + return "not (" + GenerateBuildTimeSettingCondition(unary->operand) + ")"; + } + if (auto* binary = std::get_if(&node)) { + if (IsSettingComparison(*binary)) { + return renderSettingCheck(*binary); + } + // and/or of pure settings -> Python and/or. Parenthesize compound operands (safe, and + // keeps mixed and/or nests unambiguous). + if (binary->op == rls::ast::BinaryOp::And) { + return "(" + GenerateBuildTimeSettingCondition(binary->left) + ") and (" + + GenerateBuildTimeSettingCondition(binary->right) + ")"; + } + if (binary->op == rls::ast::BinaryOp::Or) { + return "(" + GenerateBuildTimeSettingCondition(binary->left) + ") or (" + + GenerateBuildTimeSettingCondition(binary->right) + ")"; + } + } + if (auto* call = std::get_if(&node)) { + // Bare setting(K) truthiness guard: OptionFilter(K, True).check(...). + if (call->callee.text == "setting") { + auto resolvedPtr = project.getResolvedCallArgs(call); + if (resolvedPtr && !resolvedPtr->empty()) { + if (auto* keyId = std::get_if(&resolvedPtr->front()->node)) { + return "OptionFilter(" + keyId->name.text + ", True).check(" + ruleContextOptions() + ")"; + } + } + } + // A call into a pure (no-arg) define: inline its body's build-time form. + if (auto it = project.DefineDecls.find(call->callee.text); it != project.DefineDecls.end()) { + return GenerateBuildTimeSettingCondition(it->second->body); + } + } + + // Precondition violated (isBuildTimeSettingCondition should have gated this) -- fall back to + // the positive rendering rather than emit malformed output. + return GenerateExpression(expr); +} + +bool ApTranspiler::isRuleConditionedRuleTernary(const rls::ast::TernaryExpr& node) const { + // A pure setting condition is evaluated at build time via .check(), so it is not the + // rule-conditioned case; the ternary stays an ordinary Python conditional (precedence 16). + if (isBuildTimeSettingCondition(node.condition)) { + return false; + } + // A ternary lowers to the rule idiom only when its condition is a rule (not a build-time + // value, which stays an ordinary Python `if`) and both branches are rules (a value branch + // could not be `&`-combined with the rule condition). + if (ClassifyExpression(node.condition) == ValueClass::BuildTime) { + return false; + } + return ExpressionIsRule(node.thenBranch) && ExpressionIsRule(node.elseBranch); +} + +// Python ternary syntax is "a if test else b" +std::string ApTranspiler::GenerateExpression(const rls::ast::TernaryExpr& node) const { + // A rule-conditioned ternary cannot be a Python `if` (`bool(rule)` raises), and the + // RuleBuilder has no rule negation to express the complement of the condition. We lower + // `C ? a : b` to `(C & a) | b`: the then-branch stays gated by the condition, while the + // else-branch becomes unconditional. This is always representable (no negation needed) and + // monotonic -- gaining the condition never *removes* the else-branch's access, which is + // what access logic wants. It deliberately does NOT synthesize a complement rule (e.g. + // `is_adult()` for `is_child()`): the source never wrote one, and assuming the condition's + // negation is some specific other rule would bake in an invariant the game may not hold. + // A pure setting condition resolves at build time against world.options, so it can be a + // real Python condition via OptionFilter.check(). The ternary then lowers to an ordinary + // `a if else b` for ANY branch types -- int (e.g. small_keys count), enum, or rule. + // This is exact: unlike the (C & a) | b idiom below it does not ungate the else-branch. + if (isBuildTimeSettingCondition(node.condition)) { + return GenerateExpression(node.thenBranch) + " if " + + GenerateBuildTimeSettingCondition(node.condition) + " else " + + GenerateExpression(node.elseBranch); + } + if (isRuleConditionedRuleTernary(node)) { + return "(" + GenerateChildExpression(node.condition, 9) + " & " + + GenerateChildExpression(node.thenBranch, 9, true) + ") | " + + GenerateChildExpression(node.elseBranch, 11, true); + } + // Otherwise the condition becomes a Python `if`, so it must be a build-time value. A + // runtime non-rule value (or a rule paired with a value branch) cannot be one -- diagnose + // rather than emit code that raises at world-load. + if (ClassifyExpression(node.condition) != ValueClass::BuildTime) { + Diagnose(node.condition->span, + "ternary condition must be a build-time value; a rule cannot be used as a " + "Python condition (the RuleBuilder raises on bool(rule))"); + } + return GenerateExpression(node.thenBranch) + " if " + + GenerateChildExpression(node.condition, 15) + " else " + + GenerateExpression(node.elseBranch); +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::CallExpr& node) const { + if (project.getResolvedCallArgs(&node) == nullptr) { + // Unknown calls or calls with semantic errors are blocked earlier in sema; + // emit empty as a defensive fallback so generation does not invent call forms. + return ""; + } + + // A rule-conditioned ternary passed as a value argument is distributed over the call, turning + // it into a conditional rule (finding D). This runs before the setting/host/default dispatch + // so it also covers host-rewrite calls (e.g. small_keys(SCENE, rule ? 2 : 3)): each branch is + // re-rendered through the full dispatch (renderCall), so host rewrites still apply per branch. + if (auto distributed = tryDistributeTernaryArg(node)) { + return *distributed; + } + + return renderCall(node, std::string::npos, nullptr); +} + +std::string ApTranspiler::renderCall(const rls::ast::CallExpr& node, size_t overrideIdx, + const rls::ast::Expr* overrideExpr) const { + const auto& resolved = *project.getResolvedCallArgs(&node); + + // setting(KEY) is a truthiness check, emitted as an OptionFilter rule (AP-generic). Its sole + // argument is a Setting key, never a distributed ternary branch, so the override never applies. + if (node.callee.text == "setting") { + if (auto* id = std::get_if(&resolved[0]->node)) { + return WrapOptionFilter(id->name.text + std::string(", True")); + } + return ""; + } + + // Game-specific host-call rewrites (has, flag, trick, small_keys, ...). + if (auto hostCall = renderHostCall(node, overrideIdx, overrideExpr)) { + return *hostCall; + } + + // Default: a regular function call, optionally threading the rule-context + // receiver (e.g. SoH's `bundle`) as the implicit first argument. + return renderDefaultCall(node, overrideIdx, overrideExpr); +} + +std::string ApTranspiler::renderDefaultCall(const rls::ast::CallExpr& node, size_t overrideIdx, + const rls::ast::Expr* overrideExpr) const { + const auto& resolved = *project.getResolvedCallArgs(&node); + std::ostringstream oss; + oss << node.callee.text << "("; + const std::string receiver = ruleContextParam(); + bool needComma = false; + if (!receiver.empty()) { + oss << receiver; + needComma = true; + } + for (size_t i = 0; i < resolved.size(); ++i) { + if (needComma) { + oss << ", "; + } + needComma = true; + + const rls::ast::Expr* arg = (i == overrideIdx) ? overrideExpr : resolved[i]; + oss << GenerateCallArgument(arg, ResolveCallParamType(node, i)); + } + oss << ")"; + return oss.str(); +} + +std::optional ApTranspiler::tryDistributeTernaryArg(const rls::ast::CallExpr& node) const { + const auto* resolvedPtr = project.getResolvedCallArgs(&node); + if (resolvedPtr == nullptr) { + return std::nullopt; + } + // Only distribute when the call itself yields a Rule, so each branch call (f(A), f(B)) is a + // rule the conditional can hold. A value-returning callee (e.g. check_price/price_of) would + // otherwise wrap non-rule values in a conditional rule; leave it to the normal path, which + // diagnoses the rule-conditioned value ternary as unrepresentable. + if (ClassifyCall(node) != ValueClass::Rule) { + return std::nullopt; + } + const auto& resolved = *resolvedPtr; + for (size_t i = 0; i < resolved.size(); ++i) { + auto* tern = std::get_if(&resolved[i]->node); + if (tern == nullptr) { + continue; + } + // A build-time or pure-setting condition already lowers to an ordinary Python `if` + // ternary in the argument -- it needs no rule to pick the branch. + if (isBuildTimeSettingCondition(tern->condition) || + ClassifyExpression(tern->condition) != ValueClass::Rule) { + continue; + } + // Only value branches are distributed. A rule-branch ternary is representable directly + // (and never appears as a value argument, whose parameter is not a rule). + if (ExpressionIsRule(tern->thenBranch) || ExpressionIsRule(tern->elseBranch)) { + continue; + } + const std::string cond = GenerateExpression(tern->condition->node); + const std::string thenCall = renderCall(node, i, tern->thenBranch.get()); + const std::string elseCall = renderCall(node, i, tern->elseBranch.get()); + return renderConditionalRule(cond, thenCall, elseCall); + } + return std::nullopt; +} + +std::string ApTranspiler::renderConditionalRule(const std::string& cond, + const std::string& thenExpr, const std::string& elseExpr) const { + const std::string receiver = ruleContextParam(); + const std::string prefix = receiver.empty() ? "" : receiver + ", "; + return "rls_conditional(" + prefix + cond + ", " + thenExpr + ", " + elseExpr + ")"; +} + +std::optional ApTranspiler::ResolveCallParamType( + const rls::ast::CallExpr& node, size_t index) const { + if (auto externIt = project.ExternDefineDecls.find(node.callee.text); + externIt != project.ExternDefineDecls.end() && index < externIt->second->params.size()) { + return project.getType(&externIt->second->params[index]); + } + + if (auto defineIt = project.DefineDecls.find(node.callee.text); + defineIt != project.DefineDecls.end() && index < defineIt->second->params.size()) { + return project.getType(&defineIt->second->params[index]); + } + + return std::nullopt; +} + +std::string ApTranspiler::GenerateCallArgument( + const rls::ast::Expr* argExpr, std::optional paramType) const { + const bool paramIsCondition = paramType == rls::ast::Type::Condition; + const bool argIsCondition = project.getType(argExpr) == rls::ast::Type::Condition; + + // An argument already of Condition type is a callable value; pass it through unchanged. + if (paramIsCondition && argIsCondition) { + return GenerateExpression(argExpr->node); + } + + // A non-Condition expression bound to a Condition parameter is wrapped in a thunk so the + // RuleBuilder evaluates it lazily: `(lambda : )`. + if (paramIsCondition) { + return "(lambda " + ruleContextParam() + ": " + GenerateExpression(argExpr->node) + ")"; + } + + // Function parameters use Python's True/False, not the True_()/False_() rule literals. + if (auto* lit = std::get_if(&argExpr->node)) { + return lit->value ? "True" : "False"; + } + + return GenerateExpression(argExpr->node); +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::InvokeExpr& node) const { + // Invoke a callable-valued result. A Condition is a rule callback typed + // `Callable[[bundle], Rule]`, so it is invoked with the rule-context receiver + // (e.g. SoH's `bundle`): `(bundle)`. This mirrors the thunk form produced + // for Condition arguments, so the two agree on arity. + return GenerateExpression(node.callee) + "(" + ruleContextParam() + ")"; +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::HereRef& node) const { + // `here` lowers to a reference to the enclosing region, resolved by sema. It renders + // like any other value of the host's region enum (e.g. SoH's `Regions.`). The + // enum name is fixed here because `here` has no identifier node to look up. + return renderEnumValue("Region", node.resolvedRegion.text); +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::MatchExpr& node) const { + // Classify the arms. A rule body anywhere makes this a rule match (matched arms are + // |-combined, accumulating down `or`-fallthrough chains); otherwise the arms produce + // build-time values and the match returns the selected one. A runtime non-rule body + // (e.g. a bottle_count comparison) is unrepresentable. + bool anyRule = false; + bool anyRuntime = false; + for (const auto& arm : node.arms) { + switch (ClassifyExpression(arm.body)) { + case ValueClass::Rule: anyRule = true; break; + case ValueClass::Runtime: anyRuntime = true; break; + case ValueClass::BuildTime: break; + } + } + + // Each arm renders to a flat `condition, body, fallthrough` triple. The condition is a + // zero-arg predicate that closes over the (build-time) discriminant; the body is a + // zero-arg thunk so the helper can pick/combine arms lazily. + const std::string disc = GenerateExpression(node.discriminant); + std::ostringstream arms; + for (size_t i = 0; i < node.arms.size(); i++) { + const auto& arm = node.arms[i]; + if (i > 0) arms << ", "; + + if (arm.isDefault) { + arms << "(lambda: True)"; + } else { + arms << "(lambda " << disc << "=" << disc << ": "; + for (size_t j = 0; j < arm.patterns.size(); j++) { + if (j > 0) arms << " or "; + arms << disc << " == " << GenerateExpression(arm.patterns[j]); + } + arms << ")"; + } + arms << ", (lambda: " << GenerateExpression(arm.body) << ")"; + arms << ", " << (arm.fallthrough ? "True" : "False"); + } + + if (anyRule) { + return "rls_match_rule(" + arms.str() + ")"; + } + if (anyRuntime) { + // A value match whose result depends on collection state cannot be represented; + // diagnose and fall back to a value match so generation still produces something. + Diagnose(node.discriminant->span, + "match arms produce a runtime value that is neither a rule nor build-time; " + "it cannot be represented (lower it to a host rule)"); + return "rls_match_value(0, " + arms.str() + ")"; + } + // A build-time value match: default to the additive identity for the result type + // (0 for ints, False for bools) when no arm matches. + const std::string defaultValue = + project.getType(node.arms.empty() ? nullptr : node.arms.front().body.get()) == rls::ast::Type::Bool + ? "False" : "0"; + return "rls_match_value(" + defaultValue + ", " + arms.str() + ")"; +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::Expr::Variant& node) const { + return std::visit([&](const auto& node) { + return ApTranspiler::GenerateExpression(node); + }, node); +} + +std::string ApTranspiler::GenerateExpression(const rls::ast::ExprPtr& expr) const { + return GenerateExpression(expr->node); +} + +} // namespace rls::transpilers::ap diff --git a/transpilers/ap/src/generate_functions.cpp b/transpilers/ap/src/generate_functions.cpp new file mode 100644 index 0000000..5d49969 --- /dev/null +++ b/transpilers/ap/src/generate_functions.cpp @@ -0,0 +1,59 @@ +#include "ap_transpiler.h" + +#include + +namespace rls::transpilers::ap { + +void ApTranspiler::GenerateFunctionDefinitionsSource(rls::OutputWriter& out) const { + auto& source = out.open("functions.gen.py"); + source << functionsPreamble(); + + // Resolve an AST node's RLS type to its Python type name, deferring the + // concrete mapping to the game hook. + auto typeName = [&](const auto* node) -> std::string { + auto type = project.getType(node); + if (!type.has_value()) { + return "missing_type"; + } + // Which enum an enum-typed node belongs to lives beside the type, so pass it along. + return pythonTypeName(type.value(), project.getEnumType(node)); + }; + + for (const auto& [name, decl] : project.DefineDecls) { + // Defines the host world supplies natively (e.g. has_bottle) or folds away at the + // call site (wallet_capacity) are not emitted -- see isHostProvidedDefine. + if (isHostProvidedDefine(name)) { + continue; + } + source << "\n"; + + std::ostringstream sig; + sig << "def " << decl->name << "("; + const std::string receiver = ruleContextParam(); + bool needComma = false; + if (!receiver.empty()) { + sig << receiver; + needComma = true; + } + for (size_t i = 0; i < decl->params.size(); i++) { + const auto& param = decl->params[i]; + if (needComma) { + sig << ", "; + } + needComma = true; + sig << param.name << ": " << typeName(¶m); + if (param.defaultValue != nullptr) { + // A default binds to the parameter exactly like a call argument: a Condition + // default is thunked, a value default uses Python True/False, not True_()/ + // False_(). Reuse the call-argument path so the two stay consistent. + sig << " = " + GenerateCallArgument(param.defaultValue.get(), project.getType(¶m)); + } + } + sig << ") -> " << typeName(decl->body.get()); + + source << sig.str() << ":\n"; + source << " return " << GenerateExpression(decl->body) << "\n"; + } +} + +} // namespace rls::transpilers::ap diff --git a/transpilers/ap/src/generate_regions.cpp b/transpilers/ap/src/generate_regions.cpp new file mode 100644 index 0000000..7e5594d --- /dev/null +++ b/transpilers/ap/src/generate_regions.cpp @@ -0,0 +1,72 @@ +#include "ap_transpiler.h" +#include "section_walk.h" + +#include +#include + +namespace rls::transpilers::ap { + +void ApTranspiler::GenerateRegionsSource(rls::OutputWriter& out) const { + auto& source = out.open("regions.gen.py"); + source << regionsPreamble(); + + // Helper-call names are the same for every region; resolve them once. + const std::string eventsFn = addEventsFn(); + const std::string locationsFn = addLocationsFn(); + const std::string exitsFn = connectRegionsFn(); + + for (const auto& [regionName, region] : project.RegionDecls) { + const auto extendRegionIt = project.ExtendRegionDecls.find(region->key.text); + + std::vector extendRegionDecls; + if (extendRegionIt != project.ExtendRegionDecls.end()) { + extendRegionDecls = extendRegionIt->second; + } + + const std::string creationArgs = regionCreationArgs(region->key.text); + + // Build one `