alef 0.79.2

Opinionated polyglot binding generator for Rust libraries
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
//! Compiles the generated Java options-field visitor path with a real `javac`.
//!
//! Substring assertions cannot see whether a generated method body is *type-correct*. The
//! convert-with-visitor body is the densest exception-flow shape the Java backend emits — nested
//! `try`/`catch`/`finally`, a captured `operationFailure` slot, a typed rethrow chain and a
//! resource `finally` that suppresses cleanup failures onto the primary — and every one of those
//! interacts with `javac`'s checked-exception analysis. This test extracts the generated method
//! together with the generated helpers it calls, compiles them against the real generated
//! `NativeLib` and exception classes, and fails on any `javac` diagnostic. ~keep

#[path = "backends_java_blocker_regressions/support.rs"]
mod support;

use alef::backends::java::JavaBackend;
use alef::core::backend::Backend;
use alef::core::config::{NewAlefConfig, ResolvedCrateConfig};
use alef::core::ir::{
    ApiSurface, EnumDef, EnumVariant, FieldDef, FunctionDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeDef,
    TypeRef,
};
use std::path::Path;
use support::{compile_java, extract_java_method, java_available, run_java_args, write_file};

/// Generated files that carry no Jackson references and can therefore be compiled as emitted.
const REAL_DEPENDENCIES: &[&str] = &[
    "NativeLib.java",
    "TestLibRsException.java",
    "ConversionErrorException.java",
    "CoreErrorException.java",
    "PanicException.java",
    "Callback.java",
    "FlowDecision.java",
];

/// Generated members the convert-with-visitor body calls, lifted into the probe alongside it.
const PROBE_MEMBERS: &[&str] = &[
    "private interface NativeReleaser",
    "static final class NativeResources implements AutoCloseable",
    "private static void checkLastError()",
];

const VISITOR_METHOD: &str = "private static WorkResult processHtmlWithVisitorInternal";

fn visitor_config() -> ResolvedCrateConfig {
    let config: NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["java", "ffi"]

[[crates]]
name = "test_lib"
sources = ["src/lib.rs"]

[crates.ffi]
prefix = "test"

[crates.java]
package = "com.test"

[[crates.trait_bridges]]
trait_name = "Callback"
type_alias = "CallbackHandle"
bind_via = "options_field"
options_type = "WorkConfig"
options_field = "hook"
context_type = "VisitContext"
result_type = "FlowDecision"
"#,
    )
    .expect("valid Java visitor config");
    config.resolve().expect("resolved Java visitor config").remove(0)
}

fn field(name: &str, ty: TypeRef) -> FieldDef {
    FieldDef {
        name: name.to_owned(),
        ty,
        ..Default::default()
    }
}

fn record(name: &str, fields: Vec<FieldDef>) -> TypeDef {
    TypeDef {
        name: name.to_owned(),
        rust_path: format!("test_lib::{name}"),
        fields,
        is_clone: true,
        has_serde: true,
        ..Default::default()
    }
}

fn callback_trait() -> TypeDef {
    TypeDef {
        name: "Callback".to_owned(),
        rust_path: "test_lib::Callback".to_owned(),
        is_trait: true,
        methods: vec![MethodDef {
            name: "inspect".to_owned(),
            params: vec![ParamDef {
                name: "context".to_owned(),
                ty: TypeRef::Named("VisitContext".to_owned()),
                is_ref: true,
                ..Default::default()
            }],
            return_type: TypeRef::Named("FlowDecision".to_owned()),
            receiver: Some(ReceiverKind::RefMut),
            has_default_impl: true,
            ..Default::default()
        }],
        ..Default::default()
    }
}

fn visitor_api() -> ApiSurface {
    ApiSurface {
        crate_name: "test_lib".to_owned(),
        version: "0.1.0".to_owned(),
        types: vec![
            record("VisitContext", vec![field("path", TypeRef::String)]),
            record(
                "WorkConfig",
                vec![
                    field("hook", TypeRef::Named("CallbackHandle".to_owned())),
                    field("mode", TypeRef::String),
                ],
            ),
            record("WorkResult", vec![field("text", TypeRef::String)]),
            callback_trait(),
        ],
        functions: vec![FunctionDef {
            name: "process_html".to_owned(),
            rust_path: "test_lib::process_html".to_owned(),
            params: vec![
                ParamDef {
                    name: "html".to_owned(),
                    ty: TypeRef::String,
                    ..Default::default()
                },
                ParamDef {
                    name: "config".to_owned(),
                    ty: TypeRef::Named("WorkConfig".to_owned()),
                    ..Default::default()
                },
            ],
            return_type: TypeRef::Named("WorkResult".to_owned()),
            ..Default::default()
        }],
        enums: vec![EnumDef {
            name: "FlowDecision".to_owned(),
            rust_path: "test_lib::FlowDecision".to_owned(),
            variants: vec![
                EnumVariant {
                    name: "Proceed".to_owned(),
                    is_default: true,
                    ..Default::default()
                },
                EnumVariant {
                    name: "DropNode".to_owned(),
                    ..Default::default()
                },
            ],
            has_serde: true,
            ..Default::default()
        }],
        ..Default::default()
    }
}

fn generated_files() -> Vec<(String, String)> {
    generate(&visitor_api(), &visitor_config())
}

fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> Vec<(String, String)> {
    JavaBackend
        .generate_bindings(api, config)
        .expect("java visitor generation must succeed")
        .into_iter()
        .map(|file| {
            let name = file
                .path
                .file_name()
                .expect("generated file name")
                .to_string_lossy()
                .into_owned();
            (name, file.content)
        })
        .collect()
}

fn facade_source(files: &[(String, String)]) -> String {
    files
        .iter()
        .find(|(_, content)| content.contains(VISITOR_METHOD))
        .unwrap_or_else(|| panic!("no generated file declares `{VISITOR_METHOD}`"))
        .1
        .clone()
}

/// Wraps the generated convert-with-visitor method and the generated helpers it calls in a probe
/// class, supplying only the object mapper — the one collaborator that would otherwise drag the
/// Jackson jars onto the classpath.
fn probe_source(facade: &str) -> String {
    let mut members = vec![extract_java_method(facade, VISITOR_METHOD)];
    members.extend(PROBE_MEMBERS.iter().map(|member| extract_java_method(facade, member)));
    format!(
        "package com.test;\n\n\
         import java.lang.foreign.Arena;\n\
         import java.lang.foreign.MemorySegment;\n\
         import java.util.List;\n\n\
         final class VisitorProbe {{\n{}\n\
         \x20   private static final ProbeMapper MAPPER = new ProbeMapper();\n\n\
         \x20   static final class ProbeMapper {{\n\
         \x20       String writeValueAsString(final Object value) {{\n\
         \x20           return \"{{}}\";\n\
         \x20       }}\n\n\
         \x20       <T> T readValue(final String json, final Class<T> type) {{\n\
         \x20           return null;\n\
         \x20       }}\n\
         \x20   }}\n\
         }}\n",
        members.join("\n")
    )
}

/// The `operationFailure` slot must be typed as the crate exception, not `Throwable`.
///
/// Every value assigned to it is already that exception, and the `Throwable` clause of the catch
/// chain rethrows the slot itself. Typing the slot `Throwable` compiles only because the whole
/// body sits inside an outer `catch (Throwable)`; the moment that outer chain changes shape the
/// rethrow becomes `unreported exception Throwable`. This assertion runs without a JDK. ~keep
#[test]
fn visitor_operation_failure_slot_is_typed_as_the_crate_exception() {
    let facade = facade_source(&generated_files());
    let method = extract_java_method(&facade, VISITOR_METHOD);
    assert!(
        method.contains("TestLibRsException operationFailure = null;"),
        "{method}"
    );
    assert!(!method.contains("Throwable operationFailure"), "{method}");
    assert!(method.contains("throw operationFailure;"), "{method}");
}

#[test]
fn generated_visitor_method_compiles_under_javac() {
    if !java_available() {
        return;
    }
    let files = generated_files();
    let directory = tempfile::tempdir().expect("temporary Java visitor directory");
    let mut sources: Vec<String> = Vec::new();
    for name in REAL_DEPENDENCIES {
        let content = files
            .iter()
            .find(|(file_name, _)| file_name == name)
            .unwrap_or_else(|| panic!("generated {name} must be emitted"))
            .1
            .clone();
        write_file(directory.path(), &format!("com/test/{name}"), &content);
        sources.push(format!("com/test/{name}"));
    }
    write_file(
        directory.path(),
        "com/test/Stubs.java",
        include_str!("fixtures/java_visitor_stubs.java"),
    );
    sources.push("com/test/Stubs.java".to_owned());
    write_file(
        directory.path(),
        "com/test/VisitorProbe.java",
        &probe_source(&facade_source(&files)),
    );
    sources.push("com/test/VisitorProbe.java".to_owned());

    let arguments: Vec<&str> = sources.iter().map(String::as_str).collect();
    compile_java(directory.path(), &arguments);
}

/// Generated files the bridge compile needs verbatim; the context record is stubbed for Jackson.
const BRIDGE_DEPENDENCIES: &[&str] = &["VisitorBridge.java", "Callback.java", "FlowDecision.java"];

fn bridge_config() -> ResolvedCrateConfig {
    let config: NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["java", "ffi"]

[[crates]]
name = "test_lib"
sources = ["src/lib.rs"]

[crates.ffi]
prefix = "test"

[crates.java]
package = "com.test"

[[crates.trait_bridges]]
trait_name = "Callback"
type_alias = "CallbackHandle"
bind_via = "options_field"
options_type = "WorkConfig"
options_field = "hook"
context_type = "NodeContext"
result_type = "FlowDecision"
"#,
    )
    .expect("valid Java bridge config");
    config.resolve().expect("resolved Java bridge config").remove(0)
}

fn bridge_api() -> ApiSurface {
    let mut api = visitor_api();
    api.types[0] = record(
        "NodeContext",
        vec![
            field("kind", TypeRef::Named("NodeKind".to_owned())),
            field("name", TypeRef::String),
            field("depth", TypeRef::Primitive(PrimitiveType::U64)),
            field("position", TypeRef::Primitive(PrimitiveType::U64)),
            field("parent", TypeRef::String),
            field("inline", TypeRef::Primitive(PrimitiveType::Bool)),
        ],
    );
    api.types[3] = TypeDef {
        methods: vec![MethodDef {
            name: "inspect".to_owned(),
            params: vec![ParamDef {
                name: "context".to_owned(),
                ty: TypeRef::Named("NodeContext".to_owned()),
                is_ref: true,
                ..Default::default()
            }],
            return_type: TypeRef::Named("FlowDecision".to_owned()),
            receiver: Some(ReceiverKind::RefMut),
            has_default_impl: true,
            ..Default::default()
        }],
        ..callback_trait()
    };
    api.enums.push(EnumDef {
        name: "NodeKind".to_owned(),
        rust_path: "test_lib::NodeKind".to_owned(),
        variants: vec![
            EnumVariant {
                name: "Element".to_owned(),
                is_default: true,
                ..Default::default()
            },
            EnumVariant {
                name: "Text".to_owned(),
                ..Default::default()
            },
        ],
        has_serde: true,
        ..Default::default()
    });
    api
}

/// The visitor bridge is the other half of the visitor path and had no compile coverage either.
///
/// Its upcall handlers must return a discriminant the native side understands when the host
/// callback throws, and its `decodeContext` must construct the generated context record. Both are
/// invisible to substring assertions and both are `javac` errors when wrong. ~keep
#[test]
fn generated_visitor_bridge_compiles_under_javac() {
    if !java_available() {
        return;
    }
    let files = generate(&bridge_api(), &bridge_config());
    let directory = tempfile::tempdir().expect("temporary Java bridge directory");
    let mut sources: Vec<String> = Vec::new();
    for name in BRIDGE_DEPENDENCIES {
        let content = files
            .iter()
            .find(|(file_name, _)| file_name == name)
            .unwrap_or_else(|| panic!("generated {name} must be emitted"))
            .1
            .clone();
        write_file(directory.path(), &format!("com/test/{name}"), &content);
        sources.push(format!("com/test/{name}"));
    }
    write_file(
        directory.path(),
        "com/test/NodeContext.java",
        include_str!("fixtures/java_visitor_bridge_context.java"),
    );
    sources.push("com/test/NodeContext.java".to_owned());

    let arguments: Vec<&str> = sources.iter().map(String::as_str).collect();
    compile_java(directory.path(), &arguments);
}

/// A context whose shape is nothing like the six-field one above.
///
/// Different arity, no leading enum discriminant, sub-word scalars that force `#[repr(C)]`
/// padding in two places, and two components the C struct cannot carry at all. The six-field
/// fixture above happens to match the layout the bridge template used to hardcode, so it stayed
/// green while every other context shape emitted Java that could not compile. ~keep
fn span_config() -> ResolvedCrateConfig {
    let config: NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["java", "ffi"]

[[crates]]
name = "test_lib"
sources = ["src/lib.rs"]

[crates.ffi]
prefix = "test"
visitor_callbacks = true

[crates.java]
package = "com.test"

[[crates.trait_bridges]]
trait_name = "Callback"
type_alias = "CallbackHandle"
bind_via = "options_field"
options_type = "WorkConfig"
options_field = "hook"
context_type = "SpanContext"
result_type = "FlowDecision"
"#,
    )
    .expect("valid Java span config");
    config.resolve().expect("resolved Java span config").remove(0)
}

fn optional_field(name: &str, ty: TypeRef) -> FieldDef {
    FieldDef {
        optional: true,
        ..field(name, ty)
    }
}

fn span_api() -> ApiSurface {
    let mut api = visitor_api();
    api.types[0] = record(
        "SpanContext",
        vec![
            field("label", TypeRef::String),
            field("severity", TypeRef::Primitive(PrimitiveType::U8)),
            field("active", TypeRef::Primitive(PrimitiveType::Bool)),
            field("offset", TypeRef::Primitive(PrimitiveType::I16)),
            optional_field("note", TypeRef::String),
            field("weight", TypeRef::Primitive(PrimitiveType::F64)),
            field("tags", TypeRef::Vec(Box::new(TypeRef::String))),
        ],
    );
    api.types[3] = TypeDef {
        methods: vec![MethodDef {
            name: "inspect".to_owned(),
            params: vec![ParamDef {
                name: "context".to_owned(),
                ty: TypeRef::Named("SpanContext".to_owned()),
                is_ref: true,
                ..Default::default()
            }],
            return_type: TypeRef::Named("FlowDecision".to_owned()),
            receiver: Some(ReceiverKind::RefMut),
            has_default_impl: true,
            ..Default::default()
        }],
        ..callback_trait()
    };
    api
}

fn compile_span_bridge(directory: &Path, extra: &[&str]) {
    let files = generate(&span_api(), &span_config());
    let mut sources: Vec<String> = Vec::new();
    for name in BRIDGE_DEPENDENCIES {
        let content = files
            .iter()
            .find(|(file_name, _)| file_name == name)
            .unwrap_or_else(|| panic!("generated {name} must be emitted"))
            .1
            .clone();
        write_file(directory, &format!("com/test/{name}"), &content);
        sources.push(format!("com/test/{name}"));
    }
    write_file(
        directory,
        "com/test/SpanContext.java",
        include_str!("fixtures/java_visitor_span_context.java"),
    );
    sources.push("com/test/SpanContext.java".to_owned());
    sources.extend(extra.iter().map(|source| (*source).to_owned()));

    let arguments: Vec<&str> = sources.iter().map(String::as_str).collect();
    compile_java(directory, &arguments);
}

/// `decodeContext` must construct whatever record the IR describes, not a fixed six-tuple.
#[test]
fn generated_visitor_bridge_compiles_for_a_context_of_another_shape() {
    if !java_available() {
        return;
    }
    let directory = tempfile::tempdir().expect("temporary Java span directory");
    compile_span_bridge(directory.path(), &[]);
}

/// The derived offsets must be the ones `#[repr(C)]` actually places the fields at.
///
/// A layout that is wrong by a few bytes of padding still type-checks, so `javac` alone proves
/// nothing about it. Loading the generated class runs `MemoryLayout.structLayout` over the
/// derived members and lets the probe read the offset constants back. ~keep
#[test]
fn generated_visitor_bridge_lays_the_context_out_where_repr_c_does() {
    if !java_available() {
        return;
    }
    let directory = tempfile::tempdir().expect("temporary Java span probe directory");
    write_file(
        directory.path(),
        "com/test/SpanProbe.java",
        include_str!("fixtures/java_visitor_span_probe.java"),
    );
    compile_span_bridge(directory.path(), &["com/test/SpanProbe.java"]);
    run_java_args(
        directory.path(),
        &["--enable-native-access=ALL-UNNAMED", "-cp", ".", "com.test.SpanProbe"],
    );
}

/// The Java layout and the C struct must describe the same fields, in the same order.
///
/// Two generators deriving the same shape independently is how the Java bridge came to hardcode
/// one consumer's context; this pins the two emissions to each other so a future divergence is a
/// test failure rather than a consumer's broken build. It also pins the skip decision: `weight`
/// and `tags` have no C representation, so neither side may mention them. ~keep
#[test]
fn java_context_layout_names_the_same_fields_as_the_generated_c_struct() {
    let carried = ["label", "severity", "active", "offset", "note"];

    let java = generate(&span_api(), &span_config());
    let bridge = &java
        .iter()
        .find(|(name, _)| name == "VisitorBridge.java")
        .expect("generated VisitorBridge.java")
        .1;
    let java_order: Vec<&str> = carried
        .iter()
        .filter(|field| bridge.contains(&format!("withName(\"{field}\")")))
        .copied()
        .collect();
    assert_eq!(java_order, carried, "{bridge}");

    let ffi = alef::backends::ffi::FfiBackend
        .generate_bindings(&span_api(), &span_config())
        .expect("ffi visitor generation must succeed");
    let struct_source = ffi
        .iter()
        .map(|file| file.content.as_str())
        .find(|content| content.contains("pub struct TestContext {"))
        .expect("generated #[repr(C)] context struct");
    let declaration = struct_source
        .split("pub struct TestContext {")
        .nth(1)
        .and_then(|rest| rest.split_once('}'))
        .expect("context struct body")
        .0;
    for (field, c_type) in
        carried
            .iter()
            .zip(["*const std::ffi::c_char", "u8", "i32", "i16", "*const std::ffi::c_char"])
    {
        assert!(
            declaration.contains(&format!("pub {field}: {c_type},")),
            "`{field}: {c_type}` missing from:{declaration}"
        );
    }

    for dropped in ["weight", "tags"] {
        assert!(!declaration.contains(dropped), "{declaration}");
        assert!(!bridge.contains(&format!("withName(\"{dropped}\")")), "{bridge}");
    }
}