opy-rs 0.3.9

Standalone OverPy-compatible .opy implementation with parsing, tooling, and bounded Workshop compilation.
Documentation
use std::path::Path;

use opy_rs::hir::types::{Expr, Stmt};

#[test]
fn source_surface_reaches_validated_hir_with_spans() {
    let source = concat!(
        "globalvar value\n",
        "rule \"syntax surface\":\n",
        "    @Event global\n",
        "    del value[1]\n",
        "    value min= 2\n",
        "    value max= 3\n",
        "    while value < 4:\n",
        "        continue\n",
        "    goto RULE_START\n",
        "    goto target\n",
        "    goto loc + value\n",
        "    target:\n",
        "    return\n",
    );
    let program = opy_rs::compile(source, "source-syntax.opy", Path::new(""))
        .expect("the source-syntax source surface must lower to HIR");
    program.validate().expect("the generated HIR must validate");

    let rule = match &program.rules[0] {
        opy_rs::hir::types::RuleEntry::Rule(rule) => rule,
        other => panic!("expected rule, got {other:?}"),
    };
    assert!(matches!(
        rule.actions[0],
        Stmt::Delete { span: Some(_), .. }
    ));
    assert!(matches!(
        &rule.actions[1],
        Stmt::Assign {
            value,
            span: Some(_),
            ..
        } if matches!(value.as_ref(), Expr::Binary { op, .. } if op == "min")
    ));
    assert!(matches!(
        &rule.actions[2],
        Stmt::Assign {
            value,
            span: Some(_),
            ..
        } if matches!(value.as_ref(), Expr::Binary { op, .. } if op == "max")
    ));
    let Stmt::While {
        body,
        span: Some(_),
        ..
    } = &rule.actions[3]
    else {
        panic!("expected while with a source span");
    };
    assert!(matches!(
        body.as_slice(),
        [Stmt::Continue { span: Some(_) }]
    ));
    assert!(matches!(
        &rule.actions[4],
        Stmt::Goto {
            label: None,
            offset: None,
            rule_start: true,
            span: Some(_),
        }
    ));
    assert!(matches!(
        &rule.actions[5],
        Stmt::Goto {
            label: Some(_),
            offset: None,
            rule_start: false,
            span: Some(_),
        }
    ));
    assert!(matches!(
        &rule.actions[6],
        Stmt::Goto {
            label: None,
            offset: Some(_),
            rule_start: false,
            span: Some(_),
        }
    ));
    assert!(matches!(&rule.actions[7], Stmt::Label { name, span: Some(_) } if name == "target"));
    assert!(matches!(&rule.actions[8], Stmt::Return { span: Some(_) }));
}

#[test]
fn source_return_reaches_abort_in_canonical_wir() {
    let source = "rule \"return\":\n    @Event global\n    return\n    debug(\"retained\")\n";
    let artifact = opy_rs::Compiler::new()
        .expect("the compiler contract loads")
        .compile_source(source, "source-syntax-return.opy", Path::new(""))
        .expect("return must lower to a canonical abort action");
    assert!(artifact.emitted_workshop.contains("Abort;"));
    assert!(!artifact.emitted_workshop.contains("Abort If(True)"));
}

#[test]
fn source_invalid_statement_contexts_are_source_diagnostics() {
    let cases = [
        (
            "rule \"invalid delete\":\n    @Event global\n    del value\n",
            "parse-error",
        ),
        (
            "rule \"invalid goto\":\n    @Event global\n    goto loc\n",
            "parse-error",
        ),
        (
            "rule \"outside continue\":\n    @Event global\n    continue\n",
            "continue-context",
        ),
    ];
    for (source, expected_code) in cases {
        let error = opy_rs::compile(source, "source-syntax-invalid.opy", Path::new(""))
            .expect_err("invalid source-syntax form unexpectedly compiled");
        assert_eq!(error.code, expected_code, "source: {source}");
        assert!(
            error.span.is_some(),
            "source diagnostic lost its span: {source}"
        );
    }
}

#[test]
fn source_backend_boundary_is_explicit_for_source_only_statements() {
    let cases = [(
        "rule \"goto\":\n    @Event global\n    goto target\n",
        "unknown goto label",
    )];
    let compiler = opy_rs::Compiler::new().expect("the compiler contract loads");
    for (source, expected_text) in cases {
        let error = compiler
            .compile_source(source, "source-syntax-backend.opy", Path::new(""))
            .expect_err("source-only syntax must not be silently discarded");
        assert_eq!(error.diagnostic.code, "unsupported-integration-surface");
        assert!(error.diagnostic.message.contains(expected_text));
        assert!(error.diagnostic.span.is_some());
    }
}

#[test]
fn source_delete_reaches_canonical_index_mutation() {
    let source = "globalvar A\nrule \"delete\":\n    @Event global\n    del A[1]\n";
    let artifact = opy_rs::Compiler::new()
        .expect("the compiler contract loads")
        .compile_source(source, "source-syntax-delete.opy", Path::new(""))
        .expect("indexed delete must lower to canonical array operations");
    assert!(
        artifact
            .emitted_workshop
            .contains("Modify Global Variable(A, Remove From Array By Index, 1)")
    );
}

#[test]
fn source_continue_reaches_canonical_loop_control() {
    let source = concat!(
        "globalvar A\n",
        "rule \"continue\":\n",
        "    @Event global\n",
        "    while A < 1:\n",
        "        if A == 0:\n",
        "            continue\n",
        "        wait()\n",
        "        A += 1\n",
    );
    let artifact = opy_rs::Compiler::new()
        .expect("the compiler contract loads")
        .compile_source(source, "source-syntax-continue.opy", Path::new(""))
        .expect("continue must lower to canonical loop control");
    assert!(artifact.emitted_workshop.contains("Skip If("));
}

#[test]
fn source_nested_continue_uses_the_nearest_loop_and_preserves_tail_flow() {
    let source = concat!(
        "globalvar A\n",
        "rule \"nested continue\":\n",
        "    @Event global\n",
        "    while A < 2:\n",
        "        if A == 0:\n",
        "            if A < 1:\n",
        "                continue\n",
        "            A = 1\n",
        "            A = A + 10\n",
        "        A = A + 1\n",
    );
    let artifact = opy_rs::Compiler::new()
        .expect("the compiler contract loads")
        .compile_source(source, "source-syntax-nested-continue.opy", Path::new(""))
        .expect("nested continue must lower to canonical loop control");
    assert!(
        artifact
            .emitted_workshop
            .contains("Skip If(Compare(Global.A, <, 1), 4);")
    );
    assert!(
        artifact
            .emitted_workshop
            .contains("Modify Global Variable(A, Add, 1);")
    );
}

#[test]
fn source_nested_loop_continue_targets_the_innermost_loop() {
    let source = concat!(
        "globalvar A\n",
        "rule \"nested loops\":\n",
        "    @Event global\n",
        "    while A < 2:\n",
        "        while A < 1:\n",
        "            continue\n",
        "            A = A + 10\n",
        "        A = A + 1\n",
    );
    let artifact = opy_rs::Compiler::new()
        .expect("the compiler contract loads")
        .compile_source(source, "source-syntax-nested-loops.opy", Path::new(""))
        .expect("nested loop continue must lower");
    assert!(
        artifact
            .emitted_workshop
            .contains("While(Compare(Global.A, <, 1));\n                Skip(1);")
    );
    assert!(
        artifact
            .emitted_workshop
            .contains("Modify Global Variable(A, Add, 1);")
    );
}

#[test]
fn source_continue_skips_later_conditional_branches() {
    let source = concat!(
        "globalvar A\n",
        "rule \"continue if else\":\n",
        "    @Event global\n",
        "    while A < 2:\n",
        "        if A == 0:\n",
        "            continue\n",
        "        else:\n",
        "            A = A + 10\n",
        "        A = A + 1\n",
    );
    let artifact = opy_rs::Compiler::new()
        .expect("the compiler contract loads")
        .compile_source(source, "source-syntax-continue-if-else.opy", Path::new(""))
        .expect("continue in an if/else must lower to canonical loop control");
    assert!(artifact.emitted_workshop.contains("Skip(4);"));
    assert!(artifact.emitted_workshop.contains("Else;"));
}

#[test]
fn source_multiline_grouped_expressions_reach_hir_with_spans() {
    let source = concat!(
        "rule \"multiline expressions\":\n",
        "    @Event global\n",
        "    debug(\n",
        "        (\n",
        "            1\n",
        "            + 2\n",
        "        ) if (\n",
        "            true\n",
        "        ) else (\n",
        "            3\n",
        "            * 4\n",
        "        )\n",
        "    )\n",
    );
    let program = opy_rs::compile(source, "source-syntax-expressions.opy", Path::new(""))
        .expect("multiline grouped expressions must reach HIR");
    program.validate().expect("the generated HIR must validate");

    let rule = match &program.rules[0] {
        opy_rs::hir::types::RuleEntry::Rule(rule) => rule,
        other => panic!("expected rule, got {other:?}"),
    };
    let Stmt::Expr { expr, .. } = &rule.actions[0] else {
        panic!("expected an expression statement");
    };
    let Expr::Call { args, span, .. } = &**expr else {
        panic!("expected a call expression, got {expr:?}");
    };
    assert_eq!(span.unwrap().start.line, 3);
    assert_eq!(span.unwrap().end.line, 13);
    let Expr::Conditional {
        then_value,
        condition,
        else_value,
        span,
    } = &args[0]
    else {
        panic!("expected a conditional value, got {:?}", args[0]);
    };
    assert_eq!(span.unwrap().start.line, 5);
    assert_eq!(span.unwrap().end.line, 11);
    assert!(matches!(
        then_value.as_ref(),
        Expr::Binary { op, .. } if op == "+"
    ));
    assert!(matches!(
        else_value.as_ref(),
        Expr::Binary { op, .. } if op == "*"
    ));
    assert_eq!(then_value.span().unwrap().start.line, 5);
    assert_eq!(condition.span().unwrap().start.line, 8);
    assert_eq!(else_value.span().unwrap().start.line, 10);
}

#[test]
fn source_multiline_conditional_missing_else_is_a_source_diagnostic() {
    let source = concat!(
        "rule \"invalid multiline expression\":\n",
        "    @Event global\n",
        "    debug(\n",
        "        1 if\n",
        "        true\n",
        "    )\n",
    );
    let error = opy_rs::compile(
        source,
        "source-syntax-invalid-expression.opy",
        Path::new(""),
    )
    .expect_err("a conditional without else must remain rejected");
    assert_eq!(error.code, "parse-error");
    assert!(error.message.contains("expected `else`"));
    assert!(error.span.is_some());
}