opy-rs 0.1.43

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 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),
        _ => {}
    }
}