opy-rs 0.1.29

Standalone OverPy-compatible .opy implementation with parsing, tooling, and bounded Workshop compilation.
Documentation
//! Oracle-backed control-flow lowering and behavior evidence.

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

use crate::Compiler;
use workshop_rs::catalog::{Catalog, Locale};
use workshop_rs::roundtrip::equivalent;
use workshop_rs::wir::{self, Action, Value};

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

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

fn assert_native_wir_equivalent(name: &str) {
    let dir = fixture_dir(name);
    let source = std::fs::read_to_string(dir.join("source.opy")).unwrap();
    let hir = crate::compile(&source, "source.opy", &dir).expect("fixture must resolve");
    let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap();
    let catalog = Catalog::builtin().unwrap();
    let locale = Locale::new("en-US");
    let oracle = workshop_rs::parser::parse(&oracle_workshop(&dir), &catalog, &locale).unwrap();

    assert!(
        equivalent(&artifact.wir, &oracle),
        "native WIR diverged\n{}",
        artifact.emitted
    );
}

#[derive(Debug, PartialEq, Eq)]
enum NativeInstruction {
    If,
    Else,
    End,
    Skip(wir::ValueId),
    SetGlobal(i64),
}

fn flatten_action(
    program: &wir::Program,
    action_id: wir::ActionId,
    rule_final: bool,
    instructions: &mut Vec<NativeInstruction>,
) {
    match program.actions.get(action_id).unwrap() {
        Action::SetGlobalVariable { value, .. } => {
            let Value::Number { value, .. } = &program.values.get(*value).unwrap().value else {
                panic!("switch behavior probe expects numeric assignments")
            };
            instructions.push(NativeInstruction::SetGlobal(*value as i64));
        }
        Action::If {
            branches,
            else_body,
            ..
        } => {
            for (index, branch) in branches.iter().enumerate() {
                if index > 0 {
                    instructions.push(NativeInstruction::Else);
                }
                instructions.push(NativeInstruction::If);
                for action in &branch.body {
                    flatten_action(program, *action, false, instructions);
                }
            }
            if let Some(else_body) = else_body {
                instructions.push(NativeInstruction::Else);
                for action in else_body {
                    flatten_action(program, *action, false, instructions);
                }
            }
            if !rule_final {
                instructions.push(NativeInstruction::End);
            }
        }
        Action::Call { name, args, .. } if name == "skip" => {
            instructions.push(NativeInstruction::Skip(args[0]));
        }
        other => panic!("switch behavior probe found unexpected action: {other:?}"),
    }
}

fn array_values(program: &wir::Program, value_id: wir::ValueId) -> Vec<wir::ValueId> {
    match &program.values.get(value_id).unwrap().value {
        Value::Array(values) => values.clone(),
        Value::Call { name, args } if name == "array" => args.clone(),
        other => panic!("expected an array value, got {other:?}"),
    }
}

fn numeric_value(program: &wir::Program, value_id: wir::ValueId, selector: i64) -> i64 {
    match &program.values.get(value_id).unwrap().value {
        Value::Number { value, .. } => *value as i64,
        Value::Null => 0,
        Value::GlobalVariable(_) => selector,
        Value::Array(values) => panic!("array value must be consumed by a call: {values:?}"),
        Value::Call { name, args } => match name.as_str() {
            "add" => args
                .iter()
                .map(|value| numeric_value(program, *value, selector))
                .sum(),
            "indexOfArrayValue" => {
                let values = array_values(program, args[0]);
                let needle = numeric_value(program, args[1], selector);
                values
                    .iter()
                    .position(|value| numeric_value(program, *value, selector) == needle)
                    .map_or(-1, |index| index as i64)
            }
            "valueInArray" => {
                let values = array_values(program, args[0]);
                let index = numeric_value(program, args[1], selector);
                numeric_value(program, values[index as usize], selector)
            }
            other => panic!("switch behavior probe found unexpected value call: {other}"),
        },
        other => panic!("switch behavior probe found unexpected value: {other:?}"),
    }
}

fn matching_end(instructions: &[NativeInstruction], else_index: usize) -> usize {
    let mut nested = 0;
    for (index, instruction) in instructions.iter().enumerate().skip(else_index + 1) {
        match instruction {
            NativeInstruction::If => nested += 1,
            NativeInstruction::End if nested == 0 => return index,
            NativeInstruction::End => nested -= 1,
            _ => {}
        }
    }
    instructions.len()
}

fn native_switch_trace(program: &wir::Program, rule: &wir::Rule, selector: i64) -> Vec<i64> {
    let mut instructions = Vec::new();
    for (index, action) in rule.actions.iter().enumerate() {
        flatten_action(
            program,
            *action,
            index + 1 == rule.actions.len(),
            &mut instructions,
        );
    }

    let mut trace = Vec::new();
    let mut if_stack = Vec::new();
    let mut pc = 0;
    while pc < instructions.len() {
        match &instructions[pc] {
            NativeInstruction::If => {
                if_stack.push(true);
                pc += 1;
            }
            NativeInstruction::Else => {
                if if_stack.pop().unwrap_or(false) {
                    pc = matching_end(&instructions, pc) + 1;
                } else {
                    if_stack.push(true);
                    pc += 1;
                }
            }
            NativeInstruction::End => {
                if_stack.pop();
                pc += 1;
            }
            NativeInstruction::Skip(value) => {
                let count = numeric_value(program, *value, selector);
                assert!(count >= 0, "switch skip count must be non-negative");
                pc += count as usize + 1;
            }
            NativeInstruction::SetGlobal(value) => {
                trace.push(*value);
                pc += 1;
            }
        }
    }
    trace
}

fn pinned_switch_traces(dir: &Path) -> Vec<(i64, Vec<i64>)> {
    let workshop = oracle_workshop(dir);
    let offsets = workshop
        .split("Skip(Value In Array(Array(")
        .nth(1)
        .unwrap()
        .split("), Add")
        .next()
        .unwrap()
        .split(", ")
        .map(|value| value.parse::<i64>().unwrap())
        .collect::<Vec<_>>();
    let mut actions = Vec::new();
    for line in workshop.lines() {
        if let Some(value) = line
            .trim()
            .strip_prefix("Set Global Variable(value, ")
            .and_then(|line| line.strip_suffix(");"))
        {
            actions.push(value.parse::<i64>().unwrap());
        } else if line.trim() == "Else;" {
            actions.push(-1);
        }
    }
    let trace_at = |offset: i64| {
        let mut trace = Vec::new();
        for action in actions.iter().skip(offset as usize) {
            if *action == -1 {
                break;
            }
            trace.push(*action);
        }
        trace
    };
    [0, 1, 2, 3, 99]
        .into_iter()
        .map(|selector| {
            let case_offset = [1, 2, 3]
                .iter()
                .position(|value| *value == selector)
                .map_or(offsets[0], |index| offsets[index + 1]);
            (selector, trace_at(case_offset))
        })
        .collect()
}

#[test]
fn switch_lowering_matches_the_pinned_oracle() {
    for name in [
        "control-flow",
        "control-flow-lowering",
        "switch-break",
        "switch-order",
        "switch-structured-target",
    ] {
        assert_native_wir_equivalent(name);
    }
}

#[test]
fn control_flow_debug_lowers_to_a_native_hud_action() {
    let dir = fixture_dir("control-flow");
    let source = std::fs::read_to_string(dir.join("source.opy")).unwrap();
    let hir = crate::compile(&source, "source.opy", &dir).unwrap();
    let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap();

    let rule = artifact
        .wir
        .rules
        .get(workshop_rs::wir::RuleId::from_index(0))
        .unwrap();
    let for_action = artifact.wir.actions.get(rule.actions[0]).unwrap();
    let Action::ForGlobalVariable { body, .. } = for_action else {
        panic!("control-flow fixture must lower its for loop");
    };
    let if_action = artifact.wir.actions.get(body[0]).unwrap();
    let Action::If { branches, .. } = if_action else {
        panic!("control-flow fixture must lower its conditional");
    };
    let hud_action = artifact.wir.actions.get(branches[0].body[0]).unwrap();
    let Action::Call { name, span, .. } = hud_action else {
        panic!("control-flow fixture debug must lower to a native HUD action");
    };
    assert_eq!(name, "createHudText");
    assert_eq!(span.unwrap().start.line, 7);
}

#[test]
fn do_while_break_shapes_match_the_pinned_oracle() {
    assert_native_wir_equivalent("do-while-break");
}

#[test]
fn multiple_switch_breaks_match_independent_semantic_oracle() {
    let compiler = Compiler::new().unwrap();
    let dir = fixture_dir("switch-multiple-break");
    let source = std::fs::read_to_string(dir.join("source.opy")).unwrap();
    let hir = crate::compile(&source, "source.opy", &dir).unwrap();
    let artifact = compiler
        .compile_hir(&hir)
        .expect("multi-break switch must lower");
    let semantic_oracle: serde_json::Value =
        serde_json::from_str(&std::fs::read_to_string(dir.join("semantic-oracle.json")).unwrap())
            .unwrap();
    let catalog = Catalog::builtin().unwrap();
    let semantic_wir = workshop_rs::parser::parse(
        semantic_oracle["compile"]["workshop"].as_str().unwrap(),
        &catalog,
        &Locale::new("en-US"),
    )
    .unwrap();
    assert!(
        equivalent(&artifact.wir, &semantic_wir),
        "native WIR diverged from the independent switch semantic oracle\n{}",
        artifact.emitted
    );
}

#[test]
fn pinned_overpy_switch_action_trace() {
    let compiler = Compiler::new().unwrap();
    let dir = fixture_dir("switch-multiple-break");
    let source = std::fs::read_to_string(dir.join("source.opy")).unwrap();
    let hir = crate::compile(&source, "source.opy", &dir).unwrap();
    let artifact = compiler
        .compile_hir(&hir)
        .expect("multi-break switch must lower");
    let rule = artifact
        .wir
        .rules
        .iter()
        .find(|rule| rule.name == "issue 47 switch multiple break")
        .unwrap();
    for (selector, expected) in pinned_switch_traces(&dir) {
        assert_eq!(
            native_switch_trace(&artifact.wir, rule, selector),
            expected,
            "native switch behavior diverged from the pinned OverPy action trace for selector {selector}"
        );
    }
}

#[test]
fn invalid_do_while_placement_is_source_attributed() {
    let dir = fixture_dir("do-while-invalid");
    let source = std::fs::read_to_string(dir.join("source.opy")).unwrap();
    let error = crate::compile(&source, "source.opy", &dir)
        .expect_err("invalid do-while placement must be rejected");
    assert_eq!(error.code, "do-while-placement");
    assert_eq!(error.span.unwrap().start.line, 6);
}

#[test]
fn nested_switch_break_is_source_attributed_when_not_representable() {
    let compiler = Compiler::new().unwrap();
    let dir = fixture_dir("switch-break-unsupported");
    let source = std::fs::read_to_string(dir.join("source.opy")).unwrap();
    let hir = crate::compile(&source, "source.opy", &dir).unwrap();
    let error = match compiler.compile_hir(&hir) {
        Ok(_) => panic!("nested switch break unexpectedly lowered"),
        Err(error) => error,
    };
    assert_eq!(error.diagnostic.code, "unsupported-integration-surface");
    assert_eq!(error.diagnostic.span.unwrap().start.line, 7);
}