ironwork-exec 0.6.0

ironwork for COBOL: an interpreter over EBCDIC storage
Documentation
use super::*;

fn source(data: &str, statements: &[&str]) -> String {
    let long = statements.iter().flat_map(|s| s.split('\n')).find(|l| l.len() > 61);
    assert!(long.is_none(), "past column 72: {long:?}");
    let body: String = statements.iter().flat_map(|s| s.split('\n')).map(line).chain([line("GOBACK.")]).collect();
    program("", data, &body)
}

fn displays(data: &str, statements: &[&str]) -> String {
    let o = Harness::source(&source(data, statements)).run(Executor::Interpreter);
    assert!(o.ending.is_ok(), "{:?}\n{}", o.ending, o.err);
    o.out
}

/// As `displays`, and the VM must run the program to its end with the same output.
fn displays_on_both(data: &str, statements: &[&str]) -> String {
    let out = displays(data, statements);
    let vm = Harness::source(&source(data, statements)).run(Executor::Vm);
    assert_eq!((vm.out.as_str(), vm.ending.is_ok()), (out.as_str(), true), "{}", vm.err);
    out
}

const CODES: &str = "       01  Code-Out PIC 9(3).\n       01  Status-Out PIC 9(3).\n";
const SHOW: &str = "MOVE JSON-CODE TO Code-Out\nMOVE JSON-STATUS TO Status-Out\nDISPLAY 'CODE ' Code-Out ' STATUS ' Status-Out";

#[test]
fn a_document_fills_the_items_its_names_match() {
    let data = format!(
        "{CODES}       01  T PIC X(60) VALUE\n           '{{\"msg\":{{\"ver\":5,\"uid\":1000,\"txt\":\"Hello World!\"}}}}'.\n       01  U PIC X(60).\n       01  msg.\n           04 ver usage comp-1.\n           04 uid pic 9999 usage display.\n           04 txt pic x(32).\n"
    );
    let out = displays(
        &data,
        &[
            "JSON PARSE T INTO msg ENCODING 1140 END-JSON",
            SHOW,
            "IF ver EQUAL TO 5 DISPLAY 'Message ID is ' uid",
            "DISPLAY \"Message text is '\" txt \"'\" END-IF",
            "MOVE FUNCTION DISPLAY-OF(FUNCTION NATIONAL-OF(T) 1208) TO U",
            "MOVE SPACES TO msg",
            "JSON PARSE U INTO msg",
            SHOW,
            "DISPLAY uid ' ' txt",
            "JSON PARSE T INTO msg",
            SHOW,
        ],
    );
    assert_eq!(
        out,
        concat!(
            "CODE 000 STATUS 000\nMessage ID is 1000\nMessage text is 'Hello World!                    '\n",
            "CODE 000 STATUS 000\n1000 Hello World!                    \n",
            "CODE 100 STATUS 000\n",
        )
    );
}

#[test]
fn name_suppress_and_the_matching_rules_set_the_status() {
    let data = format!(
        "{CODES}       01  T PIC X(80).\n       01  mydata pic 999.\n       01 G.\n         05 h.\n           10 a pic x(10).\n           10 3_ pic 9.\n           10 C-c pic x(10).\n"
    );
    let parse = |text: &str, phrase: &str| format!("MOVE '{text}' TO T\nJSON PARSE T INTO G ENCODING 1140{phrase}\n{SHOW}\nDISPLAY a '|' 3_ '|' C-c\nMOVE ALL '-' TO a C-c");
    let statements = [
        "MOVE '{\"abc+\":100}' TO T\nJSON PARSE T INTO mydata ENCODING 1140\n    NAME OF mydata IS \"abc+\"\nEND-JSON\nDISPLAY 'mydata is ' mydata".to_owned(),
        parse("{\"g\": {\"H\": {\"A\": \"Eh?\", \"3_\": 5, \"c-C\": \"See\"}}}", ""),
        parse("{\"g\": {\"H\": {\"A\": \"Eh?\", \"c-C\": \"See\"}}}", ""),
        parse("{\"G\": {\"h\": {\"A\": \"Eh?\", \"B\": \"Bee\", \"3_\": 5}}}", ""),
        parse("{\"g\": {\"A\": \"Eh?\", \"3_\": 5, \"c-C\": \"See\"}}", ""),
        parse("{\"g\": {\"H\": {\"A\": \"x\", \"3_\": \"x\", \"c-C\": \"abc\"}}}", ""),
        parse("{\"g\": {\"H\": {\"A\": \"one\", \"A\": \"two\"}}}", ""),
        parse("{\"g\": {\"H\": {\"A\": \"one\", \"3_\": 7, \"C-C\": \"x\"}}}", "\n    SUPPRESS 3_\n    NAME OF a IS 'C-C' C-c IS 'A'"),
    ];
    let out = displays(&data, &statements.iter().map(String::as_str).collect::<Vec<_>>());
    assert_eq!(
        out,
        concat!(
            "mydata is 100\n",
            "CODE 000 STATUS 000\nEh?       |5|See       \n",
            "CODE 000 STATUS 001\nEh?       |5|See       \n",
            "CODE 000 STATUS 003\nEh?       |5|----------\n",
            "CODE 106 STATUS 003\n----------|5|----------\n",
            "CODE 104 STATUS 000\nx         |5|----------\n",
            "CODE 103 STATUS 000\none       |5|----------\n",
            "CODE 000 STATUS 000\nx         |5|one       \n",
        )
    );
}

#[test]
fn arrays_fill_tables_up_to_their_occurrences() {
    let data = format!(
        "{CODES}       01  T PIC X(120).\n       01  n pic 9.\n       01  some-data.\n        02 msg occurs 0 to 5 depending on n.\n           04 ver pic 9.\n           04 uid pic 9999 usage display.\n"
    );
    let out = displays(
        &data,
        &[
            "MOVE 4 TO n",
            "MOVE 0 TO ver(1) ver(2) ver(3) ver(4)",
            "MOVE '{\"some-data\":{\"msg\":[{\"ver\":5,\"uid\":10},' TO T",
            "MOVE '{\"ver\":5,\"uid\":11},{\"ver\":5,\"uid\":12}]}}' TO T(41:)",
            "JSON PARSE T INTO some-data ENCODING 1140",
            SHOW,
            "DISPLAY ver(1) uid(1) ' ' ver(2) uid(2) ' ' ver(3) uid(3)\n    ' ' ver(4)",
            "MOVE 2 TO n",
            "JSON PARSE T INTO some-data ENCODING 1140",
            SHOW,
        ],
    );
    assert_eq!(out, "CODE 000 STATUS 008\n50010 50011 50012 0\nCODE 000 STATUS 016\n");
}

#[test]
fn an_anonymous_array_parses_and_generates_with_name_omitted() {
    let data = format!(
        "{CODES}       01  T PIC X(100).\n       01  N PIC 9(4).\n       01 ACT.\n         02 B1 occurs 2.\n           03 C1.\n            04 M1 pic 9.\n            04 D1 occurs 2.\n              05 N1 pic 9.\n"
    );
    let out = displays(
        &data,
        &[
            "move spaces to ACT",
            "move '[{\"C1\":{\"M1\":1,\"D1\":[{\"N1\":3},{\"N1\":4}]}},' to T",
            "move '{\"C1\":{\"M1\":2,\"D1\":[{\"N1\":5},{\"N1\":6}]}}]' to T(43:)",
            "json parse T into b1 encoding 1140\n    name b1 is omitted\nend-json",
            SHOW,
            "display M1(1) M1(2) N1(1 1) N1(1 2) N1(2 1) N1(2 2)",
            "json parse T into b1 encoding 1140",
            SHOW,
            "move spaces to T",
            "json generate T from b1 count N encoding 1140\n    name b1 is omitted\nend-json",
            "display T(1:N)",
        ],
    );
    assert_eq!(
        out,
        "CODE 000 STATUS 000\n123456\nCODE 108 STATUS 000\n[{\"C1\":{\"M1\":1,\"D1\":[{\"N1\":3},{\"N1\":4}]}},{\"C1\":{\"M1\":2,\"D1\":[{\"N1\":5},{\"N1\":6}]}}]\n"
    );
}

#[test]
fn nulls_are_ignored_converted_or_indicated_and_booleans_converted() {
    let data = format!(
        concat!(
            "{}       01  T PIC X(80).\n",
            "       01 my-record.\n         02 data-a pic 9999.\n         02 data-b pic x(10).\n",
            "       01 MY-REC.\n         02 DATA-1-IS-NULL PIC X.\n         02 DATA-1 PIC X(10).\n",
            "       01 myrecord.\n         02 flag-a pic x.\n           88 flag-a-on value 'T' false 'F'.\n",
            "         02 flag-b pic x.\n           88 flag-b-true value '1'.\n           88 flag-b-false value '0'.\n",
            "         02 flag-c pic x.\n",
        ),
        CODES
    );
    let out = displays(
        &data,
        &[
            "MOVE 1234 TO data-a",
            "MOVE '0123456789' TO data-b",
            "MOVE '{\"my-record\" : {\"data-a\" : null,' TO T",
            "MOVE ' \"data-b\" : null}}' TO T(38:)",
            "JSON PARSE T INTO my-record ENCODING 1140",
            SHOW,
            "DISPLAY data-a \"'\" data-b \"'\"",
            "JSON PARSE T INTO my-record ENCODING 1140\n    CONVERTING data-a FROM NULL USING ZERO\n    ALSO data-b FROM JSON NULL USING SPACES\nEND-JSON",
            SHOW,
            "DISPLAY data-a \"'\" data-b \"'\"",
            "MOVE 1234 TO data-a",
            "JSON PARSE T INTO my-record ENCODING 1140\n    IGNORING JSON NULL FOR ALL",
            SHOW,
            "MOVE '{ \"MY-REC\" : { \"data-1\" : null } }' TO T",
            "MOVE ALL 'z' TO MY-REC",
            "JSON PARSE T INTO MY-REC ENCODING 1140\n    INDICATING DATA-1 IS JSON NULL\n    USING 'Y' AND 'N' IN DATA-1-IS-NULL\nEND-JSON",
            SHOW,
            "DISPLAY 'DATA-1-IS-NULL: ' DATA-1-IS-NULL ' ' DATA-1",
            "MOVE '{\"myrecord\":{\"flag-a\":true,' TO T",
            "MOVE '\"flag-b\":false,\"flag-c\":true}}' TO T(28:)",
            "JSON PARSE T INTO myrecord ENCODING 1140\n    CONVERTING flag-a FROM BOOLEAN USING flag-a-on\n    ALSO flag-b FROM BOOLEAN\n    USING flag-b-true AND flag-b-false\n    ALSO flag-c FROM BOOLEAN USING 'a' AND 'z'",
            SHOW,
            "DISPLAY flag-a flag-b flag-c",
            "JSON PARSE T INTO myrecord ENCODING 1140",
            SHOW,
        ],
    );
    assert_eq!(
        out,
        concat!(
            "CODE 000 STATUS 032\n1234'0123456789'\n",
            "CODE 000 STATUS 000\n0000'          '\n",
            "CODE 000 STATUS 000\n",
            "CODE 000 STATUS 000\nDATA-1-IS-NULL: Y zzzzzzzzzz\n",
            "CODE 000 STATUS 000\nT0a\n",
            "CODE 107 STATUS 000\n",
        )
    );
}

#[test]
fn the_programming_guides_client_data_moves_into_edited_and_national_items() {
    let data = concat!(
        "       01 jtxt-client-data.\n",
        "         03 pic x(16)  value '{\"client-data\":{'.\n",
        "         03 pic x(28)  value ' \"account-num\":123456789012,'.\n",
        "         03 pic x(19)  value ' \"balance\":-125.53,'.\n",
        "         03 pic x(17)  value ' \"billing-info\":{'.\n",
        "         03 pic x(22)  value '  \"name-first\":\"John\",'.\n",
        "         03 pic x(22)  value '  \"name-last\":\"Smith\",'.\n",
        "         03 pic x(21)  value '  \"addr-code\":\"10203\"'.\n",
        "         03 pic x(6)   value '  } }}'.\n",
        "       01 jtxt-transactions.\n",
        "         03 pic x(31)  value '{\"transactions\": {\"tx-record\":['.\n",
        "         03 pic x(39)  value '{\"tx-uid\":107,\"tx-item-uid\":\"ab142424\",'.\n",
        "         03 pic x(38)  value '\"tx-priceinUS$\":12.34},{\"tx-uid\":1904,'.\n",
        "         03 pic x(25)  value '\"tx-item-uid\":\"gb051533\",'.\n",
        "         03 pic x(23)  value '\"tx-priceinUS$\":833.22}'.\n",
        "         03 pic x(3)   value ']}}'.\n",
        "       77 txnum pic 999999 usage display value zero.\n",
        "       01 client-data.\n",
        "         03 account-num   pic 999,999,999,999.\n",
        "         03 balance       pic $$$9.99CR.\n",
        "         03 billing-info.\n",
        "          05 name-first  pic n(20).\n",
        "          05 name-last   pic n(20).\n",
        "          05 addr-code   pic n(10).\n",
        "         03 transactions.\n",
        "          05 tx-record occurs 0 to 100 depending txnum.\n",
        "           07 tx-uid       pic 99999 usage display.\n",
        "           07 tx-item-uid  pic AA/9999B99.\n",
        "           07 tx-price     pic $$$9.99.\n",
    );
    let out = displays(
        data,
        &[
            "Json parse jtxt-client-data into client-data\n    encoding 1140\n    with detail\n    suppress transactions\n    not on exception\n    display \"Successful JSON Parse\"\nend-json",
            "Display account-num ' ' balance",
            "Display function display-of(name-last)\n    function display-of(addr-code)",
            "Move 2 to txnum",
            "Json parse jtxt-transactions into transactions encoding 1140\n    name tx-price is 'tx-priceinUS$'\n    not on exception\n    display \"Successful JSON Parse\"\nend-json",
            "Display tx-uid(1) ' ' tx-item-uid(1) ' ' tx-price(1)",
            "Display tx-uid(2) ' ' tx-item-uid(2) ' ' tx-price(2)",
        ],
    );
    assert_eq!(
        out,
        concat!(
            "Successful JSON Parse\n123,456,789,012 $125.53CR\nSmith               10203     \n",
            "Successful JSON Parse\n00107 ab/1424 24  $12.34\n01904 gb/0515 33 $833.22\n",
        )
    );
}

#[test]
fn integers_go_into_national_and_alphanumeric_items_and_any_number_into_floating_point() {
    let data = format!("{CODES}       01  T PIC X(40) VALUE '{{\"G\":{{\"N\":42,\"F\":1.5E1,\"A\":7}}}}'.\n       01  G.\n           05 N PIC N(4).\n           05 F COMP-2.\n           05 A PIC X(3).\n");
    let out = displays_on_both(&data, &["JSON PARSE T INTO G ENCODING 1140", SHOW, "DISPLAY FUNCTION DISPLAY-OF(N) '|' A '|'", "COMPUTE Code-Out = F", "DISPLAY Code-Out"]);
    assert_eq!(out, "CODE 000 STATUS 000\n42  |7  |\n015\n");
}

#[test]
fn a_number_moves_only_where_table_46_allows_and_an_exponent_is_never_expanded() {
    let data = format!("{CODES}       01  T PIC X(60).\n       01  G.\n           05 ALPHA PIC A(5).\n           05 ALNUM PIC X(5).\n           05 NUM PIC 9(4).\n");
    let out = displays_on_both(
        &data,
        &[
            "MOVE '{\"G\":{\"ALPHA\":42}}' TO T",
            "JSON PARSE T INTO G ENCODING 1140",
            SHOW,
            "MOVE '{\"G\":{\"ALNUM\":1e2}}' TO T",
            "JSON PARSE T INTO G ENCODING 1140",
            SHOW,
            "MOVE '{\"G\":{\"ALNUM\":42}}' TO T",
            "JSON PARSE T INTO G ENCODING 1140",
            SHOW,
            "MOVE '{\"G\":{\"NUM\":1e99999999999999999999}}' TO T",
            "JSON PARSE T INTO G ENCODING 1140",
            SHOW,
            "DISPLAY ALNUM ' ' NUM",
        ],
    );
    assert_eq!(out, "CODE 104 STATUS 000\nCODE 104 STATUS 000\nCODE 000 STATUS 001\nCODE 000 STATUS 129\n42    0000\n");
}

#[test]
fn an_alphabetic_item_a_contained_program_inherits_takes_no_number() {
    let source = [
        "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. OUTER.\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n",
        "       01  G GLOBAL.\n           05 ALPHA PIC A(5) VALUE 'ABCDE'.\n       PROCEDURE DIVISION.\n",
        &line("CALL 'INNER'"),
        &line("GOBACK."),
        "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. INNER.\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n",
        "       01  T PIC X(40) VALUE '{\"G\":{\"ALPHA\":42}}'.\n       01  C PIC 9(3).\n       PROCEDURE DIVISION.\n",
        &line("JSON PARSE T INTO G ENCODING 1140"),
        &line("MOVE JSON-CODE TO C"),
        &line("DISPLAY C ' ' ALPHA"),
        &line("GOBACK."),
        "       END PROGRAM INNER.\n       END PROGRAM OUTER.\n",
    ]
    .concat();
    let o = Harness::source(&source).run(Executor::Interpreter);
    assert_eq!((o.out.as_str(), o.ending.is_ok()), ("104 ABCDE\n", true), "{}", o.err);
}

#[test]
fn a_member_after_an_occurs_depending_on_table_is_placed_by_the_count_its_pair_set() {
    let data = concat!(
        "       01  T PIC X(60) VALUE\n           '{\"REC\":{\"CNT\":2,\"ITEM\":[\"A\",\"B\"],\"LATER\":\"XYZ\"}}'.\n",
        "       01  REC.\n           05 CNT PIC 9 VALUE 5.\n           05 ITEM PIC X OCCURS 1 TO 5 DEPENDING ON CNT.\n           05 LATER PIC X(3).\n",
    );
    assert_eq!(displays(&format!("{CODES}{data}"), &["JSON PARSE T INTO REC ENCODING 1140", SHOW, "DISPLAY REC"]), "CODE 000 STATUS 000\n2ABXYZ\n");
}