Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c30a32c
feat(parser): add ops block grammar rules
jellllly420 Apr 27, 2026
2bf6646
refactor(parser): address code review on ops grammar (rename, layout,…
jellllly420 Apr 27, 2026
53b8906
feat(parser): add OpRef rule for $group::$op call targets
jellllly420 Apr 27, 2026
d4118a9
fix(context, meta): update MirFnOperand match sites for OpRef arm
jellllly420 Apr 27, 2026
f5b9526
feat(context): add OpsBlock, OpGroup, OpSignature AST types
jellllly420 Apr 27, 2026
21ee842
refactor(context): use derive_more::Debug for ops AST consistency
jellllly420 Apr 27, 2026
30c9dfd
feat(context): add Operand::OpRef variant + parser lowering
jellllly420 Apr 27, 2026
6c02f25
fix(context): strip $ in Operand::OpRef lowering for resolver lookup
jellllly420 Apr 27, 2026
b34487d
feat(context): lower ops block into Pattern.ops_block
jellllly420 Apr 27, 2026
7e6e2a2
fix(context): align OpsMetaLookup indices and clarify R1 preconditions
jellllly420 Apr 27, 2026
e5ad9d6
feat(resolve): well-formedness checks R1-R3 for ops blocks
jellllly420 Apr 27, 2026
7a7a9f6
feat(resolve): op-ref resolution R4-R6 + groups_used
jellllly420 Apr 27, 2026
c8fd07f
feat(config): RawOpInstance + RplConfig.ops field
jellllly420 Apr 27, 2026
4868b39
feat(context): substitution + validation for op instances
jellllly420 Apr 27, 2026
a3109a4
feat(driver): thread OpsConfig through the matcher pipeline
jellllly420 Apr 27, 2026
0f5d054
feat(driver): cartesian-product iteration over op instances
jellllly420 Apr 27, 2026
6f2f959
feat(match): resolve OpRef and op-typed params via bindings
jellllly420 Apr 27, 2026
25358cf
test(ops): add lock_unlock end-to-end UI test
jellllly420 Apr 27, 2026
998e2f5
test(ops): folding, partial-bad, set-op composition, two-groups
jellllly420 Apr 27, 2026
00c599c
test(ops): CVE-2025-68260 POC — abstract intrusive-list-under-lock pa…
jellllly420 Apr 27, 2026
82fb981
style(ops): apply cargo fmt across new abstract-ops code
jellllly420 May 11, 2026
af1c727
fix(context): replace UB `&PatternItem` -> `*mut` cast with OnceCell
jellllly420 May 11, 2026
0825e48
refactor(context): use FxHashMap for deterministic ops ordering; drop…
jellllly420 May 11, 2026
82e63b0
test(ops): align black_box paths, uniform -Zinline-mir, drop broken s…
jellllly420 May 11, 2026
eab7bfc
fix(ops): silence clippy lints (collapsible_match, needless_borrow, u…
jellllly420 May 11, 2026
4fe893a
style(ops): re-fmt after if-let collapse (rustfmt was missed before p…
jellllly420 May 11, 2026
687638b
fix(config): treat empty resolved pattern paths as no-RPL_PATS
jellllly420 May 11, 2026
20d9fa4
fix(driver): respect outer pat_op bindings in nested RustItems cartesian
jellllly420 May 11, 2026
a131d92
fix(context): wire R6 (ops meta-vars must not leak into patt bodies)
jellllly420 May 11, 2026
afbf30d
refactor(rpl.toml): adopt design-intended $T::method op-binding syntax
jellllly420 May 12, 2026
62effef
revert(toolchain): drop rust-analyzer component (not needed for CI)
jellllly420 May 27, 2026
2f10724
refactor(tests): share static arena + mctx helpers across ops integra…
jellllly420 May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 27 additions & 3 deletions crates/rpl_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ use std::path::{Path, PathBuf};

use serde::Deserialize;

mod ops;
mod patterns;
mod run;
mod util;

pub use ops::RawOpInstance;

#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to read {path}: {source}")]
Expand Down Expand Up @@ -46,9 +49,11 @@ pub enum ConfigError {
}

#[derive(Debug, Deserialize)]
struct RplConfig {
run: Option<run::RunConfig>,
patterns: Option<patterns::PatternsConfig>,
pub struct RplConfig {
pub(crate) run: Option<run::RunConfig>,
pub(crate) patterns: Option<patterns::PatternsConfig>,
#[serde(default)]
pub ops: std::collections::HashMap<String, Vec<RawOpInstance>>,
}

#[derive(Debug)]
Expand All @@ -72,3 +77,22 @@ pub fn load_config(manifest_path: Option<&Path>, selected_groups: &[String]) ->
inline_mir,
})
}

/// Load the raw op-group instances from `rpl.toml` in the current directory
/// (or adjacent to `manifest_path` if given).
///
/// Returns an empty map when no `rpl.toml` exists or when the file contains
/// no `[ops]` table. Errors during config file reading are surfaced as
/// `Err(ConfigError)`.
pub fn load_raw_ops(
manifest_path: Option<&Path>,
) -> Result<std::collections::HashMap<String, Vec<RawOpInstance>>, ConfigError> {
let base_dir = util::resolve_base_dir(manifest_path)?;
let config_path = base_dir.join("rpl.toml");
if config_path.exists() {
let config = util::read_config(&config_path)?;
Ok(config.ops)
} else {
Ok(std::collections::HashMap::new())
}
}
57 changes: 57 additions & 0 deletions crates/rpl_config/src/ops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use std::collections::BTreeMap;

use serde::Deserialize;

/// One instance of an op-group as declared in `rpl.toml`.
///
/// All values are stored as raw strings; substitution happens later in `rpl_context`.
#[derive(Debug, Clone)]
pub struct RawOpInstance {
/// Existential type-placeholder names declared via `type = [...]`.
pub free: Vec<String>,
/// Every other key/value pair (op-level meta-var bindings + op-name bindings).
pub bindings: BTreeMap<String, String>,
}

impl<'de> Deserialize<'de> for RawOpInstance {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut raw: BTreeMap<String, toml::Value> = BTreeMap::deserialize(deserializer)?;

let free = match raw.remove("type") {
None => Vec::new(),
Some(toml::Value::Array(arr)) => arr
.into_iter()
.map(|v| match v {
Comment thread
jellllly420 marked this conversation as resolved.
toml::Value::String(s) => Ok(s),
other => Err(serde::de::Error::custom(format!(
"ops 'type' entries must be strings, got {other:?}"
))),
})
.collect::<Result<Vec<_>, D::Error>>()?,
Some(other) => {
return Err(serde::de::Error::custom(format!(
"ops 'type' must be an array of strings, got {other:?}"
)));
},
};

let mut bindings = BTreeMap::new();
for (k, v) in raw {
match v {
toml::Value::String(s) => {
bindings.insert(k, s);
},
other => {
return Err(serde::de::Error::custom(format!(
"ops binding '{k}' must be a string, got {other:?}"
)));
},
}
}

Ok(RawOpInstance { free, bindings })
}
}
13 changes: 13 additions & 0 deletions crates/rpl_config/src/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ pub(crate) fn resolve_patterns_env(
None => return Ok(None),
};

// An empty paths vector here can arise when `rpl.toml` exists but contains
// no `[patterns]` table (e.g. only `[run]` or `[[ops.<group>]]`) and the
// caller did not pass `--patterns`. In that case `resolve_patterns` takes
// the vacuous-true branch of `selected_groups.iter().all(is_remote_spec)`
// and returns `Some(ResolvedPatterns { paths: vec![] })`. Joining zero
// paths yields the empty string, which would propagate to the child
// process as `RPL_PATS=""` and trip
// `rpl_meta::cli E100: Cannot locate RPL pattern file ""`. Map empty to
// `None` here so the child falls back to the built-in pattern set.
if resolved.paths.is_empty() {
return Ok(None);
}

let mut entries = Vec::with_capacity(resolved.paths.len());
for path in resolved.paths {
if path.to_str().is_none() {
Expand Down
39 changes: 39 additions & 0 deletions crates/rpl_config/tests/ops_loading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use rpl_config::{RawOpInstance, RplConfig};

const TOML: &str = r#"
[[patterns.local]]
name = "g"
path = ["patterns/foo.rpl"]

[[ops.sync]]
type = ["$1"]
T = "std::sync::Mutex<$1>"
U = "std::sync::MutexGuard<$1>"
lock = "$T::lock"
unlock = "$U::drop"

[[ops.sync]]
type = ["$1"]
T = "parking_lot::Mutex<$1>"
U = "parking_lot::MutexGuard<$1>"
lock = "$T::lock"
unlock = "$U::drop"
"#;

#[test]
fn loads_two_sync_instances() {
let cfg: RplConfig = toml::from_str(TOML).expect("parse");
let sync_instances = cfg.ops.get("sync").expect("sync key present");
assert_eq!(sync_instances.len(), 2);
let i0: &RawOpInstance = &sync_instances[0];
assert_eq!(i0.free, vec!["$1".to_string()]);
assert_eq!(i0.bindings.get("T").unwrap(), "std::sync::Mutex<$1>");
assert_eq!(i0.bindings.get("lock").unwrap(), "$T::lock");
}

#[test]
fn type_key_is_not_in_bindings() {
let cfg: RplConfig = toml::from_str(TOML).expect("parse");
let i0 = &cfg.ops["sync"][0];
assert!(!i0.bindings.contains_key("type"));
}
4 changes: 4 additions & 0 deletions crates/rpl_context/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ edition.workspace = true
[dependencies]
derive_more.workspace = true
pest_typed.workspace = true
rpl_config.workspace = true
rpl_meta.workspace = true
rpl_parser.workspace = true
rpl_constraints.workspace = true
sync-arena.workspace = true

[dev-dependencies]
rpl_config.workspace = true

[features]

[package.metadata.rust-analyzer]
Expand Down
24 changes: 23 additions & 1 deletion crates/rpl_context/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ impl<'pcx> PatCtxt<'pcx> {
) {
let pattern = self.new_pattern();
// FIXME: process utils
let (utils, patts, diags) = collect_blocks(main);
let (utils, patts, ops, diags) = collect_blocks(main);
Comment thread
jellllly420 marked this conversation as resolved.

let symbol_tables = &mctx.symbol_tables.get(id).unwrap();
{
Expand All @@ -217,6 +217,22 @@ impl<'pcx> PatCtxt<'pcx> {
);
});
}
{
for ops_block in &ops {
let wf_errors = pattern.add_ops_block(with_path(mctx.get_active_path(), ops_block));
for err in &wf_errors {
warn!("ops well-formedness: {}", err);
}
}
// R6: op-level meta-vars (declared in `ops { ... }`) must not leak
// into pattern-block bodies. Implemented but previously never
// invoked — the rule was unenforced. Surface violations as
// warnings here, alongside R1–R3.
let r6_errors = pat::check_r6_patt_vs_ops(&ops, &patts);
for err in &r6_errors {
warn!("ops well-formedness (R6): {}", err);
}
}
{
let patt_items = patts.iter().flat_map(|patt| patt.get_matched().3.iter_matched());
let patt_symbol_tables = &symbol_tables.patt_symbol_tables;
Expand All @@ -237,6 +253,12 @@ impl<'pcx> PatCtxt<'pcx> {
}
}

// R4/R5 post-lowering use-site checks + referenced_op_groups population.
pattern.check_and_populate_op_refs();
for err in pattern.op_ref_errors() {
warn!("op-ref use-site: {}", err);
}

let mut patterns = self.rpl_patterns.lock();
debug_assert_eq!(patterns.next_index(), id);
patterns.push(pattern);
Expand Down
92 changes: 87 additions & 5 deletions crates/rpl_context/src/pat/mir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,11 @@ pub enum Operand<'pcx> {
Move(Place<'pcx>),
Constant(ConstOperand<'pcx>),
FnPat(Symbol),
/// Reference to an operation `$group::$op` declared in the `ops` block.
OpRef {
group: Symbol,
op: Symbol,
},
}

impl<'pcx> Operand<'pcx> {
Expand Down Expand Up @@ -929,19 +934,25 @@ impl<'pcx> Operand<'pcx> {
) -> Self {
let p = op.path;
match op.inner.deref() {
Choice5::_0(copy_) => Self::from_copy(WithPath::new(p, copy_.get_matched().1), pcx, fn_sym_tab),
Choice5::_1(move_) => Self::from_move(WithPath::new(p, move_.get_matched().1), pcx, fn_sym_tab),
Choice5::_2(type_path) => Self::Constant(ConstOperand::from_type_path(
Choice6::_0(copy_) => Self::from_copy(WithPath::new(p, copy_.get_matched().1), pcx, fn_sym_tab),
Choice6::_1(move_) => Self::from_move(WithPath::new(p, move_.get_matched().1), pcx, fn_sym_tab),
Choice6::_2(type_path) => Self::Constant(ConstOperand::from_type_path(
WithPath::new(p, type_path),
pcx,
fn_sym_tab,
)),
Choice5::_3(lang_item) => Self::Constant(ConstOperand::from_lang_item(
Choice6::_3(lang_item) => Self::Constant(ConstOperand::from_lang_item(
WithPath::new(p, lang_item),
pcx,
fn_sym_tab,
)),
Choice5::_4(meta_var) => Self::from_meta_var(meta_var),
Choice6::_4(op_ref) => {
let (group_meta, op_meta) = op_ref.MetaVariable();
let group = Symbol::intern(group_meta.span.as_str().trim_start_matches('$'));
let op = Symbol::intern(op_meta.span.as_str().trim_start_matches('$'));
Self::OpRef { group, op }
},
Choice6::_5(meta_var) => Self::from_meta_var(meta_var),
}
}
}
Expand Down Expand Up @@ -1569,3 +1580,74 @@ impl BasicBlockData<'_> {
pub(crate) fn with_path<T>(path: &'_ std::path::Path, inner: T) -> WithPath<'_, T> {
WithPath { path, inner }
}

#[cfg(test)]
mod tests {
use rustc_span::Symbol;

use super::Operand;

#[test]
fn op_ref_constructs_and_matches() {
rustc_span::create_session_if_not_set_then(rustc_span::edition::LATEST_STABLE_EDITION, |_| {
let group = Symbol::intern("sync");
let op = Symbol::intern("lock");
let operand: Operand<'_> = Operand::OpRef { group, op };
match operand {
Operand::OpRef { group: g, op: o } => {
assert_eq!(g.as_str(), "sync");
assert_eq!(o.as_str(), "lock");
},
_ => panic!("expected OpRef variant"),
}
});
}

/// Verify that `Operand::from_fn_op` strips the leading `$` when lowering an
/// `OpRef` (`$group::$op`) from the parser, so the stored symbols are bare
/// and will match the keys used by `OpGroup.name` / Task 7's resolver lookup.
///
/// This test uses the simplified extraction form (parse → access OpRef node →
/// apply the same `trim_start_matches('$')` as `from_fn_op` does) rather than
/// calling `from_fn_op` directly, because `from_fn_op` requires a `PatCtxt`
/// that cannot be constructed in a unit test. The test would have FAILED
/// before the fix because `group_meta.span.as_str()` returned `"$sync"` and
/// `op_meta.span.as_str()` returned `"$lock"`.
#[test]
fn op_ref_lowers_with_bare_symbols() {
use pest_typed::TypedParser as _;
use rpl_parser::parser::{Grammar, pairs};

let parsed =
Grammar::try_parse::<pairs::MirFnOperand>("$sync::$lock").expect("$sync::$lock must parse as MirFnOperand");

// `MirFnOperand` is `Choice6`; variant `_4` is `OpRef`.
let op_ref = parsed
.OpRef()
.expect("$sync::$lock must lower to the OpRef variant of MirFnOperand");

let (group_meta, op_meta) = op_ref.MetaVariable();

// Replicate the exact logic from `from_fn_op`'s `Choice6::_4` arm.
let group_raw = group_meta.span.as_str();
let op_raw = op_meta.span.as_str();

// Before the fix both of these would be "$sync" / "$lock".
let group_bare = group_raw.trim_start_matches('$');
let op_bare = op_raw.trim_start_matches('$');

assert_eq!(group_bare, "sync", "group symbol must be bare (no $ prefix)");
assert_eq!(op_bare, "lock", "op symbol must be bare (no $ prefix)");

// Also assert the raw span really does start with `$`, confirming that
// the trim is necessary (i.e., the test would have caught the bug).
assert!(
group_raw.starts_with('$'),
"parser span must include the $ sigil; got {group_raw:?}"
);
assert!(
op_raw.starts_with('$'),
"parser span must include the $ sigil; got {op_raw:?}"
);
}
}
2 changes: 2 additions & 0 deletions crates/rpl_context/src/pat/mir/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ impl Operand<'_> {
Self::Move(place) => write!(f, "(move {place:?})"),
Self::Constant(konst) => write!(f, "{konst:?}"),
Self::FnPat(fn_pat) => write!(f, "${fn_pat}"),
Self::OpRef { group, op } => write!(f, "${group}::{op}"),
}
}
}
Expand All @@ -223,6 +224,7 @@ impl fmt::Debug for Operand<'_> {
Self::Move(place) => write!(f, "move {place:?}"),
Self::Constant(konst) => write!(f, "const {konst:?}"),
Self::FnPat(fn_pat) => write!(f, "const ${fn_pat}"),
Self::OpRef { group, op } => write!(f, "${group}::{op}"),
}
}
}
Expand Down
Loading
Loading