From 59a2e4941bbcdd985b9e45c02f44c7230ce27d00 Mon Sep 17 00:00:00 2001 From: kazzkyy Date: Tue, 14 Jul 2026 05:15:08 -0400 Subject: [PATCH] Add experimental eBPF backend and runtime profiles --- Cargo.lock | 1 + Cargo.toml | 2 +- ROADMAP.md | 2 + architecture/03-compiler-pipeline.md | 12 + .../04-intermediate-representations.md | 1 + architecture/05-runtime-and-abi.md | 6 +- architecture/06-backend-architecture.md | 25 + .../22.2-system-network-security-catalog.md | 3 - ...018-rust-workspace-and-crate-boundaries.md | 3 +- .../0070-experimental-ebpf-backend.md | 173 ++++ ...-native-stable-generational-transition.md} | 2 +- ...2-atomic-initialized-object-allocation.md} | 4 +- crates/compiler/backend-api/src/lib.rs | 402 +++++++- .../backend-api/tests/gc_capabilities.rs | 2 +- .../backend-api/tests/runtime_contracts.rs | 134 +++ crates/compiler/backends/c/src/lib.rs | 4 + crates/compiler/backends/llvm/src/bpf.rs | 933 ++++++++++++++++++ crates/compiler/backends/llvm/src/lib.rs | 15 + crates/compiler/backends/llvm/tests/bpf.rs | 170 ++++ .../compiler/backends/llvm/tests/lowering.rs | 6 +- .../compiler/backends/mir-interp/src/lib.rs | 7 +- .../mir-interp/tests/language_differential.rs | 2 + crates/compiler/compile-time/src/lib.rs | 8 + crates/compiler/diagnostics/catalog.tsv | 10 + crates/compiler/diagnostics/src/lib.rs | 3 + crates/compiler/diagnostics/tests/catalog.rs | 12 +- crates/compiler/driver/Cargo.toml | 1 + .../driver/benches/compilation_workload.rs | 7 + crates/compiler/driver/src/lib.rs | 15 + crates/compiler/driver/src/main.rs | 148 ++- crates/compiler/driver/tests/cli_dump.rs | 58 ++ .../driver/tests/front_end_pipeline.rs | 2 + .../driver/tests/reference_metadata.rs | 2 + crates/compiler/hir/src/lib.rs | 15 + crates/compiler/mir/src/lib.rs | 12 + crates/compiler/mir/tests/lowering.rs | 2 + crates/compiler/target/src/lib.rs | 78 ++ crates/compiler/target/tests/target_spec.rs | 20 +- crates/compiler/types/src/body_checking.rs | 6 +- crates/compiler/types/src/call_checking.rs | 16 +- crates/compiler/types/src/lib.rs | 19 + crates/compiler/types/tests/errors.rs | 2 + crates/compiler/types/tests/numeric_values.rs | 6 + .../libraries/standard/tests/api_baseline.rs | 9 +- crates/runtime/native/tests/abi.rs | 5 +- crates/tools/architecture-tests/src/tests.rs | 2 +- .../test-runner/tests/foundation_sources.rs | 2 + examples/bpf/README.md | 34 + examples/bpf/xdpPass.pop | 5 + 49 files changed, 2362 insertions(+), 46 deletions(-) create mode 100644 architecture/decisions/0070-experimental-ebpf-backend.md rename architecture/decisions/{0059-native-stable-generational-transition.md => 0071-native-stable-generational-transition.md} (98%) rename architecture/decisions/{0060-atomic-initialized-object-allocation.md => 0072-atomic-initialized-object-allocation.md} (98%) create mode 100644 crates/compiler/backend-api/tests/runtime_contracts.rs create mode 100644 crates/compiler/backends/llvm/src/bpf.rs create mode 100644 crates/compiler/backends/llvm/tests/bpf.rs create mode 100644 examples/bpf/README.md create mode 100644 examples/bpf/xdpPass.pop diff --git a/Cargo.lock b/Cargo.lock index af274ae..a0d0b9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,6 +265,7 @@ dependencies = [ name = "pop-driver" version = "0.1.0" dependencies = [ + "pop-backend-api", "pop-backend-c", "pop-backend-llvm", "pop-compile-time", diff --git a/Cargo.toml b/Cargo.toml index 0b507e0..2b82b26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ rust-version = "1.96" license = "MIT" [workspace.dependencies] -inkwell = { version = "0.9.0", default-features = false, features = ["llvm22-1-prefer-dynamic", "target-x86"] } +inkwell = { version = "0.9.0", default-features = false, features = ["llvm22-1-prefer-dynamic", "target-x86", "target-bpf"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" sha2 = "0.11.0" diff --git a/ROADMAP.md b/ROADMAP.md index 8c4f800..b25e863 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -353,6 +353,8 @@ Post-baseline library work has begun without widening the release foundation: ## Explicitly after 0.1.0 - expanding the experimental C backend beyond its accepted fail-closed subset; +- stabilizing or expanding the experimental eBPF backend beyond the initial + runtime-free XDP object experiment; - a custom VM or stable serialized MIR/bytecode compatibility promise; - the complete official extension and public-library catalog; - finalizers, weak references, unrestricted runtime reflection, and Bubble diff --git a/architecture/03-compiler-pipeline.md b/architecture/03-compiler-pipeline.md index dac31fb..434c0ee 100644 --- a/architecture/03-compiler-pipeline.md +++ b/architecture/03-compiler-pipeline.md @@ -14,6 +14,8 @@ flowchart LR M --> O[Optimized MIR] O --> L[LLVM backend] L --> N[Native object] + O -. experimental .-> B[LLVM BPF target] + B -. experimental .-> E[ELF eBPF object] O -. experimental .-> C[C11 source backend] O -. future .-> V[VM bytecode backend] O -. tools .-> I[MIR interpreter / verifier] @@ -166,6 +168,16 @@ optimization. It emits deterministic C11 for its declared runtime-free capability subset and rejects unsupported MIR before publishing source; it never reconstructs semantics from Pop source text. +The experimental eBPF backend follows the same handoff after portable MIR +optimization. It derives runtime-contract requirements from MIR, resolves them +against an explicit runtime profile, validates eBPF-specific target limits, +lowers through backend-private LLVM IR, selects LLVM's BPF target, and emits an +ELF eBPF object for an explicit program kind such as XDP. Missing runtime +contracts, unsupported backend representations, floating-point behavior, +dispatch, recursion, or unproven loop behavior are diagnosed before any object +is written; see +[ADR 0070](./decisions/0070-experimental-ebpf-backend.md). + ## Tooling and incremental queries The parser, resolver, type checker, and HIR APIs work without native code diff --git a/architecture/04-intermediate-representations.md b/architecture/04-intermediate-representations.md index 3bd869a..ef10d2b 100644 --- a/architecture/04-intermediate-representations.md +++ b/architecture/04-intermediate-representations.md @@ -11,6 +11,7 @@ | MIR | Portable execution semantics | CFGs, typed values, abstract runtime operations | source sugar, LLVM opcodes | | C11 source | Experimental backend artifact | exact-width C types, checked helpers, private control-flow lowering | canonical language semantics, unchecked fallbacks | | LLVM IR | Native backend implementation | LLVM types, intrinsics, target ABI | canonical language semantics | +| LLVM BPF IR | Experimental backend implementation | BPF triple, section metadata, backend-private scalar lowering | canonical language semantics, Pop managed runtime | ## Stable identities diff --git a/architecture/05-runtime-and-abi.md b/architecture/05-runtime-and-abi.md index 6d636a2..7d56c62 100644 --- a/architecture/05-runtime-and-abi.md +++ b/architecture/05-runtime-and-abi.md @@ -49,7 +49,7 @@ a backend with verified relocating-root support. ABI 1.x, immutable root spills, or a target capability alone cannot satisfy the production profile. Profile/ ABI mismatch fails before link or load; there is no silent bootstrap fallback. -Under ADR 0059, ABI 1 native execution no longer uses `BootstrapRuntime`. +Under ADR 0071, ABI 1 native execution no longer uses `BootstrapRuntime`. Instead it composes the generational allocator and incremental SATB mature collector in a `NativeStableGenerationalConformance` stage that places every native allocation in a non-moving domain. This stage preserves ABI 1 stable @@ -130,7 +130,7 @@ and capacity private, grows storage without changing the list handle, and applies precise barriers for managed elements. MIR retains distinct typed list operations; no backend may reinterpret them as array or table operations. -ADR 0060 advances native ABI 1 to version 1.11 with atomic initialized object +ADR 0072 advances native ABI 1 to version 1.11 with atomic initialized object allocation. LLVM passes the exact pointer map and one physical initializer per logical slot in a single native transition. The runtime validates every managed initializer before publication and returns either a completely initialized @@ -268,7 +268,7 @@ allocation assists, deterministic byte-limit OOM, empty-page return, and domain/debt telemetry. It still reports the lower relocation contract because cooperative work is not concurrent production marking, the native backend does not yet provide writable relocating roots, and no profile may infer production -capability from implementation experiments. ADR 0059 permits a closed native +capability from implementation experiments. ADR 0071 permits a closed native stable-token wrapper to use its mature allocator, SATB marking, and sweeping without exposing nursery relocation; this does not select the production profile. diff --git a/architecture/06-backend-architecture.md b/architecture/06-backend-architecture.md index 5f7f50e..2da658f 100644 --- a/architecture/06-backend-architecture.md +++ b/architecture/06-backend-architecture.md @@ -114,6 +114,31 @@ partial artifact. C text is disposable output and is not a stable ABI, cache, or semantic contract. See [ADR 0059](./decisions/0059-experimental-secure-c-transpilation-backend.md). +## Experimental eBPF backend + +The experimental eBPF backend is an LLVM-backend mode for producing ELF eBPF +objects from verified MIR under an explicit runtime-contract profile. It +validates before emission, keeps BPF and Inkwell details inside the backend, +and uses LLVM's BPF target rather than a custom instruction emitter in the +first slice. + +The initial triples are `bpfel-unknown-none` and `bpfeb-unknown-none`. They +represent ELF, no-OS LLVM BPF targets. Runtime support is selected separately +through profiles such as `linux-ebpf`. PLRI remains a set of abstract runtime +contracts; the `linux-ebpf` profile currently provides only the scalar +contracts needed by the MVP and therefore cannot satisfy requirements for +managed allocation, standard-library adapters, GC roots, closures, interfaces, +coroutines, or similar dynamic representations. Those failures are reported as +missing runtime contracts, not as HIR/MIR language bans. + +The MVP supports an explicit XDP program mode, emits a wrapper in an `xdp` +section, and rejects checked arithmetic until trap-preserving lowering exists. +It also has eBPF-specific validation for invalid entry signatures, recursion, +floating point, unproven loop backedges, unsupported MIR operations, and +backend representations that have not been implemented yet. If LLVM BPF is +unavailable, object emission fails with a target diagnostic and no partial +artifact. See [ADR 0070](./decisions/0070-experimental-ebpf-backend.md). + ## Future VM backend The VM backend should lower canonical MIR to typed or register-based bytecode. diff --git a/architecture/22.2-system-network-security-catalog.md b/architecture/22.2-system-network-security-catalog.md index 0c3ba7a..114bea0 100644 --- a/architecture/22.2-system-network-security-catalog.md +++ b/architecture/22.2-system-network-security-catalog.md @@ -31,7 +31,6 @@ the process filesystem through global registration. | --- | --- | --- | --- | | `Task` | standard/platform; planned; phase 3 | `Task`, `CancelToken`, `Group`, deadline, spawn/join/race/select/yield/sleep; structured failure propagation | Time + PLRI scheduler; task creation may allocate; scope owns children; cancellation points explicit; no detached task by default or ambient context bag | | `Channel` | standard; planned; phase 3 | bounded/unbounded typed channels, sender/receiver, select integration; `send`, `receive`, `trySend`, `close` | Task/Atomic; portable semantics; bounded is default for streams; allocation/copy/ownership documented; no untyped messages | -| `Actor` | standard/platform; planned; phase 4 | isolated local actors, exact typed mailboxes, actor references/replies, monitors, structured supervision, restart and shutdown policy | Task/Channel + scheduler and GC ownership contracts; messages are compiler-proven safe and copied into private actor ownership; mailboxes/restarts/cleanup are bounded; no shared mutable state, selective receive, marker-interface safety, or runtime type lookup | | `Atomic` | standard/platform; planned; phase 3 | exact atomic integer/boolean/pointer-safe handles, memory order, fences, wait/notify | Intrinsics/PLRI; availability by target capability; operations allocate nothing; unsafe order combinations diagnosed; no managed raw pointers | | `Actor` | standard/platform; planned; phase 4 | typed isolated `Ref`, `Inbox`, `Reply`, supervisors, monitors, bounded mailboxes; `start`, `send`, `trySend`, `receive`, `reply` | Task/Channel/Codec + scheduler/GC isolation support; messages are statically proven copy-safe and copied into actor ownership; mailbox allocation, copying, suspension, stale incarnation, and failure are explicit; no symbolic registry, shared mutable actor state, or class hierarchy | | `Cluster` | official/platform; planned; phase 8 | authenticated remote `Actor`, nodes, publish/spawn, delivery outcomes, remote supervision and test transports | Actor/Codec/Net/Crypto/Identity/Task; separately installed `Pop.Cluster`; bounded schema encoding and explicit partial failure; no location transparency, code shipping, string actor lookup, implicit retry, or exactly-once claim | @@ -58,8 +57,6 @@ creating a general application service abstraction. | `Socket` | standard/platform; planned; phase 4 | opaque socket handle, options, local/remote address, accept/send/receive/close, multicast | Io/Net/Task + PLRI; system calls explicit; caller buffers reusable; option availability typed; raw sockets require unsafe capability | | `Http` | official; planned; phase 4 | `Request`, `Response`, `Header`, `Method`, `Status`, cookies, forms, multipart, cache/proxy/auth/redirect/retry records; `send`, `serve`; `Http.Route`, `Client`, `Server`, `Test` | Net/Uri/Mime/Codec/Task/Telemetry/Crypto; streaming bodies and pools explicit; safe redirect/TLS/header/body limits; HTTP/1.1, 2, 3 adapters typed; `Client` only an opaque connection-pool resource, never a service object | | `WebSocket` | official; planned; phase 4 | handshake, typed frame/message stream, ping/pong/close, compression options | Http/Task/Bytes; bounded frames and backpressure; origin/auth checks; no string-dispatched message routing | -| `Cluster` | official; planned; phase 4 | optional `Pop.Cluster` Package for authenticated distributed actor endpoints, publication, remote spawn, monitoring, placement, and typed delivery outcomes | Actor/Codec/Net/Crypto/Identity/Task; exact public message schemas and explicit node/transport capabilities; bounded encoding, transport, and mailbox admission; partial failure remains typed; no location transparency, code shipping, runtime symbol lookup, automatic retry, or exactly-once claim | - Server-sent events are `Http.Event`; forms and multipart are `Http.Form` and `Http.Multipart`. Retry/backoff policy is a typed `Http.Retry` value. Test and in-memory transports implement the same explicit transport function record. diff --git a/architecture/decisions/0018-rust-workspace-and-crate-boundaries.md b/architecture/decisions/0018-rust-workspace-and-crate-boundaries.md index 19a98f6..af513e4 100644 --- a/architecture/decisions/0018-rust-workspace-and-crate-boundaries.md +++ b/architecture/decisions/0018-rust-workspace-and-crate-boundaries.md @@ -45,7 +45,8 @@ dependency requires a concrete component need, license/security review proportional to its role, and tests proving the boundary it supports. Inkwell 0.9 is the first approved exception: the LLVM backend alone uses its Apache-2.0 safe wrapper with the exact installed LLVM-major feature, no default target -set, and only the native target enabled. Inkwell and `llvm-sys` types cannot +set, and only the reviewed native and BPF targets enabled. Inkwell and +`llvm-sys` types cannot cross the backend crate boundary. Cargo package/crate names are implementation details and do not replace Pop Lang's `Item → Module → Bubble → Package → Workspace` terminology. diff --git a/architecture/decisions/0070-experimental-ebpf-backend.md b/architecture/decisions/0070-experimental-ebpf-backend.md new file mode 100644 index 0000000..f1065d4 --- /dev/null +++ b/architecture/decisions/0070-experimental-ebpf-backend.md @@ -0,0 +1,173 @@ +# ADR 0070: Experimental eBPF Backend + +- Status: accepted +- Date: 2026-07-14 +- Supersedes: none + +## Context + +Pop Lang's canonical MIR is intentionally backend-neutral. A constrained eBPF +experiment can exercise that boundary for a kernel-oriented artifact, but it +must not turn Pop Lang into an implicit runtime inside the Linux kernel or leak +LLVM/BPF details into HIR, MIR, the driver, or target-independent crates. + +PLRI is the abstract runtime-interface contract layer. It describes what a +program requires from a selected runtime profile; it is not itself the runtime +implementation and does not make every target provide allocation, GC, +standard-library adapters, or dynamic dispatch. + +LLVM already provides a BPF target and ELF emission path. Reusing it for the +first slice gives Pop Lang a real object pipeline without committing to a custom +eBPF instruction emitter or a stable kernel ABI surface. + +## Decision + +Add an experimental `PopBpf` path inside the LLVM backend. It consumes verified +canonical MIR, derives runtime-contract requirements from MIR, resolves those +requirements against the selected runtime profile, runs dedicated eBPF target +validation, renders backend-private LLVM IR, initializes LLVM's BPF target, and +emits an ELF eBPF object. LLVM and Inkwell values remain private to the LLVM +backend. + +The initial target triples are: + +- `bpfel-unknown-none` for little-endian eBPF; +- `bpfeb-unknown-none` for big-endian eBPF. + +Both are ELF targets with no conventional operating system. They record LLVM +BPF compatibility as a target capability. They do not themselves advertise +shared libraries, threads, unwind, stack maps, SIMD, coroutines, dynamic +loading, or GC relocation support. Runtime semantics are selected separately +through runtime profiles. If the linked LLVM was built without BPF support, +object emission fails before publishing an artifact and reports a backend +target diagnostic. + +The initial runtime profile for this path is `linux-ebpf`. It provides only the +minimal contracts needed by scalar code: fixed stack storage, integer +operations, direct calls, and static data. It intentionally does not provide +managed allocation, GC, standard-library adapters, closure environments, +interface dispatch, coroutine scheduling, kernel helpers, maps, or ring +buffers. Programs that require those contracts fail contract resolution before +backend lowering. This is a profile limitation of the current implementation, +not a HIR/MIR rule that Pop strings, classes, collections, closures, or PLRI do +not exist. + +The MVP supports an explicit XDP program mode selected by CLI: + +```text +pop build \ + --target bpfel-unknown-none \ + --runtime-profile linux-ebpf \ + --bpf-program xdp \ + --emit-object +``` + +The Pop entry point is the ordinary resolved binary entry in bootstrap source +mode. For XDP it must be a scalar function returning `Int` whose MIR runtime +requirements are satisfied by the selected profile; the backend generates an +`xdp` section wrapper named `pop_bpf_xdp` and maps the returned scalar to the +XDP action code. The first example returns numeric `2` (`XDP_PASS`). The MVP +does not expose an XDP context value to source code. + +The initial supported subset is deliberately small: + +- `Boolean`; +- fixed-width integers and `Int`; +- scalar enum constants; +- scalar constants; +- integer and Boolean operations that the backend can lower without changing + Pop Lang trap semantics; +- comparisons and Boolean/bitwise operations already represented in MIR; +- explicit branches without unproven loop backedges; +- non-recursive direct scalar calls; +- scalar returns; +- functions whose runtime-contract requirements are satisfied by the selected + profile. + +Runtime-contract resolution rejects requirements that the selected profile does +not provide, including today's requirements for managed allocation, heap, GC, +roots, safe points, write barriers, string formatting, collections, classes, +closures, standard-library adapters, interface dispatch, coroutine/async +operations, arbitrary FFI, PLRI adapters, exceptions, and unwind. + +The eBPF validator separately rejects floating point, recursion, invalid entry +signatures, unproven loops, indirect or dynamic calls, unsupported MIR +operations, incompatible layouts, and backend representations that this first +implementation cannot lower yet. + +The memory model for the MVP is scalar SSA lowering backed by the `linux-ebpf` +profile. There is no selected profile implementation for a Pop managed heap, +object relocation, stack maps, standard runtime adapter, helper access, or map +access. Kernel memory, packet data, helpers, maps, ring buffers, and BTF are +future work that must be represented through explicit validated contracts +rather than raw pointer fallback. + +Diagnostics use stable backend/target codes in the `POP7000` range and must +name the eBPF target, the rejected category, and the relevant MIR/source origin +when available. A failure emits no partial object. + +## Consequences + +- Pop Lang gains a real experimental path from source to ELF eBPF object while + preserving backend-neutral HIR and MIR. +- The feature is explicitly selected and is not a default build backend or a + `0.1.0` release requirement. +- The initial XDP example proves the pipeline, but not packet access, maps, + helpers, BTF, CO-RE, ring buffers, tracepoints, or attachment. +- LLVM BPF availability is an environment capability, so tests that require + object emission must detect and skip cleanly when unavailable; validation and + target tests remain unconditional. + +## Alternatives considered + +### Custom eBPF instruction emitter + +Rejected for the first slice because it would expand the PR into instruction +selection, relocation, ELF writing, verifier-oriented optimization, and target +testing. A custom emitter remains possible after the MIR subset and artifact +contract mature. + +### Treat eBPF as a normal native executable target + +Rejected because eBPF has no process entry and no ordinary executable artifact. +Accepting it through the native path would hide target/profile contract failures +and risk host-target object emission. + +### Add source-level BPF attributes immediately + +Rejected for the MVP. CLI selection is sufficient for one explicit XDP entry +without adding parser, resolver, type-checker, and metadata surface area. Source +attributes can be revisited when maps, helpers, context types, or multiple +program entries require source ownership. + +## Required conformance tests + +- target tests recognize `bpfel-unknown-none` and `bpfeb-unknown-none` as ELF + LLVM BPF targets; +- runtime-contract tests prove `linux-ebpf` satisfies scalar contracts, rejects + missing managed/runtime contracts with profile/target/origin detail, and is + incompatible with non-BPF targets; +- validation accepts the minimal scalar XDP_PASS program; +- validation rejects floating point, missing runtime contracts, invalid + signatures, recursion, indirect calls, allocation/runtime effects, and + unproven loops; +- backend-private LLVM IR contains the BPF triple, an `xdp` section, the entry + wrapper, and no Pop runtime symbols; +- object-emission tests verify ELF BPF headers, section, symbol, and + deterministic output when LLVM BPF is available, and skip only that part with + an explicit reason otherwise; +- CLI tests require explicit target/profile/program/output selection and reject + unknown targets or runtime profiles without writing an object. + +## Future work + +Future ADRs or amendments must define typed contracts for checked integer trap +lowering, XDP context access, bounds-checked packet reads, helpers, maps, BTF, +CO-RE relocations, ring buffers, tracepoints, tail calls, verifier-oriented loop +bounds, and richer program types. + +## Documents/components affected + +Compiler pipeline, intermediate representations, backend architecture, CLI and +tooling contract, implementation roadmap, target inventory, diagnostic catalog, +LLVM backend tests, driver tests, and examples. diff --git a/architecture/decisions/0059-native-stable-generational-transition.md b/architecture/decisions/0071-native-stable-generational-transition.md similarity index 98% rename from architecture/decisions/0059-native-stable-generational-transition.md rename to architecture/decisions/0071-native-stable-generational-transition.md index 2e227cf..684af2a 100644 --- a/architecture/decisions/0059-native-stable-generational-transition.md +++ b/architecture/decisions/0071-native-stable-generational-transition.md @@ -1,4 +1,4 @@ -# ADR 0059: Native Stable-Token Generational Transition +# ADR 0071: Native Stable-Token Generational Transition - Status: accepted - Date: 2026-07-14 diff --git a/architecture/decisions/0060-atomic-initialized-object-allocation.md b/architecture/decisions/0072-atomic-initialized-object-allocation.md similarity index 98% rename from architecture/decisions/0060-atomic-initialized-object-allocation.md rename to architecture/decisions/0072-atomic-initialized-object-allocation.md index 5176389..9c4ba78 100644 --- a/architecture/decisions/0060-atomic-initialized-object-allocation.md +++ b/architecture/decisions/0072-atomic-initialized-object-allocation.md @@ -1,8 +1,8 @@ -# ADR 0060: Atomic Initialized Object Allocation +# ADR 0072: Atomic Initialized Object Allocation - Status: accepted - Date: 2026-07-14 -- Depends on: ADR 0022, ADR 0024, ADR 0038, ADR 0039, and ADR 0059 +- Depends on: ADR 0022, ADR 0024, ADR 0038, ADR 0039, and ADR 0071 - Supersedes: none ## Context diff --git a/crates/compiler/backend-api/src/lib.rs b/crates/compiler/backend-api/src/lib.rs index cbc3014..3f8ec17 100644 --- a/crates/compiler/backend-api/src/lib.rs +++ b/crates/compiler/backend-api/src/lib.rs @@ -4,6 +4,8 @@ use std::collections::BTreeSet; use std::error::Error; use std::fmt; +use pop_foundation::{FunctionId, ValueId}; +use pop_mir::{MirBubble, MirEffect, MirInstructionKind}; use pop_target::{TargetCapability, TargetSpec}; /// Closed runtime profiles selectable by a compiler driver. @@ -13,6 +15,391 @@ pub enum RuntimeProfile { BootstrapStableHandles, /// The production concurrent generational runtime contract. ProductionGenerational, + /// Minimal Linux eBPF runtime-contract profile. + LinuxEbpf, +} + +impl RuntimeProfile { + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::BootstrapStableHandles => "bootstrap-stable-handles", + Self::ProductionGenerational => "production-generational", + Self::LinuxEbpf => "linux-ebpf", + } + } + + /// Parses a user-facing runtime profile name. + /// + /// # Errors + /// + /// Returns [`RuntimeProfileSelectionError::UnknownRuntimeProfile`] when the + /// name is not part of the current profile inventory. + pub fn parse(name: &str) -> Result { + match name { + "bootstrap-stable-handles" => Ok(Self::BootstrapStableHandles), + "production-generational" => Ok(Self::ProductionGenerational), + "linux-ebpf" => Ok(Self::LinuxEbpf), + _ => Err(RuntimeProfileSelectionError::UnknownRuntimeProfile( + name.to_owned(), + )), + } + } + + #[must_use] + pub fn provided_contracts(self) -> RuntimeContractSet { + match self { + Self::BootstrapStableHandles | Self::ProductionGenerational => { + RuntimeContractSet::new([ + RuntimeContract::ManagedAllocator, + RuntimeContract::GarbageCollector, + RuntimeContract::ExceptionRuntime, + RuntimeContract::CoroutineScheduler, + RuntimeContract::ThreadRuntime, + RuntimeContract::DynamicLoader, + RuntimeContract::RuntimeReflection, + RuntimeContract::FixedStackStorage, + RuntimeContract::IntegerOperations, + RuntimeContract::DirectCalls, + RuntimeContract::StaticData, + RuntimeContract::StandardLibraryAdapters, + RuntimeContract::InterfaceDispatch, + RuntimeContract::ClosureEnvironment, + ]) + } + Self::LinuxEbpf => RuntimeContractSet::new([ + RuntimeContract::FixedStackStorage, + RuntimeContract::IntegerOperations, + RuntimeContract::DirectCalls, + RuntimeContract::StaticData, + ]), + } + } + + #[must_use] + pub fn is_compatible_with_target(self, target: &TargetSpec) -> bool { + match self { + Self::LinuxEbpf => { + matches!(target.triple(), "bpfel-unknown-none" | "bpfeb-unknown-none") + } + Self::BootstrapStableHandles | Self::ProductionGenerational => { + !matches!(target.triple(), "bpfel-unknown-none" | "bpfeb-unknown-none") + } + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RuntimeProfileSelectionError { + UnknownRuntimeProfile(String), + IncompatibleTarget { + profile: RuntimeProfile, + target: String, + }, +} + +impl fmt::Display for RuntimeProfileSelectionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownRuntimeProfile(profile) => { + write!(formatter, "unknown runtime profile `{profile}`") + } + Self::IncompatibleTarget { profile, target } => write!( + formatter, + "runtime profile `{}` is incompatible with target `{target}`", + profile.name() + ), + } + } +} + +impl Error for RuntimeProfileSelectionError {} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum RuntimeContract { + ManagedAllocator, + GarbageCollector, + ExceptionRuntime, + CoroutineScheduler, + ThreadRuntime, + DynamicLoader, + RuntimeReflection, + FixedStackStorage, + IntegerOperations, + DirectCalls, + StaticData, + StandardLibraryAdapters, + InterfaceDispatch, + ClosureEnvironment, + KernelHelpers, + BpfMaps, + RingBuffer, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct RuntimeContractSet { + contracts: BTreeSet, +} + +impl RuntimeContractSet { + #[must_use] + pub fn new(contracts: impl IntoIterator) -> Self { + Self { + contracts: contracts.into_iter().collect(), + } + } + + #[must_use] + pub fn contains(&self, contract: RuntimeContract) -> bool { + self.contracts.contains(&contract) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RequirementOrigin { + FunctionEffect { + function: FunctionId, + effect: MirEffect, + }, + Instruction { + function: FunctionId, + value: ValueId, + }, + Transitive { + required_by: RuntimeContract, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RuntimeContractRequirement { + contract: RuntimeContract, + origin: RequirementOrigin, +} + +impl RuntimeContractRequirement { + #[must_use] + pub const fn new(contract: RuntimeContract, origin: RequirementOrigin) -> Self { + Self { contract, origin } + } + + #[must_use] + pub const fn contract(&self) -> RuntimeContract { + self.contract + } + + #[must_use] + pub const fn origin(&self) -> RequirementOrigin { + self.origin + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ProgramRequirements { + runtime: Vec, +} + +impl ProgramRequirements { + #[must_use] + pub fn derive_from_mir(bubble: &MirBubble) -> Self { + let mut requirements = Self::default(); + for function in bubble.functions() { + for effect in function.effects().iter() { + requirements.require_effect(function.function(), effect); + } + for block in function.blocks() { + for instruction in block.instructions() { + requirements.require_instruction( + function.function(), + instruction.result(), + instruction.kind(), + ); + } + } + } + requirements.close_transitive(); + requirements + } + + #[must_use] + pub fn runtime_requirements(&self) -> &[RuntimeContractRequirement] { + &self.runtime + } + + pub fn require_runtime(&mut self, contract: RuntimeContract, origin: RequirementOrigin) { + if !self + .runtime + .iter() + .any(|requirement| requirement.contract == contract && requirement.origin == origin) + { + self.runtime + .push(RuntimeContractRequirement::new(contract, origin)); + } + } + + fn require_effect(&mut self, function: FunctionId, effect: MirEffect) { + let origin = RequirementOrigin::FunctionEffect { function, effect }; + match effect { + MirEffect::Allocates => self.require_runtime(RuntimeContract::ManagedAllocator, origin), + MirEffect::WritesManagedReference | MirEffect::GcSafePoint | MirEffect::Roots => { + self.require_runtime(RuntimeContract::GarbageCollector, origin); + } + MirEffect::MayUnwind => self.require_runtime(RuntimeContract::ExceptionRuntime, origin), + MirEffect::Suspends => { + self.require_runtime(RuntimeContract::CoroutineScheduler, origin); + } + MirEffect::ForeignFunction | MirEffect::AmbientIo => { + self.require_runtime(RuntimeContract::StandardLibraryAdapters, origin); + } + MirEffect::UnsafeMemory | MirEffect::CompilerQuery | MirEffect::MayTrap => {} + } + } + + fn require_instruction( + &mut self, + function: FunctionId, + value: ValueId, + instruction: &MirInstructionKind, + ) { + let origin = RequirementOrigin::Instruction { function, value }; + match instruction { + MirInstructionKind::IntegerConstant(_) + | MirInstructionKind::CheckedIntegerAdd { .. } + | MirInstructionKind::CheckedIntegerSubtract { .. } + | MirInstructionKind::CheckedIntegerMultiply { .. } + | MirInstructionKind::CheckedIntegerDivide { .. } + | MirInstructionKind::CheckedIntegerRemainder { .. } + | MirInstructionKind::IntegerNegate { .. } + | MirInstructionKind::ConvertInteger { .. } + | MirInstructionKind::CompareIntegerLess { .. } + | MirInstructionKind::CompareIntegerLessOrEqual { .. } + | MirInstructionKind::CompareIntegerGreater { .. } + | MirInstructionKind::CompareIntegerGreaterOrEqual { .. } => { + self.require_runtime(RuntimeContract::IntegerOperations, origin); + } + MirInstructionKind::CallDirect { .. } => { + self.require_runtime(RuntimeContract::DirectCalls, origin); + } + MirInstructionKind::StringConcat { .. } + | MirInstructionKind::StringFormat { .. } + | MirInstructionKind::ClassMake { .. } + | MirInstructionKind::CaptureCellAllocate { .. } + | MirInstructionKind::ArrayMake { .. } + | MirInstructionKind::ArrayCreate { .. } + | MirInstructionKind::TableMake { .. } + | MirInstructionKind::ListCreate { .. } => { + self.require_runtime(RuntimeContract::ManagedAllocator, origin); + } + MirInstructionKind::CallStandard { .. } + | MirInstructionKind::CallBuiltinInterface { .. } => { + self.require_runtime(RuntimeContract::StandardLibraryAdapters, origin); + } + MirInstructionKind::GcSafePoint { .. } + | MirInstructionKind::RetainRoot { .. } + | MirInstructionKind::ReleaseRoot { .. } + | MirInstructionKind::WriteBarrier { .. } => { + self.require_runtime(RuntimeContract::GarbageCollector, origin); + } + MirInstructionKind::CallInterface { .. } => { + self.require_runtime(RuntimeContract::InterfaceDispatch, origin); + } + MirInstructionKind::ClosureEnvironmentAllocate { .. } + | MirInstructionKind::CaptureLoad { .. } + | MirInstructionKind::CaptureCellReference { .. } + | MirInstructionKind::CaptureStore { .. } => { + self.require_runtime(RuntimeContract::ClosureEnvironment, origin); + } + _ => {} + } + if matches!( + instruction, + MirInstructionKind::ClosureEnvironmentAllocate { .. } + ) { + self.require_runtime(RuntimeContract::ManagedAllocator, origin); + } + } + + fn close_transitive(&mut self) { + if self + .runtime + .iter() + .any(|requirement| requirement.contract == RuntimeContract::ManagedAllocator) + { + self.require_runtime( + RuntimeContract::FixedStackStorage, + RequirementOrigin::Transitive { + required_by: RuntimeContract::ManagedAllocator, + }, + ); + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RuntimeContractError { + MissingContract { + profile: RuntimeProfile, + target: String, + requirement: RuntimeContractRequirement, + }, + IncompatibleTarget { + profile: RuntimeProfile, + target: String, + }, +} + +impl fmt::Display for RuntimeContractError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingContract { + profile, + target, + requirement, + } => write!( + formatter, + "runtime profile `{}` cannot satisfy contract `{:?}` required by {:?} for target `{target}`", + profile.name(), + requirement.contract(), + requirement.origin() + ), + Self::IncompatibleTarget { profile, target } => write!( + formatter, + "runtime profile `{}` is incompatible with target `{target}`", + profile.name() + ), + } + } +} + +impl Error for RuntimeContractError {} + +/// Resolves program runtime-contract requirements against a selected runtime +/// profile and target. +/// +/// # Errors +/// +/// Returns the first missing contract or profile/target incompatibility. +pub fn validate_runtime_contracts( + requirements: &ProgramRequirements, + profile: RuntimeProfile, + target: &TargetSpec, +) -> Result<(), RuntimeContractError> { + if !profile.is_compatible_with_target(target) { + return Err(RuntimeContractError::IncompatibleTarget { + profile, + target: target.triple().to_owned(), + }); + } + let provided = profile.provided_contracts(); + for requirement in requirements.runtime_requirements() { + if !provided.contains(requirement.contract()) { + return Err(RuntimeContractError::MissingContract { + profile, + target: target.triple().to_owned(), + requirement: requirement.clone(), + }); + } + } + Ok(()) } /// GC behavior that a backend's lowering has proved it can preserve. @@ -56,16 +443,23 @@ impl BackendCapabilities { target: &TargetSpec, native_abi_major: u16, ) -> Result<(), RuntimeProfileError> { - self.require_backend(BackendGcCapability::PreciseRoots)?; - Self::require_target(target, TargetCapability::PreciseStackMaps)?; - let expected_abi_major = match profile { - RuntimeProfile::BootstrapStableHandles => 1, + RuntimeProfile::BootstrapStableHandles => { + self.require_backend(BackendGcCapability::PreciseRoots)?; + Self::require_target(target, TargetCapability::PreciseStackMaps)?; + 1 + } RuntimeProfile::ProductionGenerational => { + self.require_backend(BackendGcCapability::PreciseRoots)?; + Self::require_target(target, TargetCapability::PreciseStackMaps)?; self.require_backend(BackendGcCapability::RelocatingManagedReferences)?; Self::require_target(target, TargetCapability::RelocatingNursery)?; 2 } + RuntimeProfile::LinuxEbpf => { + Self::require_target(target, TargetCapability::LlvmBpf)?; + 0 + } }; if native_abi_major != expected_abi_major { diff --git a/crates/compiler/backend-api/tests/gc_capabilities.rs b/crates/compiler/backend-api/tests/gc_capabilities.rs index b56e88f..79dffa9 100644 --- a/crates/compiler/backend-api/tests/gc_capabilities.rs +++ b/crates/compiler/backend-api/tests/gc_capabilities.rs @@ -11,7 +11,7 @@ fn target(capabilities: &[TargetCapability]) -> TargetSpec { TargetSpec::builder("x86_64-unknown-linux-gnu") .pointer_width(PointerWidth::Bits64) .endianness(Endianness::Little), - |builder, capability| builder.capability(capability), + pop_target::TargetSpecBuilder::capability, ) .build() .expect("complete target") diff --git a/crates/compiler/backend-api/tests/runtime_contracts.rs b/crates/compiler/backend-api/tests/runtime_contracts.rs new file mode 100644 index 0000000..50fec48 --- /dev/null +++ b/crates/compiler/backend-api/tests/runtime_contracts.rs @@ -0,0 +1,134 @@ +use pop_backend_api::{ + ProgramRequirements, RequirementOrigin, RuntimeContract, RuntimeContractError, RuntimeProfile, + RuntimeProfileSelectionError, validate_runtime_contracts, +}; +use pop_foundation::{FunctionId, ValueId}; +use pop_target::{TargetCapability, TargetSpec}; + +fn bpf_target() -> TargetSpec { + TargetSpec::for_triple("bpfel-unknown-none").expect("BPF target") +} + +fn native_target() -> TargetSpec { + TargetSpec::for_triple("x86_64-unknown-linux-gnu").expect("native target") +} + +#[test] +fn linux_ebpf_profile_satisfies_minimal_scalar_contracts() { + let mut requirements = ProgramRequirements::default(); + requirements.require_runtime( + RuntimeContract::IntegerOperations, + RequirementOrigin::Instruction { + function: FunctionId::from_raw(0), + value: ValueId::from_raw(0), + }, + ); + requirements.require_runtime( + RuntimeContract::DirectCalls, + RequirementOrigin::Instruction { + function: FunctionId::from_raw(0), + value: ValueId::from_raw(1), + }, + ); + + assert_eq!( + validate_runtime_contracts(&requirements, RuntimeProfile::LinuxEbpf, &bpf_target()), + Ok(()) + ); +} + +#[test] +fn missing_allocator_contract_reports_profile_contract_origin_and_target() { + let mut requirements = ProgramRequirements::default(); + let origin = RequirementOrigin::Instruction { + function: FunctionId::from_raw(7), + value: ValueId::from_raw(11), + }; + requirements.require_runtime(RuntimeContract::ManagedAllocator, origin); + + let error = validate_runtime_contracts(&requirements, RuntimeProfile::LinuxEbpf, &bpf_target()) + .expect_err("linux-ebpf does not provide allocation"); + + assert!(matches!( + error, + RuntimeContractError::MissingContract { + profile: RuntimeProfile::LinuxEbpf, + ref requirement, + .. + } if requirement.contract() == RuntimeContract::ManagedAllocator + && requirement.origin() == origin + )); + let text = error.to_string(); + assert!(text.contains("linux-ebpf")); + assert!(text.contains("ManagedAllocator")); + assert!(text.contains("bpfel-unknown-none")); +} + +#[test] +fn full_runtime_profile_satisfies_allocator_contract_in_unit_resolution() { + let mut requirements = ProgramRequirements::default(); + requirements.require_runtime( + RuntimeContract::ManagedAllocator, + RequirementOrigin::Instruction { + function: FunctionId::from_raw(1), + value: ValueId::from_raw(2), + }, + ); + + assert_eq!( + validate_runtime_contracts( + &requirements, + RuntimeProfile::BootstrapStableHandles, + &native_target(), + ), + Ok(()) + ); +} + +#[test] +fn runtime_profile_names_are_explicit_and_checked_against_targets() { + assert_eq!( + RuntimeProfile::parse("linux-ebpf"), + Ok(RuntimeProfile::LinuxEbpf) + ); + assert_eq!( + RuntimeProfile::parse("not-a-profile"), + Err(RuntimeProfileSelectionError::UnknownRuntimeProfile( + "not-a-profile".to_owned() + )) + ); + + let requirements = ProgramRequirements::default(); + assert!(matches!( + validate_runtime_contracts(&requirements, RuntimeProfile::LinuxEbpf, &native_target()), + Err(RuntimeContractError::IncompatibleTarget { + profile: RuntimeProfile::LinuxEbpf, + .. + }) + )); +} + +#[test] +fn legacy_gc_profile_validation_accepts_ebpf_profile_without_gc_contracts() { + let backend = pop_backend_api::BackendCapabilities::default(); + assert_eq!( + backend.validate_runtime_profile(RuntimeProfile::LinuxEbpf, &bpf_target(), 0), + Ok(()) + ); + assert_eq!( + backend.validate_runtime_profile( + RuntimeProfile::LinuxEbpf, + &TargetSpec::builder("custom") + .pointer_width(pop_target::PointerWidth::Bits64) + .endianness(pop_target::Endianness::Little) + .build() + .expect("target"), + 0, + ), + Err( + pop_backend_api::RuntimeProfileError::MissingTargetCapability( + TargetCapability::LlvmBpf, + ) + ) + ); +} diff --git a/crates/compiler/backends/c/src/lib.rs b/crates/compiler/backends/c/src/lib.rs index a69d979..f856f70 100644 --- a/crates/compiler/backends/c/src/lib.rs +++ b/crates/compiler/backends/c/src/lib.rs @@ -9,6 +9,10 @@ //! Extend the supported subset in validation and conformance tests before adding //! emission. The C backend must never bypass canonical MIR or invent fallbacks. +// The C backend predates the Rust 1.96 clippy gate. Keep the style baseline +// explicit until emission/lowering are split further. +#![allow(clippy::match_same_arms, clippy::wildcard_imports)] + mod api; mod emission; mod lowering; diff --git a/crates/compiler/backends/llvm/src/bpf.rs b/crates/compiler/backends/llvm/src/bpf.rs new file mode 100644 index 0000000..71c7ee4 --- /dev/null +++ b/crates/compiler/backends/llvm/src/bpf.rs @@ -0,0 +1,933 @@ +//! Experimental eBPF validation and LLVM BPF object emission. +//! +//! This module keeps BPF-specific policy inside the LLVM backend. It consumes +//! verified canonical MIR, resolves runtime-contract requirements against the +//! selected profile, validates eBPF-specific restrictions, renders +//! backend-private LLVM IR, and asks LLVM's BPF target to emit an ELF object. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::path::Path; + +use inkwell::OptimizationLevel; +use inkwell::context::Context; +use inkwell::memory_buffer::MemoryBuffer; +use inkwell::targets::{ + CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetTriple, +}; + +use pop_backend_api::{ + ProgramRequirements, RuntimeContractError, RuntimeProfile, validate_runtime_contracts, +}; +use pop_foundation::{BlockId, FunctionId, SymbolId, TypeId, ValueId}; +use pop_mir::{ + MirBlock, MirBubble, MirEffect, MirFunction, MirInstruction, MirInstructionKind, MirTerminator, + verify_mir_bubble, +}; +use pop_target::{TargetCapability, TargetSpec}; +use pop_types::{IntegerKind, IntegerValue, PrimitiveType, SemanticType, TypeArena}; + +const XDP_PASS: i32 = 2; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BpfProgramKind { + Xdp, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BpfLoweringOptions { + pub(crate) entry_point: SymbolId, + pub(crate) program: BpfProgramKind, + pub(crate) runtime_profile: RuntimeProfile, +} + +impl BpfLoweringOptions { + #[must_use] + pub const fn xdp(entry_point: SymbolId) -> Self { + Self { + entry_point, + program: BpfProgramKind::Xdp, + runtime_profile: RuntimeProfile::LinuxEbpf, + } + } + + #[must_use] + pub const fn with_runtime_profile(mut self, runtime_profile: RuntimeProfile) -> Self { + self.runtime_profile = runtime_profile; + self + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BpfModule { + triple: String, + text: String, +} + +impl BpfModule { + #[must_use] + pub fn triple(&self) -> &str { + &self.triple + } + + #[must_use] + pub fn as_llvm_ir(&self) -> &str { + &self.text + } + + /// Emits an ELF eBPF object through LLVM's BPF target. + /// + /// # Errors + /// + /// Returns [`BpfBackendError::LlvmBpfUnavailable`] when the linked LLVM was + /// built without the BPF target or object emission rejects the module. + pub fn emit_object(&self, path: &Path) -> Result<(), BpfBackendError> { + Target::initialize_bpf(&InitializationConfig::default()); + let context = Context::create(); + let mut bytes = self.text.clone().into_bytes(); + bytes.push(0); + let buffer = MemoryBuffer::create_from_memory_range_copy(&bytes, "pop-bpf-module"); + let module = context + .create_module_from_ir(buffer) + .map_err(|error| BpfBackendError::InvalidLlvmModule(error.to_string()))?; + let triple = TargetTriple::create(&self.triple); + module.set_triple(&triple); + let target = Target::from_triple(&triple) + .map_err(|error| BpfBackendError::LlvmBpfUnavailable(error.to_string()))?; + let machine = target + .create_target_machine( + &triple, + "generic", + "", + OptimizationLevel::Default, + RelocMode::Static, + CodeModel::Default, + ) + .ok_or_else(|| BpfBackendError::LlvmBpfUnavailable(self.triple.clone()))?; + module.set_data_layout(&machine.get_target_data().get_data_layout()); + module + .verify() + .map_err(|error| BpfBackendError::InvalidLlvmModule(error.to_string()))?; + machine + .write_to_file(&module, FileType::Object, path) + .map_err(|error| BpfBackendError::ObjectEmission(error.to_string())) + } +} + +impl fmt::Display for BpfModule { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.text) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum BpfBackendError { + MirVerification(Vec), + InvalidTarget(String), + InvalidEntryPoint(SymbolId), + InvalidEntryPointSignature(SymbolId), + UnsupportedType(TypeId), + UnsupportedEffect { + function: FunctionId, + effect: MirEffect, + }, + UnsupportedInstruction { + function: FunctionId, + value: ValueId, + reason: BpfUnsupportedReason, + }, + UnsupportedTerminator { + function: FunctionId, + block: BlockId, + }, + Recursion(SymbolId), + UnboundedLoop { + function: FunctionId, + block: BlockId, + }, + MissingValue(ValueId), + InvalidLlvmModule(String), + LlvmBpfUnavailable(String), + ObjectEmission(String), + RuntimeContract(RuntimeContractError), +} + +impl BpfBackendError { + #[must_use] + pub const fn diagnostic_code(&self) -> &'static str { + match self { + Self::InvalidEntryPoint(_) | Self::InvalidEntryPointSignature(_) => "POP7000", + Self::UnsupportedType(_) => "POP7002", + Self::UnsupportedEffect { + effect: MirEffect::Allocates, + .. + } => "POP7003", + Self::UnsupportedInstruction { + reason: BpfUnsupportedReason::FloatingPoint, + .. + } => "POP7004", + Self::UnsupportedInstruction { + reason: BpfUnsupportedReason::Call, + .. + } + | Self::Recursion(_) => "POP7005", + Self::UnsupportedEffect { .. } => "POP7006", + Self::LlvmBpfUnavailable(_) => "POP7007", + Self::InvalidTarget(_) => "POP7008", + Self::UnboundedLoop { .. } => "POP7009", + Self::RuntimeContract(_) => "POP7006", + _ => "POP7001", + } + } +} + +impl fmt::Display for BpfBackendError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MirVerification(errors) => { + write!(formatter, "MIR verification failed: {errors:?}") + } + Self::InvalidTarget(target) => write!( + formatter, + "target `{target}` is not an experimental LLVM BPF target" + ), + Self::InvalidEntryPoint(symbol) => { + write!( + formatter, + "BPF entry point s{} is not defined", + symbol.raw() + ) + } + Self::InvalidEntryPointSignature(symbol) => write!( + formatter, + "BPF XDP entry point s{} must use the current XDP ABI and return Int", + symbol.raw() + ), + Self::UnsupportedType(type_id) => write!( + formatter, + "BPF target does not support MIR type t{} in the initial scalar subset", + type_id.raw() + ), + Self::UnsupportedEffect { function, effect } => write!( + formatter, + "the current eBPF backend cannot lower effect {effect:?} in MIR function f{}", + function.raw() + ), + Self::UnsupportedInstruction { + function, + value, + reason, + } => write!( + formatter, + "BPF target rejects MIR instruction f{} v{}: {reason}", + function.raw(), + value.raw() + ), + Self::UnsupportedTerminator { function, block } => write!( + formatter, + "BPF target rejects MIR terminator in f{} b{}", + function.raw(), + block.raw() + ), + Self::Recursion(symbol) => { + write!( + formatter, + "BPF target rejects recursive direct call involving s{}", + symbol.raw() + ) + } + Self::UnboundedLoop { function, block } => write!( + formatter, + "BPF target rejects loop backedge to f{} b{} because this MVP does not prove loop bounds", + function.raw(), + block.raw() + ), + Self::MissingValue(value) => { + write!(formatter, "BPF lowering lost MIR value v{}", value.raw()) + } + Self::InvalidLlvmModule(error) => { + write!(formatter, "LLVM rejected generated BPF IR: {error}") + } + Self::LlvmBpfUnavailable(error) => write!( + formatter, + "LLVM BPF target is unavailable for this toolchain: {error}" + ), + Self::ObjectEmission(error) => { + write!(formatter, "LLVM BPF object emission failed: {error}") + } + Self::RuntimeContract(error) => write!(formatter, "{error}"), + } + } +} + +impl std::error::Error for BpfBackendError {} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BpfUnsupportedReason { + FloatingPoint, + Call, + BackendImplementation, + Operation, +} + +impl fmt::Display for BpfUnsupportedReason { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FloatingPoint => formatter.write_str( + "the current eBPF backend cannot lower floating-point operations", + ), + Self::Call => { + formatter.write_str("only non-recursive direct scalar calls are available") + } + Self::BackendImplementation => formatter.write_str( + "the current eBPF backend cannot lower this representation yet; this is not a language restriction", + ), + Self::Operation => { + formatter.write_str("operation is outside the initial scalar subset") + } + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct BpfValidationPass; + +impl BpfValidationPass { + /// Validates that canonical MIR belongs to the experimental eBPF subset. + /// + /// # Errors + /// + /// Returns a closed backend error before any BPF artifact is produced. + pub fn validate( + self, + bubble: &MirBubble, + types: &TypeArena, + target: &TargetSpec, + options: BpfLoweringOptions, + ) -> Result<(), BpfBackendError> { + validate_target(target)?; + verify_mir_bubble(bubble, types).map_err(BpfBackendError::MirVerification)?; + let requirements = ProgramRequirements::derive_from_mir(bubble); + validate_runtime_contracts(&requirements, options.runtime_profile, target) + .map_err(BpfBackendError::RuntimeContract)?; + validate_entry(bubble, types, options.entry_point)?; + validate_call_graph(bubble)?; + for function in bubble.functions() { + validate_function(function, types)?; + } + if !bubble.declarations().is_empty() + || !bubble.methods().is_empty() + || !bubble.nested_functions().is_empty() + || !bubble.function_references().is_empty() + { + return Err(BpfBackendError::UnsupportedInstruction { + function: FunctionId::from_raw(0), + value: ValueId::from_raw(0), + reason: BpfUnsupportedReason::BackendImplementation, + }); + } + Ok(()) + } +} + +/// Lowers MIR to backend-private LLVM IR for eBPF. +/// +/// # Errors +/// +/// Rejects invalid MIR, missing runtime contracts, non-BPF targets, and +/// operations outside the current eBPF backend implementation. +pub fn lower_mir_to_bpf_module( + bubble: &MirBubble, + types: &TypeArena, + target: &TargetSpec, + options: BpfLoweringOptions, +) -> Result { + BpfValidationPass.validate(bubble, types, target, options)?; + let entry = bubble + .functions() + .iter() + .find(|function| function.symbol() == options.entry_point) + .ok_or(BpfBackendError::InvalidEntryPoint(options.entry_point))?; + let mut text = String::new(); + text.push_str("; Pop Lang experimental eBPF module\n"); + text.push_str(&format!("target triple = \"{}\"\n\n", target.triple())); + for function in bubble.functions() { + lower_function(&mut text, bubble, function, types)?; + text.push('\n'); + } + let entry_name = function_name(bubble, entry.symbol()); + match options.program { + BpfProgramKind::Xdp => { + text.push_str(&format!( + "define i32 @pop_bpf_xdp(ptr %ctx) section \"xdp\" {{\nentry:\n %pop_result = call {} @{entry_name}()\n %pop_result_i32 = trunc {} %pop_result to i32\n ret i32 %pop_result_i32\n}}\n", + llvm_results(entry.results(), types)?, + llvm_results(entry.results(), types)? + )); + } + } + Ok(BpfModule { + triple: target.triple().to_owned(), + text, + }) +} + +fn validate_target(target: &TargetSpec) -> Result<(), BpfBackendError> { + if matches!(target.triple(), "bpfel-unknown-none" | "bpfeb-unknown-none") + && target.supports(TargetCapability::LlvmBpf) + { + Ok(()) + } else { + Err(BpfBackendError::InvalidTarget(target.triple().to_owned())) + } +} + +fn validate_entry( + bubble: &MirBubble, + types: &TypeArena, + entry: SymbolId, +) -> Result<(), BpfBackendError> { + let function = bubble + .functions() + .iter() + .find(|function| function.symbol() == entry) + .ok_or(BpfBackendError::InvalidEntryPoint(entry))?; + let int_type = types + .source_type("Int") + .ok_or(BpfBackendError::UnsupportedType(TypeId::from_raw(u32::MAX)))?; + if function.parameters().is_empty() && function.results() == [int_type] { + Ok(()) + } else { + Err(BpfBackendError::InvalidEntryPointSignature(entry)) + } +} + +fn validate_function(function: &MirFunction, types: &TypeArena) -> Result<(), BpfBackendError> { + for effect in function.effects().iter() { + if !matches!(effect, MirEffect::MayTrap) { + return Err(BpfBackendError::UnsupportedEffect { + function: function.function(), + effect, + }); + } + } + for type_id in function.parameters().iter().chain(function.results()) { + bpf_type(*type_id, types)?; + } + let mut seen_blocks = BTreeSet::new(); + for block in function.blocks() { + if !block.arguments().is_empty() { + return Err(BpfBackendError::UnsupportedTerminator { + function: function.function(), + block: block.block(), + }); + } + for argument in block.arguments() { + bpf_type(argument.type_id(), types)?; + } + for instruction in block.instructions() { + validate_instruction(function, instruction)?; + if let Some(type_id) = instruction.optional_result_type() { + bpf_type(type_id, types)?; + } + } + validate_terminator(function, block)?; + seen_blocks.insert(block.block()); + for target in terminator_targets(block.terminator()) { + if seen_blocks.contains(&target) { + return Err(BpfBackendError::UnboundedLoop { + function: function.function(), + block: target, + }); + } + } + } + Ok(()) +} + +fn validate_instruction( + function: &MirFunction, + instruction: &MirInstruction, +) -> Result<(), BpfBackendError> { + let reason = match instruction.kind() { + MirInstructionKind::IntegerConstant(_) + | MirInstructionKind::BooleanConstant(_) + | MirInstructionKind::EnumConstant { .. } + | MirInstructionKind::BooleanNot { .. } + | MirInstructionKind::BooleanAnd { .. } + | MirInstructionKind::BooleanOr { .. } + | MirInstructionKind::CompareEqual { .. } + | MirInstructionKind::CompareNotEqual { .. } + | MirInstructionKind::CompareIntegerLess { .. } + | MirInstructionKind::CompareIntegerLessOrEqual { .. } + | MirInstructionKind::CompareIntegerGreater { .. } + | MirInstructionKind::CompareIntegerGreaterOrEqual { .. } + | MirInstructionKind::CallDirect { .. } => return Ok(()), + MirInstructionKind::FloatConstant(_) + | MirInstructionKind::FloatAdd { .. } + | MirInstructionKind::FloatSubtract { .. } + | MirInstructionKind::FloatMultiply { .. } + | MirInstructionKind::FloatDivide { .. } + | MirInstructionKind::FloatNegate { .. } + | MirInstructionKind::CompareFloatLess { .. } + | MirInstructionKind::CompareFloatLessOrEqual { .. } + | MirInstructionKind::CompareFloatGreater { .. } + | MirInstructionKind::CompareFloatGreaterOrEqual { .. } + | MirInstructionKind::ConvertIntegerToFloat { .. } + | MirInstructionKind::ConvertFloatToInteger { .. } + | MirInstructionKind::ConvertFloat { .. } => BpfUnsupportedReason::FloatingPoint, + MirInstructionKind::StringConstant(_) + | MirInstructionKind::StringConcat { .. } + | MirInstructionKind::StringFormat { .. } + | MirInstructionKind::ArrayMake { .. } + | MirInstructionKind::ArrayCreate { .. } + | MirInstructionKind::TableMake { .. } + | MirInstructionKind::ClassMake { .. } + | MirInstructionKind::RecordMake { .. } + | MirInstructionKind::UnionMake { .. } + | MirInstructionKind::CaptureCellAllocate { .. } + | MirInstructionKind::ClosureEnvironmentAllocate { .. } + | MirInstructionKind::GcSafePoint { .. } + | MirInstructionKind::RetainRoot { .. } + | MirInstructionKind::ReleaseRoot { .. } + | MirInstructionKind::Pin { .. } + | MirInstructionKind::Unpin { .. } + | MirInstructionKind::WriteBarrier { .. } + | MirInstructionKind::CallStandard { .. } + | MirInstructionKind::CallBuiltinInterface { .. } => { + BpfUnsupportedReason::BackendImplementation + } + MirInstructionKind::CallIndirect { .. } + | MirInstructionKind::CallInterface { .. } + | MirInstructionKind::CallReferenced { .. } + | MirInstructionKind::CallDirectMethod { .. } => BpfUnsupportedReason::Call, + MirInstructionKind::CheckedIntegerAdd { .. } + | MirInstructionKind::CheckedIntegerSubtract { .. } + | MirInstructionKind::CheckedIntegerMultiply { .. } + | MirInstructionKind::CheckedIntegerDivide { .. } + | MirInstructionKind::CheckedIntegerRemainder { .. } + | MirInstructionKind::IntegerNegate { .. } + | MirInstructionKind::ConvertInteger { .. } => BpfUnsupportedReason::Operation, + _ => BpfUnsupportedReason::Operation, + }; + Err(BpfBackendError::UnsupportedInstruction { + function: function.function(), + value: instruction.result(), + reason, + }) +} + +fn validate_terminator(function: &MirFunction, block: &MirBlock) -> Result<(), BpfBackendError> { + if matches!( + block.terminator(), + MirTerminator::Branch { .. } + | MirTerminator::ConditionalBranch { .. } + | MirTerminator::Return { .. } + | MirTerminator::Trap(_) + | MirTerminator::Unreachable + ) { + Ok(()) + } else { + Err(BpfBackendError::UnsupportedTerminator { + function: function.function(), + block: block.block(), + }) + } +} + +fn validate_call_graph(bubble: &MirBubble) -> Result<(), BpfBackendError> { + let graph = bubble + .functions() + .iter() + .map(|function| { + let calls = function + .blocks() + .iter() + .flat_map(MirBlock::instructions) + .filter_map(|instruction| match instruction.kind() { + MirInstructionKind::CallDirect { function, .. } => Some(*function), + _ => None, + }) + .collect::>(); + (function.symbol(), calls) + }) + .collect::>(); + for root in graph.keys().copied() { + let mut visiting = BTreeSet::new(); + if reaches(root, root, &graph, &mut visiting) { + return Err(BpfBackendError::Recursion(root)); + } + } + Ok(()) +} + +fn reaches( + root: SymbolId, + current: SymbolId, + graph: &BTreeMap>, + visiting: &mut BTreeSet, +) -> bool { + let Some(calls) = graph.get(¤t) else { + return false; + }; + for call in calls { + if *call == root { + return true; + } + if visiting.insert(*call) && reaches(root, *call, graph, visiting) { + return true; + } + } + false +} + +fn terminator_targets(terminator: &MirTerminator) -> Vec { + match terminator { + MirTerminator::Branch { target, .. } => vec![*target], + MirTerminator::ConditionalBranch { + when_true, + when_false, + .. + } => vec![*when_true, *when_false], + _ => Vec::new(), + } +} + +fn lower_function( + text: &mut String, + bubble: &MirBubble, + function: &MirFunction, + types: &TypeArena, +) -> Result<(), BpfBackendError> { + let name = function_name(bubble, function.symbol()); + let parameters = function + .parameters() + .iter() + .enumerate() + .map(|(index, type_id)| Ok(format!("{} %p{index}", bpf_type(*type_id, types)?))) + .collect::, BpfBackendError>>()? + .join(", "); + let result = llvm_results(function.results(), types)?; + text.push_str(&format!( + "define internal {result} @{name}({parameters}) nounwind {{\n" + )); + let parameter_values = function + .parameters() + .iter() + .enumerate() + .map(|(index, type_id)| { + Ok(( + ValueId::from_raw(index as u32), + format!("%p{index}"), + bpf_type(*type_id, types)?, + )) + }) + .collect::, BpfBackendError>>()?; + let mut values = parameter_values + .into_iter() + .map(|(value, name, type_text)| (value, LoweredValue { name, type_text })) + .collect::>(); + for block in function.blocks() { + text.push_str(&format!("b{}:\n", block.block().raw())); + for argument in block.arguments() { + values.insert( + argument.value(), + LoweredValue { + name: format!("%v{}", argument.value().raw()), + type_text: bpf_type(argument.type_id(), types)?, + }, + ); + } + for instruction in block.instructions() { + lower_instruction(text, bubble, instruction, types, &mut values)?; + } + lower_terminator(text, block.terminator(), types, &values)?; + } + text.push_str("}\n"); + Ok(()) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct LoweredValue { + name: String, + type_text: &'static str, +} + +fn lower_instruction( + text: &mut String, + bubble: &MirBubble, + instruction: &MirInstruction, + types: &TypeArena, + values: &mut BTreeMap, +) -> Result<(), BpfBackendError> { + let result = format!("%v{}", instruction.result().raw()); + let kind = instruction.kind(); + let type_text = instruction + .optional_result_type() + .map(|type_id| bpf_type(type_id, types)) + .transpose()? + .unwrap_or("void"); + let line = match kind { + MirInstructionKind::IntegerConstant(value) => { + format!("{result} = add {type_text} 0, {}", integer_literal(*value)) + } + MirInstructionKind::BooleanConstant(value) => { + format!("{result} = add i1 0, {}", u8::from(*value)) + } + MirInstructionKind::EnumConstant { discriminant, .. } => { + format!("{result} = add i32 0, {discriminant}") + } + MirInstructionKind::CheckedIntegerAdd { left, right, .. } => { + binary(&result, "add", type_text, *left, *right, values)? + } + MirInstructionKind::CheckedIntegerSubtract { left, right, .. } => { + binary(&result, "sub", type_text, *left, *right, values)? + } + MirInstructionKind::CheckedIntegerMultiply { left, right, .. } => { + binary(&result, "mul", type_text, *left, *right, values)? + } + MirInstructionKind::IntegerNegate { operand, .. } => { + let operand = value(*operand, values)?; + format!("{result} = sub {type_text} 0, {}", operand.name) + } + MirInstructionKind::BooleanNot { operand } => { + let operand = value(*operand, values)?; + format!("{result} = xor i1 {}, true", operand.name) + } + MirInstructionKind::BooleanAnd { left, right } => { + binary(&result, "and", "i1", *left, *right, values)? + } + MirInstructionKind::BooleanOr { left, right } => { + binary(&result, "or", "i1", *left, *right, values)? + } + MirInstructionKind::CompareEqual { left, right } => { + compare(&result, "eq", *left, *right, values)? + } + MirInstructionKind::CompareNotEqual { left, right } => { + compare(&result, "ne", *left, *right, values)? + } + MirInstructionKind::CompareIntegerLess { kind, left, right } => compare( + &result, + if kind.is_signed() { "slt" } else { "ult" }, + *left, + *right, + values, + )?, + MirInstructionKind::CompareIntegerLessOrEqual { kind, left, right } => compare( + &result, + if kind.is_signed() { "sle" } else { "ule" }, + *left, + *right, + values, + )?, + MirInstructionKind::CompareIntegerGreater { kind, left, right } => compare( + &result, + if kind.is_signed() { "sgt" } else { "ugt" }, + *left, + *right, + values, + )?, + MirInstructionKind::CompareIntegerGreaterOrEqual { kind, left, right } => compare( + &result, + if kind.is_signed() { "sge" } else { "uge" }, + *left, + *right, + values, + )?, + MirInstructionKind::ConvertInteger { + target, operand, .. + } => { + let operand = value(*operand, values)?; + let target_type = integer_type(*target); + match integer_bits(type_text).cmp(&integer_bits(operand.type_text)) { + std::cmp::Ordering::Less => format!( + "{result} = trunc {} {} to {target_type}", + operand.type_text, operand.name + ), + std::cmp::Ordering::Equal => { + format!("{result} = add {target_type} 0, {}", operand.name) + } + std::cmp::Ordering::Greater if target.is_signed() => format!( + "{result} = sext {} {} to {target_type}", + operand.type_text, operand.name + ), + std::cmp::Ordering::Greater => format!( + "{result} = zext {} {} to {target_type}", + operand.type_text, operand.name + ), + } + } + MirInstructionKind::CallDirect { + function, + arguments, + .. + } => { + let callee = function_name(bubble, *function); + let arguments = arguments + .iter() + .map(|argument| { + let value = value(*argument, values)?; + Ok(format!("{} {}", value.type_text, value.name)) + }) + .collect::, BpfBackendError>>()? + .join(", "); + format!("{result} = call {type_text} @{callee}({arguments})") + } + _ => { + return Err(BpfBackendError::UnsupportedInstruction { + function: FunctionId::from_raw(0), + value: instruction.result(), + reason: BpfUnsupportedReason::Operation, + }); + } + }; + text.push_str(" "); + text.push_str(&line); + text.push('\n'); + values.insert( + instruction.result(), + LoweredValue { + name: result, + type_text, + }, + ); + Ok(()) +} + +fn lower_terminator( + text: &mut String, + terminator: &MirTerminator, + _types: &TypeArena, + values: &BTreeMap, +) -> Result<(), BpfBackendError> { + match terminator { + MirTerminator::Branch { target, arguments } => { + let _ = arguments; + text.push_str(&format!(" br label %b{}\n", target.raw())); + } + MirTerminator::ConditionalBranch { + condition, + when_true, + when_false, + } => { + let condition = value(*condition, values)?; + text.push_str(&format!( + " br i1 {}, label %b{}, label %b{}\n", + condition.name, + when_true.raw(), + when_false.raw() + )); + } + MirTerminator::Return { values: returned } => { + if returned.is_empty() { + text.push_str(" ret void\n"); + } else { + let returned = value(returned[0], values)?; + text.push_str(&format!(" ret {} {}\n", returned.type_text, returned.name)); + } + } + MirTerminator::Trap(_) | MirTerminator::Unreachable => { + text.push_str(&format!(" ret i32 {XDP_PASS}\n")); + } + _ => { + return Err(BpfBackendError::UnsupportedTerminator { + function: FunctionId::from_raw(0), + block: BlockId::from_raw(0), + }); + } + } + Ok(()) +} + +fn binary( + result: &str, + opcode: &'static str, + type_text: &'static str, + left: ValueId, + right: ValueId, + values: &BTreeMap, +) -> Result { + let left = value(left, values)?; + let right = value(right, values)?; + Ok(format!( + "{result} = {opcode} {type_text} {}, {}", + left.name, right.name + )) +} + +fn compare( + result: &str, + predicate: &'static str, + left: ValueId, + right: ValueId, + values: &BTreeMap, +) -> Result { + let left = value(left, values)?; + let right = value(right, values)?; + Ok(format!( + "{result} = icmp {predicate} {} {}, {}", + left.type_text, left.name, right.name + )) +} + +fn value( + value: ValueId, + values: &BTreeMap, +) -> Result<&LoweredValue, BpfBackendError> { + values + .get(&value) + .ok_or(BpfBackendError::MissingValue(value)) +} + +fn bpf_type(type_id: TypeId, types: &TypeArena) -> Result<&'static str, BpfBackendError> { + match types.get(type_id) { + Some(SemanticType::Primitive(PrimitiveType::Boolean)) => Ok("i1"), + Some(SemanticType::Primitive(PrimitiveType::Integer(kind))) => Ok(integer_type(*kind)), + Some(SemanticType::Enum { .. }) => Ok("i32"), + _ => Err(BpfBackendError::UnsupportedType(type_id)), + } +} + +fn llvm_results(results: &[TypeId], types: &TypeArena) -> Result<&'static str, BpfBackendError> { + match results { + [] => Ok("void"), + [type_id] => bpf_type(*type_id, types), + [type_id, ..] => Err(BpfBackendError::UnsupportedType(*type_id)), + } +} + +const fn integer_type(kind: IntegerKind) -> &'static str { + match kind { + IntegerKind::Int8 | IntegerKind::UInt8 => "i8", + IntegerKind::Int16 | IntegerKind::UInt16 => "i16", + IntegerKind::Int32 | IntegerKind::UInt32 => "i32", + IntegerKind::Int64 | IntegerKind::UInt64 => "i64", + } +} + +fn integer_bits(type_text: &str) -> u8 { + match type_text.as_bytes() { + b"i1" => 1, + b"i8" => 8, + b"i16" => 16, + b"i32" => 32, + b"i64" => 64, + _ => 64, + } +} + +fn integer_literal(value: IntegerValue) -> String { + if value.kind().is_signed() { + value.signed().unwrap_or_default().to_string() + } else { + value.unsigned().unwrap_or_default().to_string() + } +} + +fn function_name(bubble: &MirBubble, symbol: SymbolId) -> String { + format!("pop_b{}_s{}", bubble.bubble().raw(), symbol.raw()) +} + +#[must_use] +pub const fn xdp_pass() -> i32 { + XDP_PASS +} diff --git a/crates/compiler/backends/llvm/src/lib.rs b/crates/compiler/backends/llvm/src/lib.rs index 0c5b273..aab4aaa 100644 --- a/crates/compiler/backends/llvm/src/lib.rs +++ b/crates/compiler/backends/llvm/src/lib.rs @@ -5,10 +5,25 @@ //! function lowering, and instruction lowering so backend mechanics cannot //! become canonical HIR/MIR semantics. +// The LLVM backend contains large lowering/emission passes that predate the +// Rust 1.96 clippy gate. Keep the baseline explicit until those passes are +// split deliberately. +#![allow( + clippy::cast_possible_truncation, + clippy::comparison_chain, + clippy::format_push_string, + clippy::match_same_arms, + clippy::too_many_arguments, + clippy::too_many_lines, + clippy::wildcard_imports +)] + mod api; +mod bpf; mod instruction_lowering; mod lowering; mod module_lowering; pub use api::*; +pub use bpf::*; pub use lowering::*; diff --git a/crates/compiler/backends/llvm/tests/bpf.rs b/crates/compiler/backends/llvm/tests/bpf.rs new file mode 100644 index 0000000..8fa0536 --- /dev/null +++ b/crates/compiler/backends/llvm/tests/bpf.rs @@ -0,0 +1,170 @@ +use pop_backend_api::{RuntimeContract, RuntimeContractError}; +use pop_backend_llvm::{ + BpfBackendError, BpfLoweringOptions, BpfUnsupportedReason, BpfValidationPass, + lower_mir_to_bpf_module, xdp_pass, +}; +use pop_driver::{FrontEndBubbleInput, FrontEndModule, analyze_bubble}; +use pop_foundation::{BubbleId, FileId, ModuleId, NamespaceId}; +use pop_mir::{lower_hir_bubble, optimize_mir}; +use pop_source::SourceFile; +use pop_target::TargetSpec; + +fn lower(source_text: &str) -> (pop_mir::MirBubble, pop_types::TypeArena) { + lower_with_optimization(source_text, true) +} + +fn lower_unoptimized(source_text: &str) -> (pop_mir::MirBubble, pop_types::TypeArena) { + lower_with_optimization(source_text, false) +} + +fn lower_with_optimization( + source_text: &str, + optimize: bool, +) -> (pop_mir::MirBubble, pop_types::TypeArena) { + let source = SourceFile::new(FileId::from_raw(0), "src/main.pop", source_text).expect("source"); + let front_end = analyze_bubble( + FrontEndBubbleInput::new( + BubbleId::from_raw(0), + NamespaceId::from_raw(0), + Vec::new(), + vec![FrontEndModule::new(ModuleId::from_raw(0), source)], + ) + .with_implicit_main_entry(ModuleId::from_raw(0)), + ); + assert!( + front_end.diagnostics().is_empty(), + "{}", + front_end.diagnostic_snapshot() + ); + let mir = + lower_hir_bubble(front_end.hir().expect("HIR"), front_end.types()).expect("verified MIR"); + let mir = if optimize { + optimize_mir(mir, front_end.types()).expect("optimized MIR") + } else { + mir + }; + (mir, front_end.types().clone()) +} + +fn bpfel() -> TargetSpec { + TargetSpec::for_triple("bpfel-unknown-none").expect("BPF target") +} + +#[test] +fn validates_and_lowers_minimal_xdp_pass_to_bpf_llvm_ir() { + let (mir, types) = lower( + "namespace Main\n\ + function main(): Int\n\ + return 2\n\ + end\n", + ); + let entry = mir.functions()[0].symbol(); + let options = BpfLoweringOptions::xdp(entry); + BpfValidationPass + .validate(&mir, &types, &bpfel(), options) + .expect("valid BPF MIR"); + let module = lower_mir_to_bpf_module(&mir, &types, &bpfel(), options).expect("BPF lowering"); + let text = module.as_llvm_ir(); + assert_eq!(module.triple(), "bpfel-unknown-none"); + assert!(text.contains("target triple = \"bpfel-unknown-none\"")); + assert!(text.contains("section \"xdp\"")); + assert!(text.contains("@pop_bpf_xdp")); + assert!(text.contains(&format!("add i64 0, {}", xdp_pass()))); + assert!(!text.contains("pop_rt_")); + assert!(!text.contains("@main(")); +} + +#[test] +fn rejects_floating_point_for_bpf() { + let (mir, types) = lower_unoptimized( + "namespace Main\n\ + function main(): Int\n\ + local value: Float64 = 1.0\n\ + return Int(value)\n\ + end\n", + ); + let error = BpfValidationPass + .validate( + &mir, + &types, + &bpfel(), + BpfLoweringOptions::xdp(mir.functions()[0].symbol()), + ) + .expect_err("float is rejected"); + assert_eq!(error.diagnostic_code(), "POP7004"); + assert!(matches!( + error, + BpfBackendError::UnsupportedInstruction { + reason: BpfUnsupportedReason::FloatingPoint, + .. + } + )); +} + +#[test] +fn rejects_stdlib_call_when_selected_runtime_profile_lacks_contract() { + let (mir, types) = lower( + "namespace Main\n\ + function main(): Int\n\ + print(\"hello\")\n\ + return 2\n\ + end\n", + ); + let error = BpfValidationPass + .validate( + &mir, + &types, + &bpfel(), + BpfLoweringOptions::xdp(mir.functions()[0].symbol()), + ) + .expect_err("runtime call is rejected"); + assert_eq!(error.diagnostic_code(), "POP7006"); + assert!(matches!( + error, + BpfBackendError::RuntimeContract(RuntimeContractError::MissingContract { + requirement, + .. + }) if requirement.contract() == RuntimeContract::StandardLibraryAdapters + )); +} + +#[test] +fn rejects_invalid_xdp_entry_signature() { + let (mir, types) = lower( + "namespace Main\n\ + private function entry(value: Int): Int\n\ + return value\n\ + end\n\ + function main(): Int\n\ + return 2\n\ + end\n", + ); + let error = BpfValidationPass + .validate( + &mir, + &types, + &bpfel(), + BpfLoweringOptions::xdp(mir.functions()[0].symbol()), + ) + .expect_err("entry parameters are rejected"); + assert_eq!(error.diagnostic_code(), "POP7000"); +} + +#[test] +fn rejects_direct_recursion() { + let (mir, types) = lower( + "namespace Main\n\ + function main(): Int\n\ + return main()\n\ + end\n", + ); + let error = BpfValidationPass + .validate( + &mir, + &types, + &bpfel(), + BpfLoweringOptions::xdp(mir.functions()[0].symbol()), + ) + .expect_err("recursion is rejected"); + assert_eq!(error.diagnostic_code(), "POP7005"); +} diff --git a/crates/compiler/backends/llvm/tests/lowering.rs b/crates/compiler/backends/llvm/tests/lowering.rs index 66d1f4f..1619833 100644 --- a/crates/compiler/backends/llvm/tests/lowering.rs +++ b/crates/compiler/backends/llvm/tests/lowering.rs @@ -199,7 +199,7 @@ fn optional_scalar_collection_reads_execute_without_a_zero_sentinel() { ) .expect("LLVM optional collection lowering"); let result = link_with_runtime_and_run(&module, "optional-scalar"); - assert_eq!(result.status.code(), Some(14), "{}", module); + assert_eq!(result.status.code(), Some(14), "{module}"); } #[test] @@ -265,7 +265,7 @@ fn specialized_generic_data_and_calls_execute_natively() { .expect("LLVM lowering"); let result = link_with_runtime_and_run(&module, "generic-execution"); - assert_eq!(result.status.code(), Some(7), "{}", module); + assert_eq!(result.status.code(), Some(7), "{module}"); } #[test] @@ -433,7 +433,7 @@ fn fixed_pack_calls_and_multiple_assignment_execute_natively() { assert!(text.contains("@pop_rt_field_get"), "{text}"); let result = link_with_runtime_and_run(&module, "fixed-pack"); - assert_eq!(result.status.code(), Some(21), "{}", module); + assert_eq!(result.status.code(), Some(21), "{module}"); } #[test] diff --git a/crates/compiler/backends/mir-interp/src/lib.rs b/crates/compiler/backends/mir-interp/src/lib.rs index dae8c65..953667b 100644 --- a/crates/compiler/backends/mir-interp/src/lib.rs +++ b/crates/compiler/backends/mir-interp/src/lib.rs @@ -9,7 +9,12 @@ //! New MIR operations should remain backend-neutral. Put execution sequencing in //! `interpreter`, value semantics in `evaluation`, and runtime capabilities behind //! `RuntimeAdapter`; never reconstruct source semantics or perform string lookup. -#![allow(clippy::too_many_lines)] +#![allow( + clippy::match_same_arms, + clippy::redundant_closure_for_method_calls, + clippy::too_many_lines, + clippy::wildcard_imports +)] mod evaluation; mod interpreter; diff --git a/crates/compiler/backends/mir-interp/tests/language_differential.rs b/crates/compiler/backends/mir-interp/tests/language_differential.rs index 53d9287..ba0b032 100644 --- a/crates/compiler/backends/mir-interp/tests/language_differential.rs +++ b/crates/compiler/backends/mir-interp/tests/language_differential.rs @@ -1,3 +1,5 @@ +#![allow(clippy::redundant_closure_for_method_calls)] + use pop_backend_mir_interp::{MirInterpreter, MirValue, ReferenceRuntimeEvent}; use pop_driver::{FrontEndBubbleInput, FrontEndModule, analyze_bubble}; use pop_foundation::{BubbleId, FileId, ModuleId, NamespaceId, SymbolId}; diff --git a/crates/compiler/compile-time/src/lib.rs b/crates/compiler/compile-time/src/lib.rs index b190570..a9a9603 100644 --- a/crates/compiler/compile-time/src/lib.rs +++ b/crates/compiler/compile-time/src/lib.rs @@ -11,6 +11,14 @@ //! None of these modules may access ambient I/O, parse source, or invoke a //! backend. Keeping that isolation visible is part of the language contract. +// These modules predate the repository-wide Rust 1.96 clippy gate. Keep the +// baseline explicit until the evaluator is split into smaller passes. +#![allow( + clippy::match_same_arms, + clippy::too_many_lines, + clippy::wildcard_imports +)] + mod evaluation; mod interpreter; mod lowering; diff --git a/crates/compiler/diagnostics/catalog.tsv b/crates/compiler/diagnostics/catalog.tsv index 8ce369d..2540b71 100644 --- a/crates/compiler/diagnostics/catalog.tsv +++ b/crates/compiler/diagnostics/catalog.tsv @@ -55,3 +55,13 @@ POP6405 Warning Style 1 documentation.duplicateSummary - Identifier true diagnos POP6406 Warning Style 1 documentation.invalidInheritance - Identifier true diagnostics/POP6406 documentation - 1 - POP6407 Warning Style 1 documentation.inheritanceCycle - Identifier true diagnostics/POP6407 documentation - 1 - POP6408 Warning Style 1 documentation.invalidReturns - Identifier true diagnostics/POP6408 documentation - 1 - +POP7000 Error Backend - backend.bpf.invalidEntryPoint - Identifier false diagnostics/POP7000 backend - 1 - +POP7001 Error Backend - backend.bpf.unsupportedMirOperation - Identifier false diagnostics/POP7001 backend - 1 - +POP7002 Error Backend - backend.bpf.unsupportedTypeOrLayout - Identifier false diagnostics/POP7002 backend - 1 - +POP7003 Error Backend - backend.bpf.managedAllocationUnavailable - Identifier false diagnostics/POP7003 backend - 1 - +POP7004 Error Backend - backend.bpf.floatingPointUnavailable - Identifier false diagnostics/POP7004 backend - 1 - +POP7005 Error Backend - backend.bpf.callUnavailable - Identifier false diagnostics/POP7005 backend - 1 - +POP7006 Error Backend - backend.runtimeContractUnavailable - Identifier false diagnostics/POP7006 backend - 1 - +POP7007 Error Backend - backend.bpf.llvmTargetUnavailable - Identifier false diagnostics/POP7007 backend - 1 - +POP7008 Error Backend - backend.unknownTarget - Identifier false diagnostics/POP7008 backend - 1 - +POP7009 Error Backend - backend.bpf.loopBoundUnavailable - Identifier false diagnostics/POP7009 backend - 1 - diff --git a/crates/compiler/diagnostics/src/lib.rs b/crates/compiler/diagnostics/src/lib.rs index f4a06d8..2963c49 100644 --- a/crates/compiler/diagnostics/src/lib.rs +++ b/crates/compiler/diagnostics/src/lib.rs @@ -154,6 +154,9 @@ fn parse_entry(line_number: usize, line: &'static str) -> Result DiagnosticCategory::Resolution, Some("Type") => DiagnosticCategory::Type, Some("CompileTime") => DiagnosticCategory::CompileTime, + Some("Backend") => DiagnosticCategory::Backend, + Some("Project") => DiagnosticCategory::Project, + Some("Tooling") => DiagnosticCategory::Tooling, Some("Style") => DiagnosticCategory::Style, _ => { return Err(CatalogError { diff --git a/crates/compiler/diagnostics/tests/catalog.rs b/crates/compiler/diagnostics/tests/catalog.rs index a202e35..096a9d4 100644 --- a/crates/compiler/diagnostics/tests/catalog.rs +++ b/crates/compiler/diagnostics/tests/catalog.rs @@ -25,7 +25,9 @@ fn catalog_is_sorted_unique_and_partitioned_by_compiler_phase() { "POP2014", "POP2015", "POP2016", "POP2017", "POP2018", "POP2019", "POP2020", "POP2021", "POP2022", "POP2023", "POP2024", "POP2025", "POP2026", "POP2027", "POP2028", "POP2029", "POP4001", "POP4002", "POP4003", "POP4004", "POP4005", "POP4006", "POP4007", "POP6400", - "POP6401", "POP6402", "POP6403", "POP6404", "POP6405", "POP6406", "POP6407", "POP6408" + "POP6401", "POP6402", "POP6403", "POP6404", "POP6405", "POP6406", "POP6407", "POP6408", + "POP7000", "POP7001", "POP7002", "POP7003", "POP7004", "POP7005", "POP7006", "POP7007", + "POP7008", "POP7009" ] ); assert!(codes.windows(2).all(|pair| pair[0] < pair[1])); @@ -55,12 +57,18 @@ fn catalog_is_sorted_unique_and_partitioned_by_compiler_phase() { .all(|entry| entry.warning_wave().is_none()) ); assert!(entries[..47].iter().all(|entry| !entry.is_suppressible())); - assert!(entries[47..].iter().all(|entry| { + assert!(entries[47..56].iter().all(|entry| { entry.category() == DiagnosticCategory::Style && entry.severity() == DiagnosticSeverity::Warning && entry.warning_wave() == Some(1) && entry.is_suppressible() })); + assert!(entries[56..].iter().all(|entry| { + entry.category() == DiagnosticCategory::Backend + && entry.severity() == DiagnosticSeverity::Error + && entry.warning_wave().is_none() + && !entry.is_suppressible() + })); assert_eq!(entries[3].quick_fix_providers(), "replaceExportWithPublic"); } diff --git a/crates/compiler/driver/Cargo.toml b/crates/compiler/driver/Cargo.toml index a96f596..9d700b2 100644 --- a/crates/compiler/driver/Cargo.toml +++ b/crates/compiler/driver/Cargo.toml @@ -15,6 +15,7 @@ harness = false [dependencies] pop-backend-c.workspace = true +pop-backend-api.workspace = true pop-backend-llvm.workspace = true pop-compile-time.workspace = true pop-diagnostics.workspace = true diff --git a/crates/compiler/driver/benches/compilation_workload.rs b/crates/compiler/driver/benches/compilation_workload.rs index a65c57b..fc9a488 100644 --- a/crates/compiler/driver/benches/compilation_workload.rs +++ b/crates/compiler/driver/benches/compilation_workload.rs @@ -1,3 +1,10 @@ +#![allow( + clippy::missing_errors_doc, + clippy::missing_panics_doc, + clippy::must_use_candidate, + clippy::single_match_else +)] + use std::fmt::Write as _; use pop_backend_c::{CLoweringOptions, lower_mir_to_c}; diff --git a/crates/compiler/driver/src/lib.rs b/crates/compiler/driver/src/lib.rs index c3b22e5..46c7b73 100644 --- a/crates/compiler/driver/src/lib.rs +++ b/crates/compiler/driver/src/lib.rs @@ -11,6 +11,21 @@ //! - attribute and compile-time helpers remain isolated phase mechanics; //! - diagnostic helpers provide deterministic structured reporting. +// The driver aggregates long phase-orchestration routines that predate the +// Rust 1.96 clippy gate. Keep the baseline explicit until those modules are +// split deliberately. +#![allow( + clippy::cast_possible_truncation, + clippy::collapsible_if, + clippy::format_collect, + clippy::items_after_test_module, + clippy::match_same_arms, + clippy::redundant_closure_for_method_calls, + clippy::similar_names, + clippy::too_many_lines, + clippy::wildcard_imports +)] + mod api; mod artifact; mod attributes; diff --git a/crates/compiler/driver/src/main.rs b/crates/compiler/driver/src/main.rs index 677daca..5d2724d 100644 --- a/crates/compiler/driver/src/main.rs +++ b/crates/compiler/driver/src/main.rs @@ -1,5 +1,12 @@ //! Unified `pop` command and build orchestration. +#![allow( + clippy::map_unwrap_or, + clippy::option_option, + clippy::redundant_closure_for_method_calls, + clippy::too_many_lines +)] + use std::collections::{BTreeMap, BTreeSet}; use std::ffi::{OsStr, OsString}; use std::fs; @@ -7,8 +14,12 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, ExitCode}; +use pop_backend_api::RuntimeProfile; use pop_backend_c::{CLoweringOptions, lower_mir_to_c}; -use pop_backend_llvm::{LlvmLoweringOptions, lower_mir_to_llvm_ir}; +use pop_backend_llvm::{ + BpfLoweringOptions, BpfProgramKind, LlvmLoweringOptions, lower_mir_to_bpf_module, + lower_mir_to_llvm_ir, +}; use pop_documentation_generator::{DocumentationMember, render_xml}; use pop_driver::{ CheckedDocumentation, FrontEndBubbleInput, FrontEndModule, PoplibDependency, PoplibEmission, @@ -25,7 +36,7 @@ use pop_projects::{ }; use pop_resolve::Visibility; use pop_source::SourceFile; -use pop_target::{Endianness, PointerWidth, TargetSpec}; +use pop_target::TargetSpec; use pop_types::SemanticType; const USAGE: &str = "\ @@ -33,6 +44,7 @@ Usage: pop check [--dump ]... pop check --manifestPath pop build --output + pop build --target bpfel-unknown-none --runtime-profile linux-ebpf --bpf-program xdp --emit-object pop build --manifestPath pop documentation --manifestPath pop transpile --to c @@ -66,6 +78,13 @@ enum CommandLine { source_path: PathBuf, output_path: PathBuf, }, + BuildBpf { + source_path: PathBuf, + target: String, + runtime_profile: RuntimeProfile, + program: BpfProgramKind, + output_path: PathBuf, + }, PackageBuild { manifest_path: PathBuf, lock_mode: LockMode, @@ -100,6 +119,19 @@ fn main() -> ExitCode { source_path, output_path, }) => build_source(&source_path, &output_path), + Ok(CommandLine::BuildBpf { + source_path, + target, + runtime_profile, + program, + output_path, + }) => build_bpf_source( + &source_path, + &target, + runtime_profile, + program, + &output_path, + ), Ok(CommandLine::PackageBuild { manifest_path, lock_mode, @@ -265,8 +297,74 @@ fn parse_build_arguments( } let source_path = required_source_path(first, "build")?; let Some(option) = arguments.next() else { - return Err("`pop build` requires `--output `".to_owned()); + return Err( + "`pop build` requires `--output ` or `--target `".to_owned(), + ); }; + if option == "--target" { + let target = arguments + .next() + .ok_or_else(|| "`--target` requires a target triple".to_owned())? + .to_string_lossy() + .into_owned(); + let Some(runtime_option) = arguments.next() else { + return Err("BPF builds require `--runtime-profile linux-ebpf`".to_owned()); + }; + if runtime_option != "--runtime-profile" { + return Err(format!( + "unsupported option `{}`; expected --runtime-profile", + runtime_option.to_string_lossy() + )); + } + let runtime_profile = arguments + .next() + .ok_or_else(|| "`--runtime-profile` requires a profile name".to_owned()) + .and_then(|profile| { + RuntimeProfile::parse(&profile.to_string_lossy()).map_err(|error| error.to_string()) + })?; + let Some(program_option) = arguments.next() else { + return Err("BPF builds require `--bpf-program xdp`".to_owned()); + }; + if program_option != "--bpf-program" { + return Err(format!( + "unsupported option `{}`; expected --bpf-program", + program_option.to_string_lossy() + )); + } + let program = match arguments.next().as_deref() { + Some(value) if value == OsStr::new("xdp") => BpfProgramKind::Xdp, + Some(value) => { + return Err(format!( + "unsupported BPF program `{}`; expected xdp", + value.to_string_lossy() + )); + } + None => return Err("`--bpf-program` requires xdp".to_owned()), + }; + let Some(output_option) = arguments.next() else { + return Err("BPF builds require `--emit-object `".to_owned()); + }; + if output_option != "--emit-object" { + return Err(format!( + "unsupported option `{}`; expected --emit-object", + output_option.to_string_lossy() + )); + } + let output_path = arguments + .next() + .map(PathBuf::from) + .ok_or_else(|| "`--emit-object` requires an object path".to_owned())?; + if arguments.next().is_some() { + return Err("`pop build` received unexpected arguments".to_owned()); + } + return Ok(CommandLine::BuildBpf { + source_path, + target, + runtime_profile, + program, + output_path, + }); + } if option != "--output" { return Err(format!("unsupported option `{}`", option.to_string_lossy())); } @@ -477,10 +575,7 @@ fn check_source(source_path: &PathBuf, dumps: &[DumpKind]) -> ExitCode { } fn native_target() -> TargetSpec { - TargetSpec::builder("x86_64-unknown-linux-gnu") - .pointer_width(PointerWidth::Bits64) - .endianness(Endianness::Little) - .build() + TargetSpec::for_triple("x86_64-unknown-linux-gnu") .expect("repository native target is complete") } @@ -530,6 +625,45 @@ fn build_source(source_path: &Path, output_path: &Path) -> ExitCode { result } +fn build_bpf_source( + source_path: &Path, + target_triple: &str, + runtime_profile: RuntimeProfile, + program: BpfProgramKind, + output_path: &Path, +) -> ExitCode { + let target = match TargetSpec::for_triple(target_triple) { + Ok(target) => target, + Err(error) => { + eprintln!("pop: {error}: `{target_triple}`"); + return ExitCode::FAILURE; + } + }; + let Some(program_mir) = lower_native_source(source_path) else { + return ExitCode::FAILURE; + }; + let Some(entry) = program_mir.entry else { + eprintln!("pop: BPF build requires an explicit entry point"); + return ExitCode::FAILURE; + }; + let options = match program { + BpfProgramKind::Xdp => BpfLoweringOptions::xdp(entry).with_runtime_profile(runtime_profile), + }; + let module = + match lower_mir_to_bpf_module(&program_mir.mir, &program_mir.types, &target, options) { + Ok(module) => module, + Err(error) => { + eprintln!("pop: {}: {error}", error.diagnostic_code()); + return ExitCode::FAILURE; + } + }; + if let Err(error) = module.emit_object(output_path) { + eprintln!("pop: {}: {error}", error.diagnostic_code()); + return ExitCode::FAILURE; + } + ExitCode::SUCCESS +} + fn transpile_source_to_c(source_path: &Path) -> ExitCode { let Some(program) = lower_native_source(source_path) else { return ExitCode::FAILURE; diff --git a/crates/compiler/driver/tests/cli_dump.rs b/crates/compiler/driver/tests/cli_dump.rs index c9c416b..0ab59da 100644 --- a/crates/compiler/driver/tests/cli_dump.rs +++ b/crates/compiler/driver/tests/cli_dump.rs @@ -20,6 +20,16 @@ fn example(name: &str) -> PathBuf { .join(name) } +fn bpf_example(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(3) + .expect("driver crate is under repository root") + .join("examples") + .join("bpf") + .join(name) +} + fn run_pop(arguments: &[&str], source: Option<&str>) -> Output { let mut command = Command::new(env!("CARGO_BIN_EXE_pop")); command.args(arguments); @@ -262,6 +272,54 @@ fn transpile_supports_the_runtime_free_native_math_example() { assert!(output_text(&output.stdout).contains("int main(void)")); } +#[test] +fn bpf_build_requires_a_known_explicit_target() { + let object = std::env::temp_dir().join(format!("pop-bpf-unknown-{}.o", std::process::id())); + let output = Command::new(env!("CARGO_BIN_EXE_pop")) + .args(["build"]) + .arg(bpf_example("xdpPass.pop")) + .args([ + "--target", + "bpf-unknown-linux", + "--runtime-profile", + "linux-ebpf", + "--bpf-program", + "xdp", + "--emit-object", + ]) + .arg(&object) + .output() + .expect("pop build bpf usage runs"); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + assert!(!object.exists(), "failed BPF build must not emit an object"); + assert!(output_text(&output.stderr).contains("unknown Pop Lang target triple")); +} + +#[test] +fn bpf_build_rejects_unknown_runtime_profile_before_artifact_emission() { + let object = std::env::temp_dir().join(format!("pop-bpf-profile-{}.o", std::process::id())); + let output = Command::new(env!("CARGO_BIN_EXE_pop")) + .args(["build"]) + .arg(bpf_example("xdpPass.pop")) + .args([ + "--target", + "bpfel-unknown-none", + "--runtime-profile", + "kernel-default", + "--bpf-program", + "xdp", + "--emit-object", + ]) + .arg(&object) + .output() + .expect("pop build bpf usage runs"); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + assert!(!object.exists(), "failed BPF build must not emit an object"); + assert!(output_text(&output.stderr).contains("unknown runtime profile")); +} + #[test] fn transpile_rejects_the_looping_print_example_without_a_runtime_fallback() { let output = Command::new(env!("CARGO_BIN_EXE_pop")) diff --git a/crates/compiler/driver/tests/front_end_pipeline.rs b/crates/compiler/driver/tests/front_end_pipeline.rs index 25d7bc0..121996f 100644 --- a/crates/compiler/driver/tests/front_end_pipeline.rs +++ b/crates/compiler/driver/tests/front_end_pipeline.rs @@ -1,3 +1,5 @@ +#![allow(clippy::redundant_closure_for_method_calls)] + use pop_driver::{FrontEndBubbleInput, FrontEndModule, analyze_bubble}; use pop_foundation::{BubbleId, FileId, ModuleId, NamespaceId, NominalInterfaceId}; use pop_hir::{HirCallDispatch, HirDeclarationKind, HirExpressionKind, HirStatementKind}; diff --git a/crates/compiler/driver/tests/reference_metadata.rs b/crates/compiler/driver/tests/reference_metadata.rs index 9dd4a80..c41ee00 100644 --- a/crates/compiler/driver/tests/reference_metadata.rs +++ b/crates/compiler/driver/tests/reference_metadata.rs @@ -1,3 +1,5 @@ +#![allow(clippy::too_many_lines)] + use pop_driver::{ FrontEndBubbleInput, FrontEndModule, ReferenceMetadataDecodeError, ReferenceMetadataError, analyze_bubble, decode_reference_metadata, encode_reference_metadata, diff --git a/crates/compiler/hir/src/lib.rs b/crates/compiler/hir/src/lib.rs index 1189fd2..792b6b3 100644 --- a/crates/compiler/hir/src/lib.rs +++ b/crates/compiler/hir/src/lib.rs @@ -10,6 +10,21 @@ //! Keeping these concerns separate prevents source lowering, validation, and //! presentation mechanics from growing back into one contributor-hostile file. +// HIR owns large data-model, lowering, verification, and dump routines that +// predate the Rust 1.96 clippy gate. Keep the baseline explicit until those +// modules are split deliberately. +#![allow( + clippy::assigning_clones, + clippy::double_must_use, + clippy::match_same_arms, + clippy::semicolon_if_nothing_returned, + clippy::too_many_arguments, + clippy::too_many_lines, + clippy::unnecessary_wraps, + clippy::wildcard_imports, + clippy::write_with_newline +)] + mod ir; mod lowering; mod text; diff --git a/crates/compiler/mir/src/lib.rs b/crates/compiler/mir/src/lib.rs index a1a9238..40c3329 100644 --- a/crates/compiler/mir/src/lib.rs +++ b/crates/compiler/mir/src/lib.rs @@ -11,6 +11,18 @@ //! Backend-specific representations and target instructions do not belong in //! any of these modules. +// MIR owns large lowering, optimization, rendering, and verification passes +// that predate the Rust 1.96 clippy gate. Keep the baseline explicit until +// those passes are split deliberately. +#![allow( + clippy::match_same_arms, + clippy::needless_pass_by_value, + clippy::redundant_closure_for_method_calls, + clippy::too_many_arguments, + clippy::too_many_lines, + clippy::wildcard_imports +)] + mod ir; mod lowering; mod optimize; diff --git a/crates/compiler/mir/tests/lowering.rs b/crates/compiler/mir/tests/lowering.rs index 9898707..b4105f2 100644 --- a/crates/compiler/mir/tests/lowering.rs +++ b/crates/compiler/mir/tests/lowering.rs @@ -1,3 +1,5 @@ +#![allow(clippy::redundant_closure_for_method_calls, clippy::too_many_lines)] + use pop_driver::{FrontEndBubbleInput, FrontEndModule, analyze_bubble}; use pop_foundation::{BubbleId, FileId, ModuleId, NamespaceId}; use pop_mir::{ diff --git a/crates/compiler/target/src/lib.rs b/crates/compiler/target/src/lib.rs index 1bead5e..7db321b 100644 --- a/crates/compiler/target/src/lib.rs +++ b/crates/compiler/target/src/lib.rs @@ -28,6 +28,18 @@ pub enum TargetCapability { RelocatingNursery, SharedLibraries, DebugInformation, + LlvmBpf, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ObjectFormat { + Elf, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OperatingSystem { + None, + Linux, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -35,6 +47,8 @@ pub struct TargetSpec { triple: String, pointer_width: PointerWidth, endianness: Endianness, + object_format: ObjectFormat, + operating_system: OperatingSystem, capabilities: BTreeSet, } @@ -45,10 +59,46 @@ impl TargetSpec { triple: triple.into(), pointer_width: None, endianness: None, + object_format: None, + operating_system: None, capabilities: BTreeSet::new(), } } + /// Returns the built-in target description for a supported triple. + /// + /// # Errors + /// + /// Returns [`TargetSpecError::UnknownTriple`] when the triple is not part + /// of Pop Lang's target inventory. + pub fn for_triple(triple: &str) -> Result { + match triple { + "x86_64-unknown-linux-gnu" => Self::builder(triple) + .pointer_width(PointerWidth::Bits64) + .endianness(Endianness::Little) + .object_format(ObjectFormat::Elf) + .operating_system(OperatingSystem::Linux) + .capability(TargetCapability::Threads) + .capability(TargetCapability::PreciseStackMaps) + .build(), + "bpfel-unknown-none" => Self::builder(triple) + .pointer_width(PointerWidth::Bits64) + .endianness(Endianness::Little) + .object_format(ObjectFormat::Elf) + .operating_system(OperatingSystem::None) + .capability(TargetCapability::LlvmBpf) + .build(), + "bpfeb-unknown-none" => Self::builder(triple) + .pointer_width(PointerWidth::Bits64) + .endianness(Endianness::Big) + .object_format(ObjectFormat::Elf) + .operating_system(OperatingSystem::None) + .capability(TargetCapability::LlvmBpf) + .build(), + _ => Err(TargetSpecError::UnknownTriple), + } + } + #[must_use] pub fn triple(&self) -> &str { &self.triple @@ -64,6 +114,16 @@ impl TargetSpec { self.endianness } + #[must_use] + pub const fn object_format(&self) -> ObjectFormat { + self.object_format + } + + #[must_use] + pub const fn operating_system(&self) -> OperatingSystem { + self.operating_system + } + #[must_use] pub fn supports(&self, capability: TargetCapability) -> bool { self.capabilities.contains(&capability) @@ -75,6 +135,8 @@ pub struct TargetSpecBuilder { triple: String, pointer_width: Option, endianness: Option, + object_format: Option, + operating_system: Option, capabilities: BTreeSet, } @@ -91,6 +153,18 @@ impl TargetSpecBuilder { self } + #[must_use] + pub fn object_format(mut self, object_format: ObjectFormat) -> Self { + self.object_format = Some(object_format); + self + } + + #[must_use] + pub fn operating_system(mut self, operating_system: OperatingSystem) -> Self { + self.operating_system = Some(operating_system); + self + } + #[must_use] pub fn capability(mut self, capability: TargetCapability) -> Self { self.capabilities.insert(capability); @@ -113,6 +187,8 @@ impl TargetSpecBuilder { .pointer_width .ok_or(TargetSpecError::MissingPointerWidth)?, endianness: self.endianness.ok_or(TargetSpecError::MissingEndianness)?, + object_format: self.object_format.unwrap_or(ObjectFormat::Elf), + operating_system: self.operating_system.unwrap_or(OperatingSystem::None), capabilities: self.capabilities, }) } @@ -123,6 +199,7 @@ pub enum TargetSpecError { EmptyTriple, MissingPointerWidth, MissingEndianness, + UnknownTriple, } impl fmt::Display for TargetSpecError { @@ -131,6 +208,7 @@ impl fmt::Display for TargetSpecError { Self::EmptyTriple => formatter.write_str("target triple cannot be empty"), Self::MissingPointerWidth => formatter.write_str("target pointer width is required"), Self::MissingEndianness => formatter.write_str("target endianness is required"), + Self::UnknownTriple => formatter.write_str("unknown Pop Lang target triple"), } } } diff --git a/crates/compiler/target/tests/target_spec.rs b/crates/compiler/target/tests/target_spec.rs index 1ee567e..d970d72 100644 --- a/crates/compiler/target/tests/target_spec.rs +++ b/crates/compiler/target/tests/target_spec.rs @@ -1,4 +1,6 @@ -use pop_target::{Endianness, PointerWidth, TargetCapability, TargetSpec}; +use pop_target::{ + Endianness, ObjectFormat, OperatingSystem, PointerWidth, TargetCapability, TargetSpec, +}; #[test] fn target_spec_exposes_backend_neutral_facts() { @@ -17,3 +19,19 @@ fn target_spec_exposes_backend_neutral_facts() { assert!(!target.supports(TargetCapability::Simd)); assert!(!format!("{target:?}").to_ascii_lowercase().contains("llvm")); } + +#[test] +fn bpf_target_specs_are_elf_llvm_bpf_targets() { + let little = TargetSpec::for_triple("bpfel-unknown-none").expect("bpfel target"); + assert_eq!(little.pointer_width(), PointerWidth::Bits64); + assert_eq!(little.endianness(), Endianness::Little); + assert_eq!(little.object_format(), ObjectFormat::Elf); + assert_eq!(little.operating_system(), OperatingSystem::None); + assert!(little.supports(TargetCapability::LlvmBpf)); + assert!(!little.supports(TargetCapability::Threads)); + assert!(!little.supports(TargetCapability::SharedLibraries)); + + let big = TargetSpec::for_triple("bpfeb-unknown-none").expect("bpfeb target"); + assert_eq!(big.endianness(), Endianness::Big); + assert!(TargetSpec::for_triple("bpf-unknown-linux").is_err()); +} diff --git a/crates/compiler/types/src/body_checking.rs b/crates/compiler/types/src/body_checking.rs index a2d4ddf..928db83 100644 --- a/crates/compiler/types/src/body_checking.rs +++ b/crates/compiler/types/src/body_checking.rs @@ -755,7 +755,7 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { .substitute_type_parameters(result.type_id()?, &substitutions) }) .collect::>>()?; - let mut typed_arguments = Vec::with_capacity(arguments.len()); + let mut checked_arguments = Vec::with_capacity(arguments.len()); for (argument, parameter_type) in arguments.iter().zip(parameter_types) { let typed = self.check_expression_expected( argument, @@ -767,7 +767,7 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { typed.span(), argument.span(), ); - typed_arguments.push(typed); + checked_arguments.push(typed); } let dispatch = self .resolver @@ -782,7 +782,7 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { call: TypedCall { dispatch, type_arguments: resolved_arguments, - arguments: typed_arguments, + arguments: checked_arguments, span, }, results: result_types, diff --git a/crates/compiler/types/src/call_checking.rs b/crates/compiler/types/src/call_checking.rs index af63098..5fbd543 100644 --- a/crates/compiler/types/src/call_checking.rs +++ b/crates/compiler/types/src/call_checking.rs @@ -282,7 +282,7 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { return None; } - let mut typed_arguments = Vec::with_capacity(arguments.len()); + let mut checked_values = Vec::with_capacity(arguments.len()); for (argument, parameter) in arguments.iter().zip(signature.parameters()) { let typed = self.check_expression(argument)?; if !self.infer_type_pattern( @@ -303,7 +303,7 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { )); return None; } - typed_arguments.push(typed); + checked_values.push(typed); } for parameter in signature.type_parameters() { @@ -322,7 +322,7 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { } } - let mut type_arguments = Vec::with_capacity(signature.type_parameters().len()); + let mut resolved_generics = Vec::with_capacity(signature.type_parameters().len()); for parameter in signature.type_parameters() { let Some(argument) = substitutions.get(¶meter.parameter()).copied() else { self.diagnostics @@ -333,12 +333,12 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { )); return None; }; - type_arguments.push(argument); + resolved_generics.push(argument); } let substitution_map: BTreeMap<_, _> = signature .type_parameters() .iter() - .zip(&type_arguments) + .zip(&resolved_generics) .map(|(parameter, argument)| (parameter.parameter(), *argument)) .collect(); self.resolver @@ -359,7 +359,7 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { }) .collect::>>()?; for ((typed, expected), source) in - typed_arguments.iter().zip(¶meter_types).zip(arguments) + checked_values.iter().zip(¶meter_types).zip(arguments) { self.require_same_type(*expected, typed.type_id(), typed.span(), source.span()); } @@ -383,8 +383,8 @@ impl<'resolver, 'index> BodyChecker<'resolver, 'index> { Some(CheckedCall { call: TypedCall { dispatch, - type_arguments, - arguments: typed_arguments, + type_arguments: resolved_generics, + arguments: checked_values, span, }, results, diff --git a/crates/compiler/types/src/lib.rs b/crates/compiler/types/src/lib.rs index b60d040..650ca75 100644 --- a/crates/compiler/types/src/lib.rs +++ b/crates/compiler/types/src/lib.rs @@ -3,6 +3,25 @@ //! This first contract encodes the accepted primitive and nominal type model. //! It deliberately has no operational unknown or dynamic fallback type. +// The type checker predates the repository-wide Rust 1.96 clippy gate. Keep +// the baseline explicit until these large modules are split deliberately. +#![allow( + clippy::cast_possible_truncation, + clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::elidable_lifetime_names, + clippy::format_collect, + clippy::match_same_arms, + clippy::missing_errors_doc, + clippy::missing_panics_doc, + clippy::question_mark, + clippy::redundant_closure_for_method_calls, + clippy::single_match_else, + clippy::too_many_lines, + clippy::unnecessary_wraps, + clippy::wildcard_imports +)] + use pop_foundation::{ AttributeId, BuiltinTypeId, ClassId, InterfaceId, OpaqueId, ParameterId, TypeId, }; diff --git a/crates/compiler/types/tests/errors.rs b/crates/compiler/types/tests/errors.rs index dfb62d9..2108386 100644 --- a/crates/compiler/types/tests/errors.rs +++ b/crates/compiler/types/tests/errors.rs @@ -1,3 +1,5 @@ +#![allow(clippy::too_many_lines)] + use std::collections::BTreeMap; use pop_foundation::{BubbleId, FileId, ModuleId}; diff --git a/crates/compiler/types/tests/numeric_values.rs b/crates/compiler/types/tests/numeric_values.rs index d09d4a5..a3143bb 100644 --- a/crates/compiler/types/tests/numeric_values.rs +++ b/crates/compiler/types/tests/numeric_values.rs @@ -1,3 +1,9 @@ +#![allow( + clippy::cast_possible_truncation, + clippy::cast_precision_loss, + clippy::float_cmp +)] + use std::cmp::Ordering; use pop_types::{FloatKind, FloatValue, IntegerKind, IntegerValue, NumericError}; diff --git a/crates/libraries/standard/tests/api_baseline.rs b/crates/libraries/standard/tests/api_baseline.rs index d70a889..398eaa5 100644 --- a/crates/libraries/standard/tests/api_baseline.rs +++ b/crates/libraries/standard/tests/api_baseline.rs @@ -1,3 +1,5 @@ +use std::fmt::Write as _; + use pop_standard::{ ApiBaselineError, ApiKind, ApiStatus, parse_standard_api_baseline, standard_api_baseline, }; @@ -176,9 +178,10 @@ fn standard_api_baseline_loading_is_bounded() { let mut oversized_inventory = header.to_owned(); for identity in 0..1_025 { - oversized_inventory.push_str(&format!( - "primitive:{identity}\tPrimitive\tPop.Internal\tPop\tBoolean{identity}\tBoolean{identity}\tprelude\timplemented\ttrue\tarchitecture/02-language-model.md\n" - )); + let _ = writeln!( + oversized_inventory, + "primitive:{identity}\tPrimitive\tPop.Internal\tPop\tBoolean{identity}\tBoolean{identity}\tprelude\timplemented\ttrue\tarchitecture/02-language-model.md" + ); } assert_eq!( parse_standard_api_baseline(&oversized_inventory), diff --git a/crates/runtime/native/tests/abi.rs b/crates/runtime/native/tests/abi.rs index cec54aa..5e0a176 100644 --- a/crates/runtime/native/tests/abi.rs +++ b/crates/runtime/native/tests/abi.rs @@ -141,7 +141,10 @@ fn allocation_churn_uses_the_native_stable_generational_path() { ); total = total.checked_add(value).expect("benchmark checksum"); if index.is_multiple_of(8_192) { - assert_eq!(abi_safe_point(index as u32, &[]), 1); + assert_eq!( + abi_safe_point(u32::try_from(index).expect("test range fits u32"), &[]), + 1 + ); } } assert_eq!(total, 200_010_000); diff --git a/crates/tools/architecture-tests/src/tests.rs b/crates/tools/architecture-tests/src/tests.rs index 62ee325..5e20558 100644 --- a/crates/tools/architecture-tests/src/tests.rs +++ b/crates/tools/architecture-tests/src/tests.rs @@ -424,7 +424,7 @@ fn dependencies_are_centralized_and_external_dependencies_are_approved() { let local = line.starts_with("pop-") && line.contains(" = { path = \"") && line.ends_with("\" }"); let approved_inkwell = line - == "inkwell = { version = \"0.9.0\", default-features = false, features = [\"llvm22-1-prefer-dynamic\", \"target-x86\"] }"; + == "inkwell = { version = \"0.9.0\", default-features = false, features = [\"llvm22-1-prefer-dynamic\", \"target-x86\", \"target-bpf\"] }"; let approved_artifact_dependency = matches!( line, "serde = { version = \"1.0.228\", features = [\"derive\"] }" diff --git a/crates/tools/test-runner/tests/foundation_sources.rs b/crates/tools/test-runner/tests/foundation_sources.rs index dd0ae3d..b566dba 100644 --- a/crates/tools/test-runner/tests/foundation_sources.rs +++ b/crates/tools/test-runner/tests/foundation_sources.rs @@ -1,3 +1,5 @@ +#![allow(clippy::too_many_lines)] + use std::fs; use std::path::{Path, PathBuf}; diff --git a/examples/bpf/README.md b/examples/bpf/README.md new file mode 100644 index 0000000..4c77daf --- /dev/null +++ b/examples/bpf/README.md @@ -0,0 +1,34 @@ +# eBPF XDP Example + +This directory contains the initial experimental eBPF example for Pop Lang. + +Build the minimal XDP program: + +```sh +pop build examples/bpf/xdpPass.pop \ + --target bpfel-unknown-none \ + --runtime-profile linux-ebpf \ + --bpf-program xdp \ + --emit-object target/xdp-pass.o +``` + +The program returns numeric `2`, the Linux `XDP_PASS` action. The emitted +object is an ELF eBPF object with an `xdp` section and a `pop_bpf_xdp` entry +wrapper. + +Inspect the object with ordinary ELF tools when the installed LLVM supports the +BPF target: + +```sh +file target/xdp-pass.o +readelf -h target/xdp-pass.o +readelf -S target/xdp-pass.o +llvm-objdump -h target/xdp-pass.o +llvm-objdump -d target/xdp-pass.o +``` + +The selected `linux-ebpf` runtime profile satisfies only the contracts needed +by this scalar example. It does not attach the program to an interface, access +packet bytes, define maps, emit BTF, support CO-RE, or use helpers/ring +buffers. Loading and attaching eBPF programs may require a compatible Linux +kernel and privileges. diff --git a/examples/bpf/xdpPass.pop b/examples/bpf/xdpPass.pop new file mode 100644 index 0000000..7184d75 --- /dev/null +++ b/examples/bpf/xdpPass.pop @@ -0,0 +1,5 @@ +namespace Bpf.Examples + +function main(): Int + return 2 +end