opy-rs 0.1.51

Standalone OverPy-compatible .opy implementation with parsing, tooling, and bounded Workshop compilation.
Documentation
//! Oracle-backed catalog/member/enum lowering evidence.

use std::path::{Path, PathBuf};

use crate::Compiler;
use workshop_rs::catalog::{Catalog, Locale};
use workshop_rs::roundtrip::equivalent;

fn fixture_dir(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/corpus/synthetic")
        .join(name)
}

fn compile_fixture(name: &str) -> crate::CompilationArtifact {
    let dir = fixture_dir(name);
    let source = std::fs::read_to_string(dir.join("source.opy")).expect("source must be readable");
    let hir = crate::compile(&source, "source.opy", &dir).expect("fixture must resolve");
    Compiler::new()
        .expect("released workshop contract must load")
        .compile_hir(&hir)
        .expect("fixture must lower to canonical WIR")
}

fn compile_real_world(name: &str, source_name: &str) -> crate::CompilationArtifact {
    let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/corpus/real-world")
        .join(name);
    let source = std::fs::read_to_string(dir.join(source_name)).expect("source must be readable");
    let hir = crate::compile(&source, source_name, &dir).expect("real-world source must resolve");
    Compiler::new()
        .expect("released workshop contract must load")
        .compile_hir(&hir)
        .expect("real-world source must lower to canonical WIR")
}

fn oracle_workshop(name: &str) -> String {
    let value: serde_json::Value = serde_json::from_str(
        &std::fs::read_to_string(fixture_dir(name).join("oracle.json"))
            .expect("oracle must be readable"),
    )
    .expect("oracle must parse");
    value["compile"]["workshop"]
        .as_str()
        .expect("oracle must contain Workshop output")
        .to_string()
}

fn assert_matches_oracle(name: &str) {
    let artifact = compile_fixture(name);
    let catalog = Catalog::builtin().expect("catalog must load");
    let locale = Locale::new("en-US");
    let native =
        workshop_rs::parser::parse(&artifact.emitted, &catalog, &locale).unwrap_or_else(|error| {
            panic!("native output must reparse: {error}\n{}", artifact.emitted)
        });
    let oracle = workshop_rs::parser::parse(&oracle_workshop(name), &catalog, &locale)
        .expect("oracle output must reparse");
    assert!(
        equivalent(&native, &oracle),
        "lowering diverged for {name}\n--- native ---\n{}\n--- oracle ---\n{}",
        artifact.emitted,
        oracle_workshop(name)
    );
}

#[test]
fn catalog_backed_receiver_calls_match_the_pinned_oracle() {
    assert_matches_oracle("receiver-calls");
}

#[test]
fn catalog_backed_contextual_chase_calls_match_the_pinned_oracle() {
    let artifact = compile_fixture("chase-condition-agentlab");
    assert!(
        artifact
            .emitted
            .contains("Chase Global Variable Over Time(Global.Round_Attack_Time, 0, 30, None);")
    );
}

#[test]
fn chase_keyword_forms_match_the_pinned_oracle() {
    assert_matches_oracle("chase-keywords");
}

#[test]
fn catalog_enum_members_lower_and_validate() {
    let artifact = compile_fixture("chase-enums");
    for expected in [
        "Set Global Variable(time_reeval, None);",
        "Set Global Variable(time_reeval, Destination and Duration);",
        "Set Global Variable(rate_reeval, None);",
        "Set Global Variable(rate_reeval, Destination and Rate);",
    ] {
        assert!(artifact.emitted.contains(expected), "missing {expected}");
    }
}

#[test]
fn pinned_builtin_domains_and_constants_lower_through_the_catalog() {
    let source = r#"globalvar value

rule "domains":
    @Event eachPlayer
    @Condition eventPlayer.isHoldingButton(Button.INTERACT)
    value = Team.ALL
    value = Hero.BRIGITTE
    value = Map.BLACK_FOREST_WINTER
    value = getCurrentGamemode() == Gamemode.ASSAULT
    value = Color.LIGHT_RED
    value = Vector.BACKWARD
    value = Math.FUCKTON_OF_SPACES
    value = Math.FUCKTON_OF_NEWLINES
    value = Math.SPHERE_HORIZONTAL_RADIUS_MULT
    value = Math.RING_EXPLOSION_RADIUS_MULT

rule "slot":
    @Event playerJoined
    @Team 1
    @Slot 11
    disableInspector()
"#;
    let hir = crate::compile(source, "builtin-domains.opy", Path::new("."))
        .expect("pinned domain and constant source must resolve");
    let artifact = Compiler::new()
        .expect("released Workshop contract must load")
        .compile_hir(&hir)
        .expect("pinned domain and constant source must lower");

    assert!(artifact.emitted.contains("Player Joined Match;"));
    assert!(artifact.emitted.contains("Slot 11;"));
    assert!(artifact.emitted.contains("Custom String("));
    assert!(artifact.emitted.contains("0.984724"));
    assert!(artifact.emitted.contains("0.48"));
}

#[test]
fn pinned_texture_members_lower_with_texture_tag_setup() {
    let source = "globalvar value\nrule \"textures\":\n    @Event global\n    value = Texture.MOUSE_CURSOR\n    value = Texture.ASSAULT\n";
    let hir = crate::compile(source, "textures.opy", Path::new(".")).unwrap();
    let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap();

    assert!(artifact.emitted.contains("127: __holygrail__"));
    assert!(
        artifact
            .emitted
            .contains("Create Dummy Bot(All Heroes, If-Then-Else(")
    );
    assert!(artifact.emitted.contains("Number Of Slots(Team(Team 1))"));
    assert!(artifact.emitted.contains("Number Of Slots(Team(Team 2))"));
    assert!(
        artifact
            .emitted
            .contains("OverPy <\u{AD}tx> / <\u{AD}fg> setup code")
    );
    assert!(
        artifact
            .emitted
            .contains("String Split(First Of(Last Created Entity),")
    );
    assert!(artifact.emitted.contains("String Replace("));
    assert!(artifact.emitted.contains("String Slice("));
    assert!(artifact.emitted.contains("Destroy All Dummy Bots;"));
    assert!(
        artifact
            .emitted
            .contains("Custom String(\"{0}txc0000000002dd21>\", Global.__holygrail__)")
    );
    assert!(
        artifact
            .emitted
            .contains("Custom String(\"{0}txc00000000020af8>\", Global.__holygrail__)")
    );
}

#[test]
fn texture_setup_and_formatted_named_entities_match_pinned_features() {
    let artifact = compile_fixture("texture-entity-compat");
    let oracle = oracle_workshop("texture-entity-compat");
    for marker in [
        "Create Dummy Bot(All Heroes,",
        "Start Forcing Dummy Bot Name(Last Created Entity,",
        "String Split(First Of(Last Created Entity),",
        "String Replace(",
        "String Slice(String Replace(",
        "126, True",
        "Destroy All Dummy Bots;",
        "Custom String(\"{0}txc0000000002dd21>\", Global.__holygrail__)",
        "Custom String(\"â–  {0}\", Match Time)",
    ] {
        assert!(
            oracle.contains(marker),
            "pinned oracle is missing required feature marker: {marker}"
        );
        assert!(
            artifact.emitted.contains(marker),
            "native output is missing pinned feature marker: {marker}\n{}",
            artifact.emitted
        );
    }
}

#[test]
fn aliased_member_lowers_to_the_canonical_catalog_identity() {
    let source = "globalvar value\nrule \"r\":\n    @Event eachPlayer\n    @Condition eventPlayer.getHero() == None\n    value = eventPlayer.getHero()\n";
    let hir = crate::compile(source, "source.opy", Path::new(".")).expect("frontend resolves");
    let artifact = Compiler::new()
        .expect("compiler loads")
        .compile_hir(&hir)
        .expect("the member alias must lower to Hero Of");
    assert!(artifact.emitted.contains("Hero Of(Event Player)"));
}

#[test]
fn append_receiver_uses_the_canonical_modify_operation() {
    let source = "globalvar values\nrule \"r\":\n    @Event global\n    values.append(1)\n";
    let hir = crate::compile(source, "source.opy", Path::new(".")).expect("frontend resolves");
    let artifact = Compiler::new()
        .expect("compiler loads")
        .compile_hir(&hir)
        .expect("append lowers to canonical WIR");
    assert!(
        artifact
            .emitted
            .contains("Modify Global Variable(values, Append To Array, 1);")
    );
}

#[test]
fn computed_numbers_and_cardinal_vectors_use_canonical_workshop_spellings() {
    let source = r#"globalvar value

rule "canonical values":
    @Event global
    value = vect(0, 1, 0)
    value = 1.5 / sqrt(2) + 0.75
"#;
    let hir = crate::compile(source, "canonical-values.opy", Path::new("."))
        .expect("source must resolve");
    let artifact = Compiler::new()
        .expect("released workshop contract must load")
        .compile_hir(&hir)
        .expect("source must lower");

    assert!(artifact.emitted.contains("Set Global Variable(value, Up);"));
    assert!(artifact.emitted.contains("1.810660171779821"));
    assert!(!artifact.emitted.contains("1.8106601717798212"));
}

#[test]
fn real_world_cake_exercises_catalog_lowering_end_to_end() {
    let first = compile_real_world("overpy-cake", "source.opy");
    let second = compile_real_world("overpy-cake", "source.opy");
    assert_eq!(
        first.emitted, second.emitted,
        "emission must be deterministic"
    );

    // compile_hir already runs canonical catalog-id
    // validation. The pinned cake Workshop text contains the existing
    // ambiguous `Visible To` spelling, so reparsing it would test a
    // workshop-rs parser limitation rather than this lowering contract.
    let program = super::canonical_program(&first);
    let mut calls = std::collections::BTreeSet::new();
    let mut value_calls = std::collections::BTreeSet::new();
    for rule in &program.rules {
        for action in &rule.actions {
            collect_action_calls(action, &mut calls, &mut value_calls);
        }
        for condition in &rule.conditions {
            collect_value_calls(&condition.value, &mut value_calls);
        }
    }
    for expected in ["createBeamEffect", "playEffect"] {
        assert!(
            calls.contains(expected),
            "real-world cake must lower {expected}"
        );
    }
    for expected in ["randomReal", "randomValueInArray"] {
        assert!(
            value_calls.contains(expected),
            "real-world cake must lower value {expected}"
        );
    }
}

fn collect_action_calls(
    action: &workshop_rs::Action,
    calls: &mut std::collections::BTreeSet<String>,
    value_calls: &mut std::collections::BTreeSet<String>,
) {
    match action {
        workshop_rs::Action::Call { name, args } => {
            calls.insert(name.clone());
            for value in args {
                collect_value_calls(value, value_calls);
            }
        }
        workshop_rs::Action::SetGlobalVariable { value, .. }
        | workshop_rs::Action::ModifyGlobalVariable { value, .. }
        | workshop_rs::Action::AssignMember { value, .. }
        | workshop_rs::Action::If { condition: value }
        | workshop_rs::Action::ElseIf { condition: value }
        | workshop_rs::Action::While { condition: value } => {
            collect_value_calls(value, value_calls)
        }
        workshop_rs::Action::SetPlayerVariable { player, value, .. }
        | workshop_rs::Action::ModifyPlayerVariable { player, value, .. } => {
            collect_value_calls(player, value_calls);
            collect_value_calls(value, value_calls);
        }
        workshop_rs::Action::ForGlobalVariable {
            start, stop, step, ..
        } => {
            for value in [start, stop, step] {
                collect_value_calls(value, value_calls);
            }
        }
        workshop_rs::Action::ForPlayerVariable {
            player,
            start,
            stop,
            step,
            ..
        } => {
            for value in [player, start, stop, step] {
                collect_value_calls(value, value_calls);
            }
        }
        workshop_rs::Action::Disabled { action } => {
            collect_action_calls(action, calls, value_calls)
        }
        workshop_rs::Action::CallSubroutine { .. }
        | workshop_rs::Action::Else
        | workshop_rs::Action::End => {}
    }
}

fn collect_value_calls(value: &workshop_rs::Value, calls: &mut std::collections::BTreeSet<String>) {
    match value {
        workshop_rs::Value::Call { name, args } => {
            calls.insert(name.clone());
            for arg in args {
                collect_value_calls(arg, calls);
            }
        }
        workshop_rs::Value::Array(values) => {
            for value in values {
                collect_value_calls(value, calls);
            }
        }
        workshop_rs::Value::Vector { x, y, z } => {
            for value in [x, y, z] {
                collect_value_calls(value, calls);
            }
        }
        workshop_rs::Value::PlayerVariable { player, .. } => collect_value_calls(player, calls),
        _ => {}
    }
}