Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions changelog.d/7310-native-seh-decline-and-corpus-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
### In-process LLVM backend: decline SEH funclets, refresh the stale corpora (#7302 / #7301)

Three follow-ups to the EH migration, all found by auditing what the native
backend actually covers.

**1. `--target windows` + try/catch was a hard compile error.** Perry lowers
exceptions to SEH funclets on windows-msvc (`catchswitch`/`catchpad`/
`catchret`), which the in-process reader cannot construct — inkwell 0.9
exposes no `build_catch_switch`/`build_catch_pad`/`build_catch_ret` (only an
opcode enum for *reading* them), so real support needs raw `llvm-sys` FFI.
Reproduced: `PERRY_LLVM_INPROCESS=native … --target windows` on any
try/catch (or async) program failed with `unknown instruction catchswitch`.
Such modules now decline to the textual path — costing only the in-process
speedup — instead of failing the build. The decline is narrow (windows
triple AND a personality present), so the macOS EH path added in #7307 is
untouched, and it is *not* the blanket personality bail #7307 removed.

**2. The tracked corpora were stale.** `spike_text.ll` and `batch_kernel.ll`
were captured before #7305 and still contained `setjmp` calls and the
`#0`/`#1` attribute groups — so the reader's primary gate was validating IR
Perry no longer emits, while missing the forms it does. Regenerated from
current codegen: the spike corpus now carries 38 `invoke` edges and zero
setjmp, and both round-trip through native construction and the LLVM
verifier.

**3. The setjmp-era attribute handling is deleted.** With the corpora
refreshed, nothing emits or contains `#0` (returns_twice) or `#1` (noinline),
so the reader's branches for them are gone — the losing mode stops compiling
rather than lingering as an untested branch (CLAUDE.md kill policy). A corpus
that still carries one now fails loudly as stale input, which is the point.

Verified locally against LLVM 22.1.4: liveness, behavior parity and
**byte-identical object verdicts** on the spike (21,393 bytes), the 3-unit
batch kernel (56,296 bytes) and the try/catch program (46,745 bytes);
569 `perry-codegen` tests green.
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2654,6 +2654,12 @@ fn try_native_units(
target: Option<&str>,
module_prefix: &str,
) -> Option<Result<Vec<u8>>> {
// SEH funclets are the one EH shape the in-process reader cannot
// construct (see LlModule::needs_eh_funclets). Decline to the textual
// path rather than failing the compile.
if llmod.needs_eh_funclets() {
return None;
}
match crate::native_emit::native_mode() {
crate::native_emit::NativeMode::Off => None,
crate::native_emit::NativeMode::Native => Some(
Expand Down Expand Up @@ -2686,6 +2692,12 @@ fn try_native_construction(
target: Option<&str>,
module_prefix: &str,
) -> Option<Result<Vec<u8>>> {
// SEH funclets are the one EH shape the in-process reader cannot
// construct (see LlModule::needs_eh_funclets). Decline to the textual
// path rather than failing the compile.
if llmod.needs_eh_funclets() {
return None;
}
match crate::native_emit::native_mode() {
crate::native_emit::NativeMode::Off => None,
crate::native_emit::NativeMode::Native => Some(crate::native_emit::compile_module_native(
Expand Down
26 changes: 9 additions & 17 deletions crates/perry-codegen/src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,12 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> {
let attr_str = h.attr_str;
for a in attr_str.split_whitespace() {
match a {
// Attribute-group references: the only group perry stamps on
// defines is #1 (noinline, the setjmp boundary). Group
// contents live in the skeleton for *declares*; for defines
// we apply the concrete attribute.
"#1" => add_enum_attr(ctx, func, "noinline"),
// #7302 deleted the setjmp-era groups (#0 returns_twice, #1
// noinline); perry no longer stamps an attribute-group ref
// on any define. The losing mode stops compiling rather
// than lingering as an untested branch (CLAUDE.md kill
// policy) — a corpus still carrying one is stale and must
// say so loudly.
"alwaysinline" | "inlinehint" | "noinline" => add_enum_attr(ctx, func, a),
other => bail!("unknown define attribute `{other}`"),
}
Expand Down Expand Up @@ -724,9 +725,9 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> {
let callee = &after[..paren];
let close = rmatch_paren(after, paren)?;
let args_str = &after[paren + 1..close];
// Trailing callsite attribute-group ref. Only `#0` (returns_twice,
// on setjmp calls) exists in the dialect; the declare carries it too,
// so this is fidelity, not correctness.
// Trailing callsite attribute-group ref. #7302 removed the only one
// perry ever emitted (`#0`, returns_twice on setjmp calls), so any
// ref here now means stale input.
let trailing_attr = after[close + 1..].trim();

let mut args: Vec<BasicMetadataValueEnum> = Vec::new();
Expand Down Expand Up @@ -771,15 +772,6 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> {
.map_err(be)?;
match trailing_attr {
"" => {}
"#0" => {
let kind = inkwell::attributes::Attribute::get_named_enum_kind_id("returns_twice");
if kind != 0 {
site.add_attribute(
inkwell::attributes::AttributeLoc::Function,
self.ctx.create_enum_attribute(kind, 0),
);
}
}
other => bail!("unknown callsite attribute `{other}`"),
}
match site.try_as_basic_value() {
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-codegen/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,22 @@ impl LlModule {
));
}

/// SEH funclets (#7302): true when this module targets windows-msvc AND
/// contains try/catch, i.e. when its EH lowering is
/// `catchswitch`/`catchpad`/`catchret` rather than Itanium landing pads.
///
/// The in-process LLVM reader can build `invoke`/`landingpad` but NOT
/// the funclet forms: inkwell 0.9 exposes no `build_catch_switch` /
/// `build_catch_pad` / `build_catch_ret` (only an opcode enum for
/// reading them), so constructing them needs raw `llvm-sys` FFI. Until
/// that lands, such modules take the textual path — declining costs
/// nothing but the in-process speedup, whereas letting the reader hit
/// the instruction is a hard compile error.
pub fn needs_eh_funclets(&self) -> bool {
self.target_triple.contains("-windows-")
&& self.functions.iter().any(|f| f.personality.is_some())
}

/// Invoke-EH (#7302): declare the personality routine referenced by
/// every `define ... personality ptr @perry_eh_personality`. Declared
/// varargs — the symbol is only ever *named* on define lines and in the
Expand Down
7 changes: 2 additions & 5 deletions experiments/llvm-inprocess-spike/batch_kernel.ll
Original file line number Diff line number Diff line change
Expand Up @@ -1275,8 +1275,8 @@ declare void @js_wait_for_event()
declare i32 @js_event_loop_host_driven()
declare void @js_unsettled_top_level_await_exit()
declare void @js_throw(double)
declare ptr @js_try_push()
declare i32 @_setjmp(ptr) #0
declare void @js_eh_try_push()
declare i32 @perry_eh_personality(...)
declare void @js_try_end()
declare double @js_get_exception()
declare void @js_clear_exception()
Expand Down Expand Up @@ -10646,9 +10646,6 @@ entry.0:
}


attributes #0 = { returns_twice }
attributes #1 = { noinline }

attributes #2 = { nounwind willreturn readnone }

attributes #3 = { nounwind willreturn readonly }
Expand Down
Loading
Loading