Skip to content

Commit 54da073

Browse files
kumarakclaude
andcommitted
transform: add inline rewrite-mode forms (expr: and stmt:)
Adds two inline forms to `mode: rewrite` for patch specifications, alongside the existing patch-function form. Both are compiled through clang/clangCIR and spliced at the matched op's site without going through a library patch module. `expr:` — single C expression, value substitution - Wrapper return type matches the matched op's first result. - Captures bind by-reference (`T *__cap_NAME`, `$NAME` → `(*__cap_NAME)`); writes to source-storage captures propagate to caller storage. - Strict parse-time rule: leading control-flow keyword, leading `{`, or any top-level `;` is rejected with a "use stmt:" hint. `stmt:` — C statement body, spliced as CFG - Wrapper is always void-returning. Body is preprocessed so each `return X;` / `return;` becomes a `__patchestry_return(...)` marker call (typed by the enclosing function's return type and marked `noreturn`). - Inliner clones the wrapper's entry block before the matched op, skipping top-level `cir.return` / `cir.br` so fall-through becomes natural sequential flow into the matched op's tail. - MVP scope: matched op must be 0-result, wrapper must be a single function-level block, and surviving `__patchestry_*` calls (i.e. user wrote `return X;`) are loud-rejected — the marker → CFG rewrite is a follow-up phase. `patch:` — unchanged, but `mode: replace` keyword retired in favor of unified `mode: rewrite`. The parser rejects `mode: replace` with a migration error pointing at `mode: rewrite` + `patch:` + `arguments:`. `mode: rewrite` requires exactly one of `expr:` / `stmt:` / `patch:`. Capture write semantics (shared between expr: and stmt:) - Source-storage captures (`cir.load %alloca` / `cir.load %global`): wrapper receives `&%alloca`. Writes propagate. - Non-storage captures (binop result, constant, unalloca'd block arg): wrapper receives the address of a freshly materialised `arg_ref` temp. Reads work. Writes are loud-rejected — the validator walks transitive uses through `cir.get_member`, `cir.ptr_stride`, and `cir.cast` chains rooted at the wrapper block-arg, so by-value-struct field writes and array-element writes are caught alongside direct `cir.store v, %arg`. - The chain stops at `cir.load` so writes through a *loaded* pointer (which target real caller storage) stay permitted. Inliner safety - `rewriteWithExpression`'s `fail()` lambda rolls back both the cloned ops and the `arg_ref` temps materialised by `pass.create_reference`, restoring the "no IR drift on error" contract. - Both inliners route the matched-op erase through `pass.erase_op`, scrubbing `inline_worklists` so chained patches (`apply_after` followed by a `mode: rewrite` matching the emitted patch call by name) can't leave a dangling pointer for the post-pass `inline_call` loop. Implementation surface - YAML schema (`include/patchestry/YAML/PatchSpec.hpp`): add `stmt` field to `Action` / `PatchActionObject` / `PatchEntry`; three-way mutual-exclusion validation in both surfaces. - Fragment compiler (`lib/patchestry/Passes/FragmentCompiler.cpp`, `.hpp`): `compile_fragment` + `compile_stmt_fragment` with a process-scoped content-keyed cache. Marker preprocessor (`preprocess_return_markers`, string-aware over strings/char/ comments). Strict-expr validator (`looks_like_statement`). - Inliners (`lib/patchestry/Passes/PatchOperationImpl.{cpp,hpp}`): `rewriteWithExpression` (expr-form) and `rewriteWithStatements` (stmt-form). Shared `find_capture_source_alloca`, `promote_parameter_slots`, `reaches_store_via_addr` storage- write validator. `replaceCallWithPatch` / `replaceOperationWithPatch` for the `patch:` form. - Dispatch (`lib/patchestry/Passes/InstrumentationPass.cpp`): function-mode and operation-mode branches both route REWRITE to the right inliner based on which inline-form field is set. - Documentation (`docs/GettingStarted/patch_specifications.md`): three-form rewrite mode documented with examples for each. - Test fixtures: `rewrite_emit_*.yaml` for expr-form coverage, `rewrite_stmt_*.yaml` for stmt-form (free-and-null, if-guarded free, while-with-locals, compound block with declaration). Reject fixtures verify storage-write rejection and the parser-time three-way exclusion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3ff95f4 commit 54da073

80 files changed

Lines changed: 3101 additions & 193 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/patch-development.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ paths:
2424
| Add LIT transform test | `test/patchir-transform/` — copy an existing YAML test as template |
2525
| Modify CIR→LLVM lowering | `tools/patchir-cir2llvm/main.cpp` — CIR to LLVM IR/bitcode with contract metadata |
2626
| Update spec docs | `docs/GettingStarted/patch_specifications.md` |
27+
| Update fragment wrapper synthesis | `lib/patchestry/Passes/FragmentCompiler.cpp``synthesise_wrapper`, `is_statement_form`, `substitute_metavars`. Captures pass by-reference (`T *__cap_NAME`, `$NAME``(*__cap_NAME)`); clang/clangIR is the only parser. |
28+
| Extend the rewrite inliner — `expr:` form | `lib/patchestry/Passes/PatchOperationImpl.cpp``rewriteWithExpression`, `find_rewrite_root_in_wrapper`, `find_capture_source_alloca`, `promote_parameter_slots`. Each wrapper block-arg maps to either the capture's source alloca (writes propagate) or a fresh `arg_ref` temp from `pass.create_reference` (read-only; the post-clang validator rejects `cir.store ..., %arg` against non-storage captures). |
29+
| Adjust the struct-decl walker (capture types → wrapper-source decls) | `lib/patchestry/Passes/PatchOperationImpl.cpp``collect_struct_types_rec`, `topo_order_structs`, `lookup_field_name`, `emit_struct_decls_for_types`. Field names are recovered from module-level `cir::GetMemberOp` ops; `rewriteWithExpression` seeds collection with capture types + the matched op's result type. |
30+
| Add a C-type spelling for capture-type conversion | `lib/patchestry/Passes/Utils.cpp``ConvertCirTypesToCTypes`. If the new spelling needs a typedef for clang to accept it, also extend the prelude in `synthesise_wrapper` (`lib/patchestry/Passes/FragmentCompiler.cpp`). |
2731

2832
## Adding a New Argument Source
2933

docs/GettingStarted/patch_specifications.md

Lines changed: 234 additions & 98 deletions
Large diffs are not rendered by default.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2025, Trail of Bits, Inc.
3+
*
4+
* This source code is licensed in accordance with the terms specified in
5+
* the LICENSE file found in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <cstdint>
11+
#include <optional>
12+
#include <string>
13+
#include <vector>
14+
15+
#include <llvm/ADT/ArrayRef.h>
16+
#include <llvm/ADT/StringRef.h>
17+
18+
namespace patchestry::passes::fragment_expr {
19+
20+
// `name` is without the leading `$`; `c_type` is the spelling clang
21+
// will see in the wrapper's parameter list (e.g. "uint32_t", "int32_t *").
22+
struct CaptureBinding
23+
{
24+
std::string name;
25+
std::string c_type;
26+
};
27+
28+
struct FragmentResult
29+
{
30+
std::string func_name;
31+
std::optional< std::string > module_text;
32+
std::string error;
33+
};
34+
35+
// Compile a rewrite-mode fragment by wrapping it in a synthesised C
36+
// function and invoking clang/clangCIR. `arch` follows the Ghidra
37+
// `lang` convention ("ARM:LE:32:default"). Identical inputs hit a
38+
// process-scoped content-keyed cache.
39+
FragmentResult compile_fragment(
40+
llvm::StringRef fragment, llvm::ArrayRef< CaptureBinding > captures,
41+
llvm::StringRef arch, llvm::StringRef return_c_type = "int32_t",
42+
llvm::StringRef extra_decls = ""
43+
);
44+
45+
// Compile a `stmt:` body: auto-converts `return X;` / `return;`
46+
// to `__patchestry_return(...)` markers, substitutes `$NAME`,
47+
// and wraps in a void function with a `noreturn`-marked extern
48+
// for the marker (typed by `enclosing_return_c_type`). Cache
49+
// key includes the enclosing return type.
50+
FragmentResult compile_stmt_fragment(
51+
llvm::StringRef body, llvm::ArrayRef< CaptureBinding > captures,
52+
llvm::StringRef arch, llvm::StringRef enclosing_return_c_type = "void",
53+
llvm::StringRef extra_decls = ""
54+
);
55+
56+
} // namespace patchestry::passes::fragment_expr

include/patchestry/Passes/InstrumentationPass.hpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ namespace patchestry::passes { // NOLINT
108108
* entry block, after the alloca + parameter-store prologue. Patch-only — contracts
109109
* are static and attach predicates as MLIR attributes, so "entrypoint" has no
110110
* meaning for them.
111-
* - REPLACE: Replace the matched op with a call to the patch function (patch-only).
111+
* - REWRITE: Substitute the matched op's result. Two forms (selected
112+
* per-spec): an inline C expression fragment (`expr:`) compiled
113+
* through the clang/CIR fragment compiler and inlined at the match
114+
* site, or an external patch function (`patch:` + `arguments:`)
115+
* called as a `cir.call`. The legacy REPLACE mode was merged into
116+
* this one (patch-only).
112117
* - ERASE: Delete the matched op, replacing live results with default values
113118
* (patch-only; no patch function invoked).
114119
*
@@ -163,7 +168,7 @@ namespace patchestry::passes { // NOLINT
163168
* @brief Erase an operation while keeping `inline_worklists` consistent.
164169
*
165170
* The inline worklist stores raw `Operation *` pointers pending
166-
* post-pass inlining. A later patch action with `mode: replace` or
171+
* post-pass inlining. A later patch action with `mode: rewrite` or
167172
* `mode: erase` may target a previously-emitted patch call by name
168173
* (see the intentional no-skip comment in `apply_patch_action_to_targets`).
169174
* If such an action erases an op that's still in `inline_worklists`,
@@ -332,15 +337,19 @@ namespace patchestry::passes { // NOLINT
332337
mlir::ModuleOp dest, mlir::ModuleOp src, const std::string &symbol_name
333338
);
334339

340+
public:
335341
/**
336342
* @brief Creates a cast operation if types differ, otherwise returns the original
337-
* value.
343+
* value. Public so the rewrite-mode inliner (FragmentCompiler) can
344+
* reuse the same cast-kind decision table the rest of the pass uses, rather
345+
* than re-deriving it.
338346
*/
339347
mlir::Value create_cast_if_needed(
340348
mlir::OpBuilder &builder, mlir::Operation *call_op, mlir::Value value,
341349
mlir::Type target_type
342350
);
343351

352+
private:
344353
/**
345354
* @brief Creates a reference (alloca + store) for a given value.
346355
*/

include/patchestry/YAML/BaseSpec.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ namespace patchestry::passes {
2323
APPLY_BEFORE,
2424
APPLY_AFTER,
2525
APPLY_AT_ENTRYPOINT, // Insert patch call at caller's entry block
26-
REPLACE,
27-
ERASE // Delete matched op, no patch function
26+
ERASE, // Delete matched op, no patch function
27+
REWRITE
2828
};
2929

3030
enum class ArgumentSourceType : uint8_t {
@@ -45,8 +45,8 @@ namespace patchestry::passes {
4545
static_assert(static_cast< uint8_t >(InstrumentationMode::APPLY_BEFORE) == 1);
4646
static_assert(static_cast< uint8_t >(InstrumentationMode::APPLY_AFTER) == 2);
4747
static_assert(static_cast< uint8_t >(InstrumentationMode::APPLY_AT_ENTRYPOINT) == 3);
48-
static_assert(static_cast< uint8_t >(InstrumentationMode::REPLACE) == 4);
49-
static_assert(static_cast< uint8_t >(InstrumentationMode::ERASE) == 5);
48+
static_assert(static_cast< uint8_t >(InstrumentationMode::ERASE) == 4);
49+
static_assert(static_cast< uint8_t >(InstrumentationMode::REWRITE) == 5);
5050

5151
static_assert(static_cast< uint8_t >(ArgumentSourceType::OPERAND) == 0);
5252
static_assert(static_cast< uint8_t >(ArgumentSourceType::VARIABLE) == 1);

0 commit comments

Comments
 (0)