ironwork-exec 0.1.2

ironwork for COBOL: storage layout and an interpreter over EBCDIC storage
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
use super::*;

/// The options IBM compiles object-oriented COBOL with; RENT and DBCS are its defaults.
const OO_CARD: &str = "       CBL THREAD,DLL\n";

/// A class definition: its REPOSITORY entries, then FACTORY and OBJECT paragraphs as written.
fn class(head: &str, repository: &[&str], parts: &str) -> String {
    let entries: Vec<String> = repository.iter().map(|e| format!("           CLASS {e}")).collect();
    format!(
        "{OO_CARD}       IDENTIFICATION DIVISION.\n       CLASS-ID. {head}.\n       ENVIRONMENT DIVISION.\n       CONFIGURATION SECTION.\n       REPOSITORY.\n{}.\n{parts}       END CLASS {}.\n",
        entries.join("\n"),
        head.split_whitespace().next().unwrap()
    )
}

fn part(kind: &str, data: &str, methods: &[String]) -> String {
    let data = if data.is_empty() { String::new() } else { format!("       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n{data}") };
    format!("       IDENTIFICATION DIVISION.\n       {kind}.\n{data}       PROCEDURE DIVISION.\n{}       END {kind}.\n", methods.concat())
}

/// One method: its name, DATA DIVISION sections, PROCEDURE DIVISION header phrase and body.
fn method(name: &str, data: &str, header: &str, body: &[&str]) -> String {
    let data = if data.is_empty() { String::new() } else { format!("       DATA DIVISION.\n{data}") };
    let body: String = body.iter().map(|l| line(l)).collect();
    format!("       IDENTIFICATION DIVISION.\n       METHOD-ID. \"{name}\".\n{data}       PROCEDURE DIVISION{header}.\n{body}       END METHOD \"{name}\".\n")
}

fn client(repository: &[&str], data: &str, body: &[&str]) -> String {
    let entries: Vec<String> = repository.iter().map(|e| format!("           CLASS {e}")).collect();
    let body: String = body.iter().map(|l| line(l)).collect();
    format!(
        "{OO_CARD}       IDENTIFICATION DIVISION.\n       PROGRAM-ID. CLIENT RECURSIVE.\n       ENVIRONMENT DIVISION.\n       CONFIGURATION SECTION.\n       REPOSITORY.\n{}.\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n{data}       PROCEDURE DIVISION.\n{body}",
        entries.join("\n")
    )
}

/// Runs the first program of `main` with the rest of its programs, and `classes`, in its run unit.
fn run_oo(main: &str, classes: &[String]) -> (String, String, Result<(Ending, i16), Abend>) {
    let o = Harness::source(main).classes(classes).clock(unit::Clock::Fixed(0, 0)).run(Executor::Interpreter);
    (o.out, o.err, o.ending.map(|e| (e, o.return_code)))
}

fn errors(source: &str) -> String {
    compile_errors(source)
}

fn account() -> String {
    class(
        "Account INHERITS Base",
        &["Base IS \"java.lang.Object\"", "Account IS \"Account\""],
        &[
            part(
                "FACTORY",
                "       01  OPENED PIC S9(9) BINARY VALUE 0.\n",
                &[
                    method(
                        "open",
                        "       LINKAGE SECTION.\n       01  OPENING USAGE OBJECT REFERENCE Account.\n",
                        " RETURNING OPENING",
                        &["INVOKE Account NEW RETURNING OPENING", "ADD 1 TO OPENED."],
                    ),
                    method("opened", "       LINKAGE SECTION.\n       01  N PIC S9(9) BINARY.\n", " RETURNING N", &["MOVE OPENED TO N."]),
                ],
            ),
            part(
                "OBJECT",
                "       01  BALANCE PIC S9(9) BINARY VALUE 100.\n       01  LINKED USAGE OBJECT REFERENCE Account.\n",
                &[
                    method("credit", "       LINKAGE SECTION.\n       01  AMOUNT PIC S9(9) BINARY.\n", " USING BY VALUE AMOUNT", &["ADD AMOUNT TO BALANCE."]),
                    method(
                        "credit",
                        "       LINKAGE SECTION.\n       01  SMALL PIC S9(4) COMP-5.\n",
                        " USING BY VALUE SMALL",
                        &["DISPLAY 'SHORT CREDIT'", "ADD SMALL TO BALANCE."],
                    ),
                    method("getBalance", "       LINKAGE SECTION.\n       01  RESULT PIC S9(9) BINARY.\n", " RETURNING RESULT", &["MOVE BALANCE TO RESULT."]),
                ],
            ),
        ]
        .concat(),
    )
}

const ACCOUNT_DATA: &str = "       01  A1 USAGE OBJECT REFERENCE Account.\n       01  A2 USAGE OBJECT REFERENCE Account.\n       01  U USAGE OBJECT REFERENCE.\n       01  AMOUNT PIC S9(9) BINARY.\n       01  SMALL PIC S9(4) BINARY VALUE 3.\n       01  BAL PIC S9(9) BINARY.\n       01  SHOWN PIC ZZZ9.\n       01  MNAME PIC X(20).\n";

#[test]
fn instances_keep_their_own_data_and_factory_data_is_shared() {
    let main = client(
        &["Account IS \"Account\""],
        ACCOUNT_DATA,
        &[
            "INVOKE Account \"open\" RETURNING A1",
            "INVOKE Account \"open\" RETURNING A2",
            "MOVE 25 TO AMOUNT",
            "INVOKE A1 \"credit\" USING BY VALUE AMOUNT",
            "INVOKE A2 \"credit\" USING BY VALUE 7",
            "INVOKE A2 \"credit\" USING BY VALUE SMALL",
            "INVOKE A1 \"getBalance\" RETURNING BAL",
            "MOVE BAL TO SHOWN DISPLAY 'A1 ' SHOWN",
            "SET U TO A2",
            "MOVE 'getBalance' TO MNAME",
            "INVOKE U MNAME RETURNING BAL",
            "MOVE BAL TO SHOWN DISPLAY 'A2 ' SHOWN",
            "INVOKE Account NEW RETURNING A1",
            "INVOKE Account \"opened\" RETURNING BAL",
            "MOVE BAL TO SHOWN DISPLAY 'OPENED ' SHOWN",
            "GOBACK.",
        ],
    );
    let (out, err, ending) = run_oo(&main, &[account()]);
    assert!(ending.is_ok(), "{ending:?}\n{err}");
    assert_eq!(out, "SHORT CREDIT\nA1  125\nA2  110\nOPENED    2\n");
}

fn animals() -> Vec<String> {
    let animal = class(
        "Animal INHERITS Base",
        &["Base IS \"java.lang.Object\""],
        &part(
            "OBJECT",
            "       01  NAME PIC X(6) VALUE 'ANIMAL'.\n",
            &[
                method("speak", "", "", &["DISPLAY 'SOUND OF ' NAME."]),
                method("describe", "", "", &["DISPLAY 'I AM ' NAME", "INVOKE SELF \"speak\"."]),
            ],
        ),
    );
    let dog = class(
        "Dog INHERITS Animal",
        &["Animal"],
        &part(
            "OBJECT",
            "       01  NAME PIC X(6) VALUE 'DOG'.\n       01  BARKS PIC 9 VALUE 0.\n",
            &[method(
                "speak",
                "       WORKING-STORAGE SECTION.\n       01  NAME PIC X(6) VALUE 'REX'.\n",
                "",
                &["ADD 1 TO BARKS", "DISPLAY 'WOOF ' BARKS ' FROM ' NAME", "INVOKE SUPER \"speak\"."],
            )],
        ),
    );
    vec![animal, dog]
}

#[test]
fn inheritance_overrides_dispatch_on_the_object_and_super_reaches_the_parent() {
    let main = client(
        &["Dog", "Animal"],
        "       01  D USAGE OBJECT REFERENCE Dog.\n       01  A USAGE OBJECT REFERENCE Animal.\n",
        &["INVOKE Dog NEW RETURNING D", "INVOKE D \"describe\"", "INVOKE D \"speak\"", "INVOKE Animal NEW RETURNING A", "INVOKE A \"describe\"", "GOBACK."],
    );
    let (out, err, ending) = run_oo(&main, &animals());
    assert!(ending.is_ok(), "{ending:?}\n{err}");
    assert_eq!(out, "I AM ANIMAL\nWOOF 1 FROM REX   \nSOUND OF ANIMAL\nWOOF 2 FROM REX   \nSOUND OF ANIMAL\nI AM ANIMAL\nSOUND OF ANIMAL\n");
}

#[test]
fn set_null_comparisons_and_the_exception_phrases() {
    let data = [ACCOUNT_DATA, "       01  B USAGE OBJECT REFERENCE Account.\n"].concat();
    let main = client(
        &["Account IS \"Account\""],
        &data,
        &[
            "SET A1 TO NULL",
            "IF A1 = NULL DISPLAY 'NULL' END-IF",
            "INVOKE Account NEW RETURNING A1",
            "SET B TO A1",
            "IF A1 = B AND B NOT = NULL DISPLAY 'SAME' END-IF",
            "INVOKE Account NEW RETURNING B",
            "IF A1 NOT = B DISPLAY 'DIFFERENT' END-IF",
            "INVOKE A1 \"nosuch\"",
            "    ON EXCEPTION DISPLAY 'NO METHOD' END-INVOKE",
            "INVOKE A1 \"credit\" USING BY VALUE 1",
            "    ON EXCEPTION DISPLAY 'NEVER'",
            "    NOT ON EXCEPTION DISPLAY 'CREDITED'",
            "END-INVOKE",
            "INVOKE A1 \"getBalance\" RETURNING SMALL",
            "    ON EXCEPTION DISPLAY 'WRONG TYPE' END-INVOKE",
            "SET A1 TO NULL",
            "INVOKE A1 \"credit\" USING BY VALUE 1",
            "DISPLAY 'NOT REACHED'",
            "GOBACK.",
        ],
    );
    let (out, _, ending) = run_oo(&main, &[account()]);
    assert_eq!(out, "NULL\nSAME\nDIFFERENT\nNO METHOD\nCREDITED\nWRONG TYPE\n");
    assert!(ending.unwrap_err().message.contains("NULL"));
    let missing = client(&["Account IS \"Account\""], ACCOUNT_DATA, &["INVOKE Account NEW RETURNING A1", "INVOKE A1 \"close\"", "GOBACK."]);
    let abend = run_oo(&missing, &[account()]).2.unwrap_err();
    assert_eq!(abend.code, "U4038");
    assert!(abend.message.contains("\"close\""), "{}", abend.message);
}

#[test]
fn a_java_class_is_checked_and_reaching_it_ends_the_run_naming_class_and_method() {
    let data = [
        ACCOUNT_DATA,
        "       01  S USAGE OBJECT REFERENCE JString.\n       01  O USAGE OBJECT REFERENCE JObject.\n",
        "       01  FLAG PIC X.\n           88 FLAG-FALSE VALUE X'00'.\n           88 FLAG-TRUE VALUE X'01' THRU X'FF'.\n",
    ]
    .concat();
    let repository = ["Account IS \"Account\"", "JString IS \"java.lang.String\"", "JObject IS \"java.lang.Object\""];
    let main = client(
        &repository,
        &data,
        &[
            "INVOKE Account NEW RETURNING A1",
            "SET O TO A1",
            "INVOKE A1 \"equals\" USING BY VALUE O RETURNING FLAG",
            "IF FLAG-TRUE DISPLAY 'EQUAL' END-IF",
            "INVOKE A1 \"toString\" RETURNING S",
            "GOBACK.",
        ],
    );
    let (out, _, ending) = run_oo(&main, &[account()]);
    assert_eq!(out, "EQUAL\n");
    let abend = ending.unwrap_err();
    assert_eq!(abend.code, "JAVA");
    assert!(abend.message.contains("\"toString\"") && abend.message.contains("java.lang.Object"), "{}", abend.message);
    let string = client(&repository, &data, &["INVOKE JString NEW USING BY VALUE 'X' RETURNING S", "GOBACK."]);
    let abend = run_oo(&string, &[]).2.unwrap_err();
    assert_eq!(abend.code, "JAVA");
    assert!(abend.message.contains("NEW") && abend.message.contains("java.lang.String"), "{}", abend.message);
    let checked = client(&repository, &data, &["INVOKE S \"length\" RETURNING BAL", "GOBACK."]);
    assert_eq!(errors(&checked), "");
    let exception = class("MyError INHERITS JavaException", &["JavaException IS \"java.lang.Exception\""], "");
    let thrower = client(&["MyError"], "       01  E USAGE OBJECT REFERENCE MyError.\n", &["INVOKE MyError NEW RETURNING E", "GOBACK."]);
    let abend = run_oo(&thrower, &[exception]).2.unwrap_err();
    assert!(abend.code == "JAVA" && abend.message.contains("java.lang.Exception"), "{abend:?}");
}

#[test]
fn method_storage_persists_local_storage_does_not_and_invoke_keeps_return_code() {
    let counter = class(
        "Counter INHERITS Base",
        &["Base IS \"java.lang.Object\""],
        &part(
            "OBJECT",
            "",
            &[
                method(
                    "tick",
                    "       WORKING-STORAGE SECTION.\n       01  CALLS PIC 9(4) VALUE 0.\n       LOCAL-STORAGE SECTION.\n       01  FRESH PIC 9(4) VALUE 5.\n",
                    "",
                    &["ADD 1 TO CALLS FRESH", "MOVE 9 TO RETURN-CODE", "DISPLAY CALLS ' ' FRESH", "EXIT METHOD", "DISPLAY 'NEVER'."],
                ),
                method("halt", "", "", &["STOP RUN."]),
            ],
        ),
    );
    let main = client(
        &["Counter"],
        "       01  C1 USAGE OBJECT REFERENCE Counter.\n       01  C2 USAGE OBJECT REFERENCE Counter.\n",
        &["MOVE 3 TO RETURN-CODE", "INVOKE Counter NEW RETURNING C1", "INVOKE Counter NEW RETURNING C2", "INVOKE C1 \"tick\"", "INVOKE C2 \"tick\"", "INVOKE C1 \"halt\"", "DISPLAY 'AFTER'", "GOBACK."],
    );
    let (out, err, ending) = run_oo(&main, &[counter]);
    assert_eq!(ending, Ok((Ending::StopRun, 3)), "{err}");
    assert_eq!(out, "0001 0006\n0002 0006\n");
}

#[test]
fn jni_reference_services_run_and_the_rest_need_the_jvm() {
    let main = [
        OO_CARD,
        "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. JNIUSER RECURSIVE.\n       ENVIRONMENT DIVISION.\n       CONFIGURATION SECTION.\n       REPOSITORY.\n           CLASS Account IS \"Account\".\n",
        "       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n",
        ACCOUNT_DATA,
        "       01  FLAG PIC X.\n           88 FLAG-TRUE VALUE X'01' THRU X'FF'.\n       01  CNAME PIC X(8) VALUE Z'Account'.\n",
        "       LINKAGE SECTION.\n           COPY JNI.\n       PROCEDURE DIVISION.\n",
        &line("SET ADDRESS OF JNIENV TO JNIENVPTR"),
        &line("SET ADDRESS OF JNINATIVEINTERFACE TO JNIENV"),
        &line("INVOKE Account NEW RETURNING A1"),
        &line("CALL NewGlobalRef USING BY VALUE JNIENVPTR A1"),
        &line("    RETURNING A2"),
        &line("CALL IsSameObject USING BY VALUE JNIENVPTR A1 A2"),
        &line("    RETURNING FLAG"),
        &line("IF FLAG-TRUE DISPLAY 'SAME OBJECT' END-IF"),
        &line("IF CNAME = X'C1838396A495A300' DISPLAY 'Z LITERAL' END-IF"),
        &line("CALL FindClass USING BY VALUE JNIENVPTR CNAME"),
        &line("    RETURNING U"),
        &line("GOBACK."),
    ]
    .concat();
    let (out, _, ending) = run_oo(&main, &[account()]);
    assert_eq!(out, "SAME OBJECT\nZ LITERAL\n");
    let abend = ending.unwrap_err();
    assert!(abend.code == "JAVA" && abend.message.contains("FindClass"), "{abend:?}");
}

#[test]
fn a_class_is_found_in_the_program_libraries_and_checks_alone() {
    let dir = temp("classlib");
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("Account.cbl"), account()).unwrap();
    let main = client(&["Account IS \"Account\""], ACCOUNT_DATA, &["INVOKE Account NEW RETURNING A1", "INVOKE A1 \"getBalance\" RETURNING BAL", "MOVE BAL TO SHOWN DISPLAY SHOWN", "GOBACK."]);
    let (out, err, ending) = run_unit(&main, vec![dir], "");
    assert!(ending.is_ok(), "{ending:?}\n{err}");
    assert_eq!(out, " 100\n");
    let compiled = compile(syntax::parse(&account()).unwrap(), &[]).unwrap_or_else(|e| panic!("{e:?}"));
    let (mut out, mut err) = (Vec::new(), Vec::new());
    let refused = compiled.run(&mut out, &mut err).unwrap_err();
    assert!(refused.message.contains("class definition"), "{}", refused.message);
    let refused = compiled.execute_cics(unit::Library::default(), files::Dds::default(), task("ACCT"), unit::Clock::Fixed(0, 0), &mut out, &mut err).unwrap_err();
    assert!(refused.message.contains("class definition"), "{}", refused.message);
}

#[test]
fn what_enterprise_cobol_refuses_is_refused() {
    let data = [ACCOUNT_DATA, "       01  TEXT PIC X(10).\n       01  P POINTER.\n"].concat();
    let refused = |body: &[&str]| errors(&client(&["Account IS \"Account\""], &data, body));
    assert!(refused(&["MOVE A1 TO A2."]).contains("object reference"));
    assert!(refused(&["DISPLAY A1."]).contains("object reference"));
    assert!(refused(&["IF A1 > A2 CONTINUE END-IF."]).contains("equal or not equal"));
    assert!(refused(&["IF A1 = TEXT CONTINUE END-IF."]).contains("compares with"));
    assert!(refused(&["SET P TO A1."]).contains("its own kind"));
    assert!(refused(&["SET A1 TO P."]).contains("another object reference"));
    assert!(refused(&["INVOKE A1 \"m\" USING BY VALUE TEXT."]).contains("not a type Java shares"));
    assert!(refused(&["INVOKE A1 \"m\" USING BY VALUE 1.5."]).contains("not an argument Java takes"));
    assert!(refused(&["INVOKE A1 MNAME."]).contains("universal"));
    assert!(refused(&["INVOKE Account NEW."]).contains("RETURNING"));
    assert!(refused(&["INVOKE Account NEW RETURNING BAL."]).contains("not an object reference"));
    assert!(refused(&["INVOKE BAL \"m\"."]).contains("not an object reference"));
    assert!(refused(&["INVOKE Nowhere \"m\"."]).contains("NOWHERE"));
    assert!(refused(&["INVOKE SELF \"m\"."]).contains("only in a method"));
    assert!(refused(&["EXIT METHOD."]).contains("only in a method"));
    assert!(refused(&["EXEC CICS RETURN END-EXEC."]).contains("EXEC CICS"));
    assert!(errors(&client(&["Account IS \"Account\""], "       01  X OBJECT REFERENCE Other.\n", &["GOBACK."])).contains("REPOSITORY"));
    let bad_class = |methods: &[String], repository: &[&str]| errors(&class("Bad INHERITS Base", repository, &part("OBJECT", "       01  D PIC X.\n", methods)));
    let base = ["Base IS \"java.lang.Object\""];
    assert!(bad_class(&[method("m", "       LINKAGE SECTION.\n       01  L PIC S9(9) BINARY.\n", " USING L", &["CONTINUE."])], &base).contains("BY VALUE"));
    assert!(bad_class(&[method("m", "       LINKAGE SECTION.\n       01  L PIC X(4).\n", " USING BY VALUE L", &["CONTINUE."])], &base).contains("not a type Java shares"));
    assert!(bad_class(&[method("m", "", "", &["EXIT PROGRAM."])], &base).contains("EXIT PROGRAM"));
    assert!(bad_class(&[method("m", "", "", &["SET ADDRESS OF D TO NULL."])], &base).contains("WORKING-STORAGE"));
    assert!(bad_class(&[method("m", "", "", &["EXEC SQL COMMIT END-EXEC."])], &base).contains("EXEC"));
    assert!(bad_class(&[method("m", "", "", &["CONTINUE."]), method("m", "", "", &["CONTINUE."])], &base).contains("same parameter types"));
    assert!(bad_class(&[], &["Other IS \"x.Y\""]).contains("REPOSITORY"));
}

#[test]
fn a_report_takes_no_object_reference_as_source_or_control() {
    let report = |control: &str, source: &str| {
        [
            OO_CARD,
            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. REPORTS RECURSIVE.\n       ENVIRONMENT DIVISION.\n       CONFIGURATION SECTION.\n       REPOSITORY.\n           CLASS Account IS \"Account\".\n",
            "       INPUT-OUTPUT SECTION.\n       FILE-CONTROL.\n           SELECT P ASSIGN TO PDD.\n       DATA DIVISION.\n       FILE SECTION.\n       FD  P REPORT IS R.\n",
            "       WORKING-STORAGE SECTION.\n       01  A USAGE OBJECT REFERENCE Account.\n       01  K PIC X.\n       REPORT SECTION.\n",
            &format!("       RD  R CONTROLS {control}.\n       01  D TYPE DE LINE PLUS 1.\n           05 COLUMN 1 PIC X(8) SOURCE {source}.\n"),
            "       PROCEDURE DIVISION.\n",
            &line("GOBACK."),
        ]
        .concat()
    };
    assert_eq!(errors(&report("K", "K")), "");
    assert!(errors(&report("K", "A")).contains("A is an object reference"));
    assert!(errors(&report("A", "K")).contains("A is an object reference"));
}

#[test]
fn a_callable_service_runs_in_a_method_and_cee3dmp_names_the_method() {
    let dumper = class(
        "Dumper INHERITS Base",
        &["Base IS \"java.lang.Object\""],
        &part(
            "OBJECT",
            "",
            &[method(
                "dump",
                "       WORKING-STORAGE SECTION.\n       01  HEADING PIC X(80) VALUE 'IN A METHOD'.\n       01  OPTS PIC X(255) VALUE SPACES.\n       01  FC PIC X(12).\n",
                "",
                &["CALL 'CEE3DMP' USING HEADING OPTS FC", "IF FC = LOW-VALUES DISPLAY 'CEE000' END-IF."],
            )],
        ),
    );
    let main = client(&["Dumper"], "       01  D USAGE OBJECT REFERENCE Dumper.\n", &["INVOKE Dumper NEW RETURNING D", "INVOKE D \"dump\"", "GOBACK."]);
    let (out, err, ending) = run_oo(&main, &[dumper]);
    assert!(ending.is_ok(), "{ending:?}\n{err}");
    assert_eq!(out, "CEE000\n");
    assert!(err.ends_with("  CLIENT\n  DUMPER.dump\n"), "{err}");
}

/// Classes and a client for the front-end fuzzer to mutate.
pub(super) fn fuzz_seeds() -> Vec<String> {
    let client = client(
        &["Account IS \"Account\"", "JString IS \"java.lang.String\""],
        &[ACCOUNT_DATA, "       01  S USAGE OBJECT REFERENCE JString.\n"].concat(),
        &[
            "INVOKE Account \"open\" RETURNING A1",
            "INVOKE A1 \"credit\" USING BY VALUE 7 LENGTH OF BAL",
            "    ON EXCEPTION CONTINUE END-INVOKE",
            "SET U TO A1",
            "IF U = A1 AND A2 NOT = NULL SET A2 TO NULL END-IF",
            "INVOKE U MNAME RETURNING S",
            "    NOT ON EXCEPTION DISPLAY 'Y' END-INVOKE",
            "GOBACK.",
        ],
    );
    let mut seeds = vec![account(), client];
    seeds.extend(animals());
    seeds
}

#[test]
fn factory_methods_are_inherited_and_self_in_one_is_its_own_class() {
    let maker = class(
        "Maker INHERITS Base",
        &["Base IS \"java.lang.Object\""],
        &part(
            "FACTORY",
            "       01  BUILT PIC 9 VALUE 0.\n",
            &[
                method("kind", "", "", &["DISPLAY 'MAKER ' BUILT."]),
                method("build", "", "", &["ADD 1 TO BUILT", "INVOKE SELF \"kind\"."]),
            ],
        ),
    );
    let sub = class("SubMaker INHERITS Maker", &["Maker"], &part("FACTORY", "", &[method("kind", "", "", &["DISPLAY 'SUBMAKER'", "INVOKE SUPER \"kind\"."])]));
    let main = client(&["SubMaker", "Maker"], "", &["INVOKE SubMaker \"build\"", "INVOKE SubMaker \"kind\"", "INVOKE Maker \"build\"", "GOBACK."]);
    let (out, err, ending) = run_oo(&main, &[maker, sub]);
    assert!(ending.is_ok(), "{ending:?}\n{err}");
    assert_eq!(out, "MAKER 1\nSUBMAKER\nMAKER 1\nMAKER 2\n");
}

const ENVIRONMENT: [&str; 2] = ["SET ADDRESS OF JNIENV TO JNIENVPTR", "SET ADDRESS OF JNINATIVEINTERFACE TO JNIENV"];

/// A class that keeps an Account in its instance data: as a method receives it, makes it, or makes
/// it global; and SELF.
fn holder() -> String {
    let jni = "       LINKAGE SECTION.\n           COPY JNI.\n";
    let account = "       01  A USAGE OBJECT REFERENCE Account.\n";
    let balance = "       LINKAGE SECTION.\n       01  B PIC S9(9) BINARY.\n";
    class(
        "Holder INHERITS Base",
        &["Base IS \"java.lang.Object\"", "Account IS \"Account\"", "Holder IS \"Holder\""],
        &part(
            "OBJECT",
            "       01  KEPT USAGE OBJECT REFERENCE Account.\n       01  ME USAGE OBJECT REFERENCE Holder.\n",
            &[
                method("keep", &format!("       LINKAGE SECTION.\n{account}"), " USING BY VALUE A", &["SET KEPT TO A."]),
                method("keepGlobal", &format!("{jni}{account}"), " USING BY VALUE A", &[ENVIRONMENT[0], ENVIRONMENT[1], "CALL NewGlobalRef USING BY VALUE JNIENVPTR A", "    RETURNING KEPT."]),
                method("drop", jni, "", &[ENVIRONMENT[0], ENVIRONMENT[1], "CALL DeleteGlobalRef USING BY VALUE JNIENVPTR KEPT."]),
                method("make", "", "", &["INVOKE Account NEW RETURNING KEPT."]),
                method("remember", "", "", &["SET ME TO SELF."]),
                method("balance", balance, " RETURNING B", &["INVOKE KEPT \"getBalance\" RETURNING B."]),
                method("again", balance, " RETURNING B", &["INVOKE ME \"balance\" RETURNING B."]),
                method("kept", "       LINKAGE SECTION.\n       01  R USAGE OBJECT REFERENCE Account.\n", " RETURNING R", &["SET R TO KEPT."]),
            ],
        ),
    )
}

/// A client whose line 23 is the first statement after it makes a Holder, H, and an Account, A1.
fn holder_client(body: &[&str]) -> (String, Result<(Ending, i16), Abend>) {
    let data = [ACCOUNT_DATA, "       01  H USAGE OBJECT REFERENCE Holder.\n"].concat();
    let body = [&["INVOKE Holder NEW RETURNING H", "INVOKE Account NEW RETURNING A1"], body, &["GOBACK."]].concat();
    let main = client(&["Account IS \"Account\"", "Holder IS \"Holder\""], &data, &body);
    let (out, _, ending) = run_oo(&main, &[account(), holder()]);
    (out, ending)
}

fn abend(ending: Result<(Ending, i16), Abend>) -> String {
    let abend = ending.unwrap_err();
    assert_eq!(abend.code, "IRONWORK", "{abend:?}");
    abend.message
}

#[test]
fn a_local_reference_a_method_keeps_expires_when_the_method_returns() {
    let invoked = |m: &str| format!("method \"{m}\" of Holder, invoked at line 23 of CLIENT");
    let message = abend(holder_client(&["INVOKE H \"keep\" USING BY VALUE A1", "INVOKE H \"balance\" RETURNING BAL"]).1);
    let expected = format!(
        "INVOKE KEPT \"getBalance\": KEPT holds a local reference to an Account object, received as argument 1 by {}; it expired when {}, returned, and IBM leaves using it unpredictable (see J17)",
        invoked("keep"),
        invoked("keep")
    );
    assert_eq!(message, expected);
    let message = abend(holder_client(&["INVOKE H \"make\"", "INVOKE H \"balance\" RETURNING BAL"]).1);
    assert!(message.contains(&format!("KEPT holds a local reference to an Account object, made by INVOKE Account NEW at line 50 of Holder.make; it expired when {}, returned", invoked("make"))), "{message}");
    let message = abend(holder_client(&["INVOKE H \"remember\"", "INVOKE H \"again\" RETURNING BAL"]).1);
    assert!(message.starts_with(&format!("INVOKE ME \"balance\": ME holds a local reference to a Holder object, SELF of {}; it expired when", invoked("remember"))), "{message}");
    let message = abend(holder_client(&["INVOKE H \"keep\" USING BY VALUE A1", "INVOKE H \"kept\" RETURNING A2"]).1);
    assert!(message.starts_with("INVOKE H \"kept\": R, the RETURNING item of method \"kept\", holds a local reference"), "{message}");
}

#[test]
fn a_global_reference_lasts_until_deleted_and_a_returned_one_is_the_invokers_own() {
    let (out, ending) = holder_client(&[
        "INVOKE H \"keepGlobal\" USING BY VALUE A1",
        "INVOKE H \"balance\" RETURNING BAL",
        "MOVE BAL TO SHOWN DISPLAY SHOWN",
        "INVOKE H \"kept\" RETURNING A2",
        "IF A1 = A2 DISPLAY 'SAME OBJECT' END-IF",
        "INVOKE H \"drop\"",
        "INVOKE A2 \"getBalance\" RETURNING BAL",
        "MOVE BAL TO SHOWN DISPLAY SHOWN",
        "INVOKE H \"balance\" RETURNING BAL",
    ]);
    assert_eq!(out, " 100\nSAME OBJECT\n 100\n");
    let message = abend(ending);
    assert!(message.contains("KEPT holds a global reference to an Account object, made by NewGlobalRef at line 34 of Holder.keepGlobal; it was deleted by DeleteGlobalRef at line 45 of Holder.drop"), "{message}");
}

/// A client with the JNI's function table in its LINKAGE SECTION.
fn jni_client(body: &[&str]) -> (String, Result<(Ending, i16), Abend>) {
    let data = [ACCOUNT_DATA, "       01  B USAGE OBJECT REFERENCE Account.\n       01  N PIC S9(9) COMP-5.\n       LINKAGE SECTION.\n           COPY JNI.\n"].concat();
    let main = client(&["Account IS \"Account\""], &data, &[&ENVIRONMENT[..], body, &["GOBACK."]].concat());
    let (out, _, ending) = run_oo(&main, &[account()]);
    (out, ending)
}

#[test]
fn jni_services_make_delete_and_frame_references() {
    let (out, ending) = jni_client(&[
        "INVOKE Account NEW RETURNING A1",
        "CALL NewGlobalRef USING BY VALUE JNIENVPTR A1 RETURNING A2",
        "CALL GetObjectRefType USING BY VALUE JNIENVPTR A1 RETURNING N",
        "DISPLAY N",
        "CALL GetObjectRefType USING BY VALUE JNIENVPTR A2 RETURNING N",
        "DISPLAY N",
        "CALL GetObjectRefType USING BY VALUE JNIENVPTR U RETURNING N",
        "DISPLAY N",
        "IF A1 = A2 DISPLAY 'ONE OBJECT' END-IF",
        "CALL NewLocalRef USING BY VALUE JNIENVPTR A2 RETURNING U",
        "CALL DeleteGlobalRef USING BY VALUE JNIENVPTR A2",
        "INVOKE U \"getBalance\" RETURNING BAL",
        "CALL DeleteLocalRef USING BY VALUE JNIENVPTR A1",
        "IF A1 NOT = NULL DISPLAY 'NOT NULL' END-IF",
        "CALL PushLocalFrame USING BY VALUE JNIENVPTR 4 RETURNING N",
        "INVOKE Account NEW RETURNING A1",
        "INVOKE Account \"open\" RETURNING B",
        "CALL PopLocalFrame USING BY VALUE JNIENVPTR A1 RETURNING A1",
        "INVOKE A1 \"getBalance\" RETURNING BAL",
        "MOVE BAL TO SHOWN DISPLAY SHOWN",
        "INVOKE B \"getBalance\" RETURNING BAL",
    ]);
    assert_eq!(out, "000000000A\n000000000B\n000000000{\nONE OBJECT\nNOT NULL\n 100\n");
    let message = abend(ending);
    assert!(
        message.starts_with("INVOKE B \"getBalance\": B holds a local reference to an Account object, returned by method \"open\" of Account, invoked at line 41 of CLIENT; it expired when PopLocalFrame at line 42 of CLIENT freed its frame"),
        "{message}"
    );
    let deleted = abend(jni_client(&["INVOKE Account NEW RETURNING A1", "SET A2 TO A1", "CALL DeleteLocalRef USING BY VALUE JNIENVPTR A1", "IF A2 = A1 CONTINUE END-IF"]).1);
    assert!(deleted.starts_with("A2 = A1: A2 holds a local reference to an Account object, made by INVOKE Account NEW at line 25 of CLIENT; it was deleted by DeleteLocalRef at line 27 of CLIENT"), "{deleted}");
    let wrong_kind = abend(jni_client(&["INVOKE Account NEW RETURNING A1", "CALL NewGlobalRef USING BY VALUE JNIENVPTR A1 RETURNING A2", "CALL DeleteLocalRef USING BY VALUE JNIENVPTR A2"]).1);
    assert!(wrong_kind.contains("A2 holds a global reference, which DeleteLocalRef does not delete"), "{wrong_kind}");
    let unpushed = abend(jni_client(&["CALL PopLocalFrame USING BY VALUE JNIENVPTR U RETURNING U"]).1);
    assert!(unpushed.contains("no frame PushLocalFrame pushed is open"), "{unpushed}");
}

#[test]
fn a_program_that_is_not_a_method_makes_its_references_in_the_running_methods_frame() {
    let maker = "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. MAKER RECURSIVE.\n       ENVIRONMENT DIVISION.\n       CONFIGURATION SECTION.\n       REPOSITORY.\n           CLASS Account IS \"Account\".\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n       01  M USAGE OBJECT REFERENCE Account.\n       01  B PIC S9(9) BINARY.\n       01  SHOWN PIC ZZZ9.\n       PROCEDURE DIVISION.\n";
    let maker = [
        maker,
        &line("IF M = NULL INVOKE Account NEW RETURNING M DISPLAY 'MADE'"),
        &line("ELSE INVOKE M \"getBalance\" RETURNING B MOVE B TO SHOWN"),
        &line("    DISPLAY 'USED ' SHOWN END-IF"),
        &line("GOBACK."),
        "       END PROGRAM MAKER.\n",
    ]
    .concat();
    let caller = class("Caller INHERITS Base", &["Base IS \"java.lang.Object\""], &part("OBJECT", "", &[method("call", "", "", &["CALL 'MAKER'."])]));
    let main = |body: &[&str]| {
        let data = "       01  C USAGE OBJECT REFERENCE Caller.\n";
        [client(&["Caller"], data, &[body, &["GOBACK."]].concat()), "       END PROGRAM CLIENT.\n".into(), maker.clone()].concat()
    };
    let (out, err, ending) = run_oo(&main(&["CALL 'MAKER'", "CALL 'MAKER'"]), &[account(), caller.clone()]);
    assert!(ending.is_ok(), "{ending:?}\n{err}");
    assert_eq!(out, "MADE\nUSED  100\n");
    let (out, _, ending) = run_oo(&main(&["INVOKE Caller NEW RETURNING C", "INVOKE C \"call\"", "CALL 'MAKER'"]), &[account(), caller]);
    assert_eq!(out, "MADE\n");
    let message = abend(ending);
    assert!(
        message.starts_with("INVOKE M \"getBalance\": M holds a local reference to an Account object, made by INVOKE Account NEW at line 29 of MAKER; it expired when method \"call\" of CALLER, invoked at line 13 of CLIENT, returned"),
        "{message}"
    );
}

#[test]
fn object_oriented_programs_need_the_options_ibm_compiles_them_with() {
    let with = |card: &str, id: &str| errors(&client(&["Account IS \"Account\""], ACCOUNT_DATA, &["INVOKE Account NEW RETURNING A1", "GOBACK."]).replacen(OO_CARD, card, 1).replacen("CLIENT RECURSIVE", id, 1));
    let needs = |missing: &str| format!("warning: program CLIENT uses object-oriented syntax, which IBM compiles only with THREAD, DLL, RENT and DBCS: {missing} missing from its CBL or PROCESS cards (see J13 and J19)");
    assert_eq!(with(OO_CARD, "CLIENT RECURSIVE"), "");
    assert_eq!(with("", "CLIENT"), needs("THREAD, DLL"));
    assert_eq!(with("       CBL THREAD\n", "CLIENT RECURSIVE"), needs("DLL"));
    assert_eq!(with("       PROCESS DLL\n", "CLIENT"), needs("THREAD"));
    assert_eq!(with("       CBL THREAD,DLL,NODBCS\n", "CLIENT RECURSIVE"), needs("DBCS"));
    assert_eq!(with("       CBL NORENT\n", "CLIENT"), needs("THREAD, DLL, RENT"));
    assert!(with("       CBL THREAD,DLL,NORENT\n", "CLIENT RECURSIVE").starts_with("warning: NORENT conflicts with THREAD and DLL, which IBM compiles only as RENT"));
    assert_eq!(with(OO_CARD, "CLIENT"), "program CLIENT is compiled with THREAD, which requires RECURSIVE in its PROGRAM-ID paragraph");
    assert_eq!(with(OO_CARD, "CLIENT RECURSIVE INITIAL"), "program CLIENT is INITIAL, which THREAD does not allow");
    let nested = [client(&["Account IS \"Account\""], ACCOUNT_DATA, &["GOBACK."]), "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. INNER.\n       PROCEDURE DIVISION.\n           GOBACK.\n       END PROGRAM INNER.\n       END PROGRAM CLIENT.\n".into()].concat();
    assert_eq!(errors(&nested), "program CLIENT contains program INNER, and THREAD does not allow nested programs");
    let bare = account().replacen(OO_CARD, "", 1);
    assert!(errors(&bare).starts_with("warning: class ACCOUNT uses object-oriented syntax, which IBM compiles only with THREAD, DLL, RENT and DBCS: THREAD, DLL missing"), "{}", errors(&bare));
}

#[test]
fn a_program_and_class_without_the_options_run_with_a_warning_unless_warnings_block() {
    let program = client(&["Account IS \"Account\""], ACCOUNT_DATA, &["INVOKE Account NEW RETURNING A1", "INVOKE A1 \"getBalance\" RETURNING BAL", "MOVE BAL TO SHOWN DISPLAY SHOWN", "GOBACK."]);
    let bare = program.replacen(OO_CARD, "", 1).replacen("CLIENT RECURSIVE", "CLIENT", 1);
    let compiled = compile(syntax::parse(&bare).unwrap(), &[]).unwrap_or_else(|e| panic!("{e:?}"));
    assert_eq!(compiled.diagnostics.iter().map(|d| (d.severity, syntax::return_code(std::slice::from_ref(d)))).collect::<Vec<_>>(), [(syntax::Severity::Warning, 4)]);
    let bare_class = account().replacen(OO_CARD, "", 1);
    let (out, err, ending) = run_oo(&bare, std::slice::from_ref(&bare_class));
    assert!(ending.is_ok(), "{ending:?}\n{err}");
    assert_eq!(out, " 100\n");
    let refused = compile(syntax::parse(&bare).unwrap(), &["-warnings-block".into()]).err().expect("refused");
    assert_eq!(refused.iter().map(|d| d.severity).collect::<Vec<_>>(), [syntax::Severity::Warning]);
    let blocked = Harness::source(&program).classes(&[bare_class]).flags(&["-warnings-block"]).run(Executor::Interpreter);
    let message = blocked.ending.unwrap_err().message;
    assert!(message.starts_with("class Account does not compile: Account: warning: class ACCOUNT uses object-oriented syntax") && message.contains("THREAD, DLL missing"), "{message}");
}

#[test]
fn thread_needs_recursive_and_refuses_initial_and_file_sorts() {
    let t = |card: &str, head: &str, body: &[&str]| {
        let text = [
            card,
            &format!("       IDENTIFICATION DIVISION.\n       PROGRAM-ID. {head}.\n       ENVIRONMENT DIVISION.\n       INPUT-OUTPUT SECTION.\n       FILE-CONTROL.\n"),
            "           SELECT S ASSIGN TO SORTWK1.\n           SELECT F ASSIGN TO FDD.\n           SELECT G ASSIGN TO GDD.\n       DATA DIVISION.\n       FILE SECTION.\n",
            "       SD  S.\n       01  S-REC PIC X(4).\n       FD  F.\n       01  F-REC PIC X(4).\n       FD  G.\n       01  G-REC PIC X(4).\n",
            "       WORKING-STORAGE SECTION.\n       01  T.\n           05 E PIC X OCCURS 3.\n       PROCEDURE DIVISION.\n",
            &body.iter().map(|l| line(l)).collect::<String>(),
            &line("GOBACK."),
        ]
        .concat();
        errors(&text)
    };
    let sort = ["SORT S ON ASCENDING KEY S-REC", "    USING F GIVING G"];
    let merge = ["MERGE S ON ASCENDING KEY S-REC", "    USING F G GIVING G"];
    assert_eq!(t("", "T", &sort), "");
    assert_eq!(t("", "T IS INITIAL", &merge), "");
    assert_eq!(t("       CBL THREAD\n", "T", &[]), "program T is compiled with THREAD, which requires RECURSIVE in its PROGRAM-ID paragraph");
    assert_eq!(t("       CBL THREAD\n", "T RECURSIVE INITIAL", &[]), "program T is INITIAL, which THREAD does not allow");
    assert_eq!(t("       CBL THREAD\n", "T RECURSIVE", &["SORT E ON ASCENDING KEY E"]), "");
    assert_eq!(t("       CBL THREAD\n", "T RECURSIVE", &sort), "SORT of a file is not allowed in a program compiled with THREAD");
    assert_eq!(t("       CBL THREAD\n", "T RECURSIVE", &merge), "MERGE is not allowed in a program compiled with THREAD");
}

#[test]
fn a_program_that_reaches_java_only_through_the_jni_needs_no_thread_or_dll() {
    let text = [
        "       CBL LIST,MAP,XREF\n       IDENTIFICATION DIVISION.\n       PROGRAM-ID. BRIDGE.\n       ENVIRONMENT DIVISION.\n       CONFIGURATION SECTION.\n       REPOSITORY.\n           CLASS Hist IS \"com.acme.Hist\".\n",
        "       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n       01  CLASS-REF PIC 9(18) COMP-5 VALUE 0.\n       01  CNAME PIC X(14) VALUE Z'com/acme/Hist'.\n       LINKAGE SECTION.\n           COPY JNI.\n       PROCEDURE DIVISION.\n",
        &line(ENVIRONMENT[0]),
        &line(ENVIRONMENT[1]),
        &line("CALL FindClass USING BY VALUE JNIENVPTR ADDRESS OF CNAME"),
        &line("    RETURNING CLASS-REF"),
        &line("GOBACK."),
    ]
    .concat();
    assert_eq!(errors(&text), "");
}