zerodds-lang-rules 1.0.0-rc.7.1

Maps the common AST onto the language CSTs through rules that are data, not code: seesaw v2 rule specifications, comparable and checkable.
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
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 ZeroDDS Contributors
//! The Java mapping of DDS-Java-PSM 1.0 Clause 9, beside the IDL-to-Java
//! mapping.
//!
//! ยง9.1.1: "DDS aggregation types shall be mapped to a final Java class.
//! Contained attributes shall be encapsulated. Java Bean style accessors
//! shall be provided. [...] The representation of internal state shall be
//! private." ยง9.1.2: "Unbounded DDS sequences are mapped to Collection<E>
//! interface. The state is encapsulated and getters/setters are provided
//! through bean style property accessors. Bounded sequences and arrays are
//! mapped to Java arrays."
//!
//! The expectations below are those two clauses, written out for one IDL
//! file; the generated text is checked for what the clauses say and for
//! what must not have moved under the default mapping.
//!
//! `print_stderr` is allowed because a skipped compiler check has to be
//! said, and standard error is the channel a passing test does not
//! otherwise use.
#![allow(
    clippy::expect_used,
    clippy::panic,
    clippy::print_stderr,
    clippy::unwrap_used
)]

use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::LazyLock;

use zerodds_ast::Ast;
use zerodds_ast_layout::key::{Encoding, Endianness, LayoutKey};
use zerodds_lang_cst::Language;
use zerodds_lang_rules::adapter::interface::ServiceMapping;
use zerodds_lang_rules::adapter::java::{JavaMapping, mapping, with_mapping};

#[path = "support/corpus_frontend.rs"]
mod corpus_frontend;
#[path = "support/corpus.rs"]
mod corpus_harness;

const IDL: &str = "
module demo {
  enum Color { RED, GREEN };
  struct Point { long x; long y; @optional long z; boolean on; };
  union Shape switch (long) {
    case 1: Point p;
    case 2: string name;
    default: long other;
  };
  struct Track {
    @key string id;
    Point center;
    Point vicinity[2];
    sequence<long> samples;
    sequence<long, 4> bounded;
    sequence<Point> points;
    sequence<sequence<string> > names;
    sequence<long> rows[3];
    map<string, long> table;
    Color tint;
    Shape form;
    @optional string note;
  };
};
";

fn model(idl: &str) -> Ast {
    let parsed = zerodds_idl_parse::parse(idl);
    assert!(
        parsed.diagnostics().is_empty(),
        "{:?}",
        parsed.diagnostics()
    );
    let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
    assert!(lowered.diagnostics.is_empty(), "{:?}", lowered.diagnostics);
    let mut ast = lowered.ast;
    ast.resolve_all_by_name().expect("resolve");
    ast
}

fn key() -> LayoutKey {
    LayoutKey::data(Encoding::PlainCdr2, Endianness::Little)
}

/// The generated Java text, types and codec, under `selected`.
fn generated(idl: &str, selected: JavaMapping) -> Result<String, String> {
    let ast = model(idl);
    with_mapping(selected, || {
        zerodds_lang_rules::generate::source(&ast, Language::Java, key())
            .map_err(|error| error.to_string())
    })
}

fn psm() -> String {
    generated(IDL, JavaMapping::Psm).expect("the Psm mapping generates")
}

fn omg() -> String {
    generated(IDL, JavaMapping::Omg).expect("the Omg mapping generates")
}

#[test]
fn psm_is_the_default_and_the_option_is_scoped() {
    // `idlc` and a library caller that names no mapping get Clause 9.
    assert_eq!(JavaMapping::default(), JavaMapping::Psm);
    assert_eq!(mapping(), JavaMapping::Psm);
    with_mapping(JavaMapping::Omg, || {
        assert_eq!(mapping(), JavaMapping::Omg);
        // Nested scopes restore what they replaced.
        with_mapping(JavaMapping::Psm, || assert_eq!(mapping(), JavaMapping::Psm));
        assert_eq!(mapping(), JavaMapping::Omg);
    });
    assert_eq!(mapping(), JavaMapping::Psm);
    // Also when the body panics.
    let result = std::panic::catch_unwind(|| {
        with_mapping(JavaMapping::Omg, || panic!("inside"));
    });
    assert!(result.is_err());
    assert_eq!(mapping(), JavaMapping::Psm);
    assert_eq!(JavaMapping::from_name("psm"), Some(JavaMapping::Psm));
    assert_eq!(JavaMapping::from_name("omg"), Some(JavaMapping::Omg));
    assert_eq!(JavaMapping::from_name("jacorb"), None);
    // Without a named mapping a library caller gets the text of Clause 9.
    assert_eq!(
        psm(),
        zerodds_lang_rules::generate::source(&model(IDL), Language::Java, key())
            .expect("generates")
    );
}

#[test]
fn the_idl_to_java_mapping_is_what_it_was() {
    let text = omg();
    for expected in [
        "public int x;",
        "public java.lang.Integer z;",
        "public boolean on;",
        "public int[] samples;",
        "public int[] bounded;",
        "public ZdTypes.demo.Point[] points;",
        "public java.lang.String[][] names;",
        "zd_enc_p_long(w, v.x);",
        "w.writeU32(v.length);",
        "v.x = ",
    ] {
        assert!(text.contains(expected), "missing `{expected}`");
    }
    for forbidden in ["getX", "setX", "Collection<", "Sequences."] {
        assert!(
            !text.contains(forbidden),
            "`{forbidden}` in the IDL-to-Java mapping"
        );
    }
}

/// ยง9.1.1.
#[test]
fn an_aggregate_is_a_final_class_with_private_state_and_bean_accessors() {
    let text = psm();
    for expected in [
        "public static final class Point {",
        "private int x;",
        "private int y;",
        "private java.lang.Integer z;",
        "public Point() {",
        "public int getX() {",
        "public void setX(int v) {",
        "public java.lang.Integer getZ() {",
        "public void setZ(java.lang.Integer v) {",
        // The special rule for boolean properties ยง9.1.1 allows.
        "public boolean isOn() {",
        "public boolean getOn() {",
        "public void setOn(boolean v) {",
        "public static final class Track {",
        "private java.lang.String id;",
    ] {
        assert!(text.contains(expected), "missing `{expected}`");
    }
    assert!(!text.contains("public int x;"));
    assert!(!text.contains("public java.lang.String id;"));
}

/// ยง9.1.2.
#[test]
fn an_unbounded_sequence_is_a_collection_and_a_bounded_one_an_array() {
    let text = psm();
    for expected in [
        "private java.util.Collection<java.lang.Integer> samples;",
        "private java.util.Collection<ZdTypes.demo.Point> points;",
        "private java.util.Collection<java.util.Collection<java.lang.String>> names;",
        "public java.util.Collection<java.lang.Integer> getSamples() {",
        "public void setSamples(java.util.Collection<java.lang.Integer> v) {",
        // Bounded sequences and arrays: Java arrays.
        "private int[] bounded;",
        "private ZdTypes.demo.Point[] vicinity;",
        // An array of unbounded sequences: an array of Collection<E>.
        "private java.util.Collection<java.lang.Integer>[] rows;",
        // A map is a java.util.Map (XTypes 1.3 ยง7.5.1.3.5), not a sequence.
        "private ZdTypes.ZdMap_string_long table;",
    ] {
        assert!(text.contains(expected), "missing `{expected}`");
    }
}

#[test]
fn a_union_keeps_its_discriminator_as_the_property_discriminator() {
    let text = psm();
    assert!(text.contains("private int _d;"), "the state is private");
    assert!(text.contains("public int getDiscriminator() {"));
    assert!(text.contains("public void setDiscriminator(int v) {"));
    assert!(text.contains("v.setDiscriminator("));
    assert!(text.contains("v.getDiscriminator() == 1"));
}

#[test]
fn the_codec_reads_and_writes_through_the_accessors() {
    let text = psm();
    for expected in [
        "zd_enc_p_long(w, v.getX());",
        "w.writeBool(v.getZ() != null);",
        "v.setX(zd_dec_p_long(r, TryConstruct.DISCARD));",
        "v.setSamples(zd_dec_seq_long(r, TryConstruct.DISCARD));",
        "v.setZ(null);",
        // The sequence routines are the same walk for arrays and Collections.
        "public static void zd_enc_seq_long(Writer w, java.util.Collection<java.lang.Integer> v) {",
        "w.writeU32(Sequences.count(v));",
        "zd_enc_p_long(w, Sequences.at(v, i));",
        "v = Sequences.alloc(v, (int) zd_n);",
        "Sequences.put(v, zd_i, zd_dec_p_long(r, TryConstruct.DISCARD));",
        "return new java.util.ArrayList<java.lang.Integer>();",
        // A bounded sequence is trimmed by the same helper.
        "v = Sequences.first(v, 4);",
        // `new Collection<Integer>[3]` is not Java.
        "Sequences.newArray(java.util.Collection.class, 3)",
    ] {
        assert!(text.contains(expected), "missing `{expected}`");
    }
    // No direct field access to a member of an aggregate is left (the
    // entry of a map is the one holder with public fields).
    for forbidden in ["v.x;", "v.x =", "v.id)", "v.samples", "v.points"] {
        assert!(!text.contains(forbidden), "`{forbidden}` in the Psm codec");
    }
}

/// A case member named `discriminator` would collide with the PSM's
/// `getDiscriminator` accessor, but it never reaches the generator from
/// IDL: DDS-XTypes 1.3 ยง7.2.2.4.4.3, "The name of the discriminator member
/// is always 'discriminator'; that name is reserved for union types and is
/// not permitted for type-specific union members", and the front end
/// refuses it in either case (IDL 4.2 ยง7.2.3.1 compares identifiers without
/// regard to case).
#[test]
fn a_member_named_discriminator_is_refused_before_the_generator() {
    for idl in [
        "union U switch (long) { case 1: long discriminator; };",
        "union U switch (long) { case 1: long Discriminator; };",
    ] {
        let parsed = zerodds_idl_parse::parse(idl);
        assert!(
            parsed.diagnostics().is_empty(),
            "{:?}",
            parsed.diagnostics()
        );
        let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
        assert!(
            lowered
                .diagnostics
                .iter()
                .any(|d| d.message.contains("cannot be named `discriminator`")),
            "{idl}: {:?}",
            lowered.diagnostics
        );
    }
}

// -- the DDS-RPC binding under Clause 9 ------------------------------------

/// Every shape an operation takes: an exception, an enumeration, a
/// structure with an unbounded sequence and an `@optional` member, a base
/// interface, attributes, `out` and `inout` parameters, a sequence
/// parameter and a sequence result.
const RICH: &str = "
module robot {
  exception Overflow { long code; string why; };
  enum Mode { SLOW, FAST };
  struct Pose { long x; long y; sequence<long> trail; @optional string tag; Mode drive; };
  typedef sequence<Pose> Poses;
  interface Base { long ping(); };
  interface Rich : Base {
    readonly attribute long total;
    attribute string label;
    long add(in long a, in long b) raises (Overflow);
    long divmod(in long a, in long b, out long remainder);
    Pose locate(in string room);
    Poses route(in Pose from, in sequence<long> via, inout Mode drive);
    void reset();
    double scale(in double value);
  };
};
";

/// The generated Java of `idl` under `selected`, for the service mapping
/// `service`.
fn generated_service(
    idl: &str,
    selected: JavaMapping,
    service: ServiceMapping,
) -> Result<String, String> {
    let ast = model(idl);
    with_mapping(selected, || {
        zerodds_lang_rules::generate::source_with_mapping(
            &ast,
            Language::Java,
            zerodds_lang_rules::emit::interface_rust::rpc_layout_key(),
            zerodds_lang_rules::emit::encode::CodecOptions::default(),
            service,
        )
        .map_err(|error| error.to_string())
    })
}

/// The generated units through `javac -Xlint:all -Werror` with the RPC
/// runtime and the DDS Java PSM it is written over.
fn rpc_compiles(text: &str, tag: &str) -> Result<(), String> {
    let root = corpus_harness::repository_root();
    let dir = std::env::temp_dir().join(format!("zd-java-psm-rpc-{}-{tag}", std::process::id()));
    let classes = dir.join("classes");
    std::fs::create_dir_all(&classes).map_err(|error| error.to_string())?;
    let mut files = Vec::new();
    for unit in zerodds_lang_rules::generate::java_units(text) {
        let body = if unit.class == "ZdCodec" {
            format!("import org.zerodds.wire.*;\n\n{}", unit.text)
        } else {
            unit.text.clone()
        };
        let file = dir.join(format!("{}.java", unit.class));
        std::fs::write(&file, body).map_err(|error| error.to_string())?;
        files.push(file);
    }
    for package in [
        "runtime/java/src/main/java/org/omg/dds/rpc",
        "runtime/java/src/main/java/org/zerodds/ddsrpc",
    ] {
        for entry in std::fs::read_dir(root.join(package)).map_err(|error| error.to_string())? {
            let path = entry.map_err(|error| error.to_string())?.path();
            if path.extension().is_some_and(|ext| ext == "java") {
                files.push(path);
            }
        }
    }
    let output = Command::new("javac")
        .args(["-encoding", "UTF-8", "-Xlint:all,-unchecked", "-Werror"])
        .arg("-d")
        .arg(&classes)
        .arg("-sourcepath")
        .arg(format!(
            "{}:{}:{}",
            root.join("runtime/java/src/main/java").display(),
            root.join("crates/java-omgdds/java/src/main/java").display(),
            dir.display()
        ))
        .args(&files)
        .output()
        .map_err(|error| error.to_string())?;
    let result = if output.status.success() {
        Ok(())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).into_owned())
    };
    let _ = std::fs::remove_dir_all(&dir);
    result
}

/// DDS-RPC 1.0 ยง7.11.2.3 under DDS-Java-PSM 1.0 Clause 9: the interface of
/// the model reaches the seam. The holders of the binding (`Request`,
/// `Reply`, `In`, `Out`, `Result`) keep their public fields and take the
/// types of the mapping in force; the data types a call carries are the
/// encapsulated classes of Clause 9, and the codec reaches them through
/// their accessors.
#[test]
fn an_interface_is_generated_under_the_psm_mapping() {
    for service in [ServiceMapping::Basic, ServiceMapping::Enhanced] {
        let psm = generated_service(RICH, JavaMapping::Psm, service)
            .expect("the Psm mapping takes an interface");
        let omg = generated_service(RICH, JavaMapping::Omg, service)
            .expect("the Omg mapping takes an interface");
        // The user types are Clause 9's: private members and accessors.
        assert!(
            psm.contains("public int getCode() {"),
            "{service:?}: the exception is encapsulated"
        );
        assert!(
            !omg.contains("getCode"),
            "{service:?}: the default mapping is unchanged"
        );
        // The operations carry the types of Clause 9: an unbounded sequence
        // is a `Collection<E>`.
        assert!(
            psm.contains("java.util.Collection<java.lang.Integer> via"),
            "{service:?}: a sequence parameter is a Collection"
        );
        assert!(
            omg.contains("int[] via"),
            "{service:?}: and an array under the default mapping"
        );
        // The service holds the exception through its accessors.
        if service == ServiceMapping::Basic {
            assert!(
                psm.contains("value.getCode()"),
                "the exception codec reads through getCode()"
            );
            assert!(
                psm.contains("value.setCode("),
                "and writes through setCode()"
            );
            assert!(
                omg.contains("value.code"),
                "the default mapping reads the field"
            );
        }
    }
}

/// The Clause 9 interface is Java: it compiles with `-Xlint:all -Werror`
/// against the RPC runtime, Basic and Enhanced.
#[test]
fn an_interface_under_the_psm_mapping_compiles() {
    if !javac_available() {
        eprintln!("SKIPPED: `javac` is not usable.");
        return;
    }
    for (tag, service) in [
        ("basic", ServiceMapping::Basic),
        ("enhanced", ServiceMapping::Enhanced),
    ] {
        let text = generated_service(RICH, JavaMapping::Psm, service).expect("generates");
        rpc_compiles(&text, &format!("rich-{tag}")).unwrap_or_else(|why| panic!("{tag}: {why}"));
    }
}

/// A service defined by a pair of types (ยง7.3.1.4) under Clause 9: the
/// `header` member is the runtime's class and is reached through its
/// accessors.
#[test]
fn a_type_pair_under_the_psm_mapping_compiles() {
    let idl = "
module dds {
  typedef octet GuidPrefix_t[12];
  struct EntityId_t { octet entityKey[3]; octet entityKind; };
  struct GUID_t { GuidPrefix_t guidPrefix; EntityId_t entityId; };
  struct SequenceNumber_t { long high; unsigned long low; };
  struct SampleIdentity { GUID_t writer_guid; SequenceNumber_t sequence_number; };
  module rpc {
    enum RemoteExceptionCode_t { REMOTE_EX_OK, REMOTE_EX_UNSUPPORTED,
      REMOTE_EX_INVALID_ARGUMENT, REMOTE_EX_OUT_OF_RESOURCES,
      REMOTE_EX_UNKNOWN_OPERATION, REMOTE_EX_UNKNOWN_EXCEPTION };
    struct RequestHeader { SampleIdentity requestId; string<255> instanceName; };
    struct ReplyHeader { SampleIdentity relatedRequestId; RemoteExceptionCode_t remoteEx; };
  };
};
module pairs {
  @RPCRequestType @DDSRequestTopic(name=\"MoveRequests\") @service(name=\"Arm\")
  struct Req { dds::rpc::RequestHeader header; long x; double speed; string label; };
  @RPCReplyType @DDSReplyTopic(name=\"MoveReplies\")
  struct Rep { dds::rpc::ReplyHeader header; long y; sequence<long> trail; };
};
";
    let text = generated_service(idl, JavaMapping::Psm, ServiceMapping::Basic).expect("generates");
    assert!(
        text.contains("value.getHeader()"),
        "the codec reads the header through getHeader()"
    );
    assert!(
        text.contains("value.setHeader("),
        "and writes it through setHeader()"
    );
    assert!(
        !text.contains("ZdTypes.dds.rpc.RequestHeader"),
        "the header is the runtime's class, not a declaration of the model"
    );
    if !javac_available() {
        eprintln!("SKIPPED: `javac` is not usable.");
        return;
    }
    rpc_compiles(&text, "pair").unwrap_or_else(|why| panic!("{why}"));
}

#[test]
fn the_psm_mapping_changes_nothing_but_the_shape() {
    // The same wire walk: every encoder and decoder of the default mapping
    // has its counterpart, with the same name, under Clause 9.
    let routine = |text: &str| -> Vec<String> {
        let mut names: Vec<String> = text
            .lines()
            .filter_map(|line| {
                let line = line.trim();
                let rest = line
                    .strip_prefix("public static ")
                    .or_else(|| line.strip_prefix("private static "))?;
                let name = rest.split('(').next()?.rsplit(' ').next()?;
                name.starts_with("zd_").then(|| name.to_string())
            })
            .collect();
        names.sort();
        names
    };
    assert_eq!(routine(&omg()), routine(&psm()));
}

// -- the compiler --------------------------------------------------------

fn javac_available() -> bool {
    Command::new("javac")
        .arg("-version")
        .output()
        .is_ok_and(|output| output.status.success())
}

/// The compiled wire runtime, built once: `Sequences` is part of what the
/// Clause 9 codec means, so the whole of `org/zerodds/wire` is on the
/// classpath.
static RUNTIME: LazyLock<Result<PathBuf, String>> = LazyLock::new(|| {
    let classes = std::env::temp_dir().join(format!("zd-java-psm-runtime-{}", std::process::id()));
    std::fs::create_dir_all(&classes).map_err(|error| error.to_string())?;
    let wire =
        corpus_harness::repository_root().join("runtime/java/src/main/java/org/zerodds/wire");
    let mut sources: Vec<PathBuf> = std::fs::read_dir(&wire)
        .map_err(|error| error.to_string())?
        .filter_map(|entry| entry.ok().map(|entry| entry.path()))
        .filter(|path| path.extension().is_some_and(|ext| ext == "java"))
        .collect();
    sources.sort();
    let output = Command::new("javac")
        .args(["-encoding", "UTF-8"])
        .arg("-d")
        .arg(&classes)
        .args(&sources)
        .output()
        .map_err(|error| error.to_string())?;
    if output.status.success() {
        Ok(classes)
    } else {
        Err(String::from_utf8_lossy(&output.stderr).into_owned())
    }
});

/// The text `generate::source` writes as the two compilation units
/// `idlc generate` writes. `idlc` puts a header and a preamble (the
/// `org.zerodds.wire` import and the `ZdEndian` holder) ahead of the
/// body; `generate::source` is the body alone: the declarations, then
/// the codec. The declarations take no preamble, the codec takes it.
fn halves(text: &str) -> (String, String) {
    let codec_at = text.find("public class ZdCodec {").expect("a codec");
    let preamble = "import org.zerodds.wire.*;\n\
                    final class ZdEndian { static final Endianness VALUE = Endianness.LITTLE; }\n";
    (
        format!("{}\n", text[..codec_at].trim_end()),
        format!("{preamble}{}\n", text[codec_at..].trim_end()),
    )
}

fn compiles(runtime: &Path, text: &str, tag: &str) -> Result<(), String> {
    let dir = std::env::temp_dir().join(format!("zd-java-psm-{}-{tag}", std::process::id()));
    let classes = dir.join("classes");
    std::fs::create_dir_all(&classes).map_err(|error| error.to_string())?;
    let (types, codec) = halves(text);
    std::fs::write(dir.join("ZdTypes.java"), types).map_err(|error| error.to_string())?;
    std::fs::write(dir.join("ZdCodec.java"), codec).map_err(|error| error.to_string())?;
    let output = Command::new("javac")
        .args(["-encoding", "UTF-8", "-Xlint:unchecked,rawtypes,cast"])
        .arg("-cp")
        .arg(runtime)
        .arg("-d")
        .arg(&classes)
        .arg(dir.join("ZdTypes.java"))
        .arg(dir.join("ZdCodec.java"))
        .output()
        .map_err(|error| error.to_string())?;
    let result = if output.status.success() && output.stderr.is_empty() {
        Ok(())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).into_owned())
    };
    let _ = std::fs::remove_dir_all(&dir);
    result
}

/// The Clause 9 types and their codec are Java: `javac` takes them with
/// no error and no warning of the kinds generics bring (unchecked, raw
/// type, redundant cast).
#[test]
fn the_psm_types_and_codec_compile_without_a_warning() {
    if !javac_available() {
        eprintln!("SKIPPED: `javac` is not usable.");
        return;
    }
    let runtime = RUNTIME.clone().expect("runtime/java compiles");
    compiles(&runtime, &psm(), "unit").unwrap_or_else(|why| panic!("{why}"));
    compiles(&runtime, &omg(), "unit-omg").unwrap_or_else(|why| panic!("{why}"));
}

/// The cases the Java column itself refuses, as `java_corpus_compiles`
/// lists them.
const REFUSED: &[&str] = &["resolve-module-named-as-type"];

/// How many corpus cases have to reach `javac` for this gate to mean
/// anything. 416 render today.
const MUST_RENDER: usize = 350;

/// The whole corpus under the Clause 9 mapping, through `javac`.
///
/// `#[ignore]`, as `java_corpus_compiles` is: one compiler start per case.
#[test]
#[ignore = "one `javac` start per corpus case; run with --include-ignored"]
fn every_corpus_case_compiles_under_the_psm_mapping() {
    if !javac_available() {
        eprintln!("SKIPPED: `javac` is not usable.");
        return;
    }
    let runtime = RUNTIME.clone().expect("runtime/java compiles");
    let mut failures = Vec::new();
    let mut checked = 0_usize;
    for (name, path) in corpus_harness::corpus() {
        if REFUSED.contains(&name.as_str()) {
            continue;
        }
        let source = std::fs::read_to_string(&path).expect("a corpus source");
        if !corpus_frontend::accepts(&source, &path.display().to_string()) {
            continue;
        }
        let text = match generated(&source, JavaMapping::Psm) {
            Ok(text) => text,
            Err(why) => {
                failures.push(format!("{name}: {why}"));
                continue;
            }
        };
        checked += 1;
        if let Err(why) = compiles(&runtime, &text, &name) {
            failures.push(format!("{name}: {why}"));
        }
    }
    assert!(
        failures.is_empty(),
        "{} of {checked} did not compile:\n{failures:#?}",
        failures.len()
    );
    assert!(
        checked >= MUST_RENDER,
        "only {checked} corpus cases reached `javac`"
    );
}

/// CORBA 3.3 part 3 ยง1.5.2: `octet` is a Java `byte`. An octet constant
/// above 127 is the byte with its bits, written as the hex literal cast to
/// `byte`; the octet constants of XTypes 1.3 Annex B are such (`EK_MINIMAL
/// = 0xF1`). One below 128 keeps its decimal form, through a typedef too.
#[test]
fn an_octet_constant_above_127_is_its_bit_pattern_as_a_byte() {
    const OCTETS: &str = "
module M {
  typedef octet Kind;
  const octet EK_MINIMAL = 0xF1;
  const Kind EK_BOTH = 0xF3;
  const octet SMALL = 0x10;
  @final struct S { octet k; };
};
";
    for mapping in [JavaMapping::Omg, JavaMapping::Psm] {
        let text = generated(OCTETS, mapping).expect("octet constants have a Java form");
        assert!(
            text.contains("public static final byte EK_MINIMAL = (byte) 0xF1;"),
            "{mapping:?}: {text}"
        );
        assert!(
            text.contains("public static final byte EK_BOTH = (byte) 0xF3;"),
            "{mapping:?}: {text}"
        );
        assert!(
            text.contains("public static final byte SMALL = (byte) 16;"),
            "{mapping:?}: {text}"
        );
    }
}