Skip to content

Commit 2c84b7f

Browse files
authored
Add cut-point rules for specific functions / intrinsics (via definition)) (#960)
This PR builds upon #931 modifying the approach in response to the comments on that PR. For full context read #931 _first_. The `kmir prove-rs` flag `--break-on-function` is implemented in this PR as a compiled definition with hooked function to retrieve the function names to match on. This is similar to the already existing pattern that compiles the static data of a KMIR configuration into the definition. This allows for functions to be provided both when creating the initial proof, and when reading from disc (triggers a recompile of llvm if different flags are provided). I added a test to demonstrate this working on functions and intrinsics, only matching those provided. I do not have a test from reading a partial proof and adding different function names - I did test it but it seemed a bit overboard for a test just now. I did try the method with [K shell access impure function](https://github.com/runtimeverification/k/blob/master/k-distribution/include/kframework/builtin/domains.md#shell-access), however this created branching for every function call since the result was stored in a symbolic value. I couldn't figure out how to get that working concretely (I don't think it is possible but might be wrong).
1 parent f2d2c5e commit 2c84b7f

9 files changed

Lines changed: 235 additions & 3 deletions

File tree

kmir/src/kmir/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,13 @@ def _arg_parser() -> ArgumentParser:
410410
action='store_true',
411411
help='Break on every MIR step (statements and terminators)',
412412
)
413+
prove_args.add_argument(
414+
'--break-on-function',
415+
dest='break_on_function',
416+
action='append',
417+
default=None,
418+
help='Break when calling functions / intrinsics matching this name (repeatable)',
419+
)
413420

414421
proof_args = ArgumentParser(add_help=False)
415422
proof_args.add_argument('id', metavar='PROOF_ID', help='The id of the proof to view')
@@ -638,6 +645,7 @@ def _parse_args(ns: Namespace) -> KMirOpts:
638645
break_every_step=ns.break_every_step,
639646
terminate_on_thunk=ns.terminate_on_thunk,
640647
add_module=ns.add_module,
648+
break_on_function=ns.break_on_function or [],
641649
)
642650
case 'link':
643651
return LinkOpts(

kmir/src/kmir/_prove.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def _prove_rs(opts: ProveRSOpts, target_path: Path, label: str) -> APRProof:
6363
symbolic=True,
6464
haskell_target=opts.haskell_target,
6565
llvm_lib_target=opts.llvm_lib_target,
66+
break_on_function=opts.break_on_function or None,
6667
)
6768
else:
6869
_LOGGER.info(f'Constructing initial proof: {label}')
@@ -92,6 +93,7 @@ def _prove_rs(opts: ProveRSOpts, target_path: Path, label: str) -> APRProof:
9293
symbolic=True,
9394
haskell_target=opts.haskell_target,
9495
llvm_lib_target=opts.llvm_lib_target,
96+
break_on_function=opts.break_on_function or None,
9597
)
9698

9799
proof = apr_proof_from_smir(
@@ -122,6 +124,7 @@ def _prove_rs(opts: ProveRSOpts, target_path: Path, label: str) -> APRProof:
122124
break_on_terminator_unreachable=opts.break_on_terminator_unreachable,
123125
break_every_terminator=opts.break_every_terminator,
124126
break_every_step=opts.break_every_step,
127+
break_on_function=opts.break_on_function,
125128
)
126129

127130
if opts.max_workers and opts.max_workers > 1:
@@ -251,6 +254,7 @@ def _cut_point_rules(
251254
break_on_terminator_unreachable: bool,
252255
break_every_terminator: bool,
253256
break_every_step: bool,
257+
break_on_function: list[str] | None = None,
254258
) -> list[str]:
255259
cut_point_rules = []
256260
if break_on_thunk:
@@ -291,6 +295,9 @@ def _cut_point_rules(
291295
or break_every_step
292296
):
293297
cut_point_rules.append('KMIR-CONTROL-FLOW.termCallFunction')
298+
if break_on_function:
299+
cut_point_rules.append('KMIR-CONTROL-FLOW.termCallFunctionFilter')
300+
cut_point_rules.append('KMIR-CONTROL-FLOW.termCallIntrinsicFilter')
294301
if break_on_terminator_assert or break_every_terminator or break_every_step:
295302
cut_point_rules.append('KMIR-CONTROL-FLOW.termAssert')
296303
if break_on_terminator_drop or break_every_terminator or break_every_step:

kmir/src/kmir/kdist/mir-semantics/kmir.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ See [`rt/configuration.md`](./rt/configuration.md) for a detailed description of
2828
```k
2929
module KMIR-CONTROL-FLOW
3030
imports BOOL
31+
imports COLLECTIONS
3132
imports LIST
3233
imports MAP
34+
imports STRING
3335
imports K-EQUAL
3436
3537
imports MONO
@@ -325,6 +327,15 @@ where the returned result should go.
325327
=> #execIntrinsic(FUNC, ARGS, DEST, SPAN) ~> #continueAt(TARGET)
326328
</k>
327329
requires isIntrinsicFunction(FUNC)
330+
andBool notBool #functionNameMatchesEnv(getFunctionName(FUNC))
331+
332+
// Intrinsic function call to a function in the break-on set - same as termCallIntrinsic but separate rule id for cut-point
333+
rule [termCallIntrinsicFilter]:
334+
<k> #execTerminatorCall(_, FUNC, ARGS, DEST, TARGET, _UNWIND, SPAN) ~> _
335+
=> #execIntrinsic(FUNC, ARGS, DEST, SPAN) ~> #continueAt(TARGET)
336+
</k>
337+
requires isIntrinsicFunction(FUNC)
338+
andBool #functionNameMatchesEnv(getFunctionName(FUNC))
328339
329340
// Regular function call - full state switching and stack setup
330341
rule [termCallFunction]:
@@ -342,11 +353,72 @@ where the returned result should go.
342353
</currentFrame>
343354
<stack> STACK => ListItem(StackFrame(OLDCALLER, OLDDEST, OLDTARGET, OLDUNWIND, LOCALS)) STACK </stack>
344355
requires notBool isIntrinsicFunction(FUNC)
356+
andBool notBool #functionNameMatchesEnv(getFunctionName(FUNC))
357+
358+
// Function call to a function in the break-on set - same as termCallFunction but separate rule id for cut-point
359+
rule [termCallFunctionFilter]:
360+
<k> #execTerminatorCall(FTY, FUNC, ARGS, DEST, TARGET, UNWIND, SPAN) ~> _
361+
=> #setUpCalleeData(FUNC, ARGS, SPAN)
362+
</k>
363+
<currentFunc> CALLER => FTY </currentFunc>
364+
<currentFrame>
365+
<currentBody> _ </currentBody>
366+
<caller> OLDCALLER => CALLER </caller>
367+
<dest> OLDDEST => DEST </dest>
368+
<target> OLDTARGET => TARGET </target>
369+
<unwind> OLDUNWIND => UNWIND </unwind>
370+
<locals> LOCALS </locals>
371+
</currentFrame>
372+
<stack> STACK => ListItem(StackFrame(OLDCALLER, OLDDEST, OLDTARGET, OLDUNWIND, LOCALS)) STACK </stack>
373+
requires notBool isIntrinsicFunction(FUNC)
374+
andBool #functionNameMatchesEnv(getFunctionName(FUNC))
345375
346376
syntax Bool ::= isIntrinsicFunction(MonoItemKind) [function]
347377
rule isIntrinsicFunction(IntrinsicFunction(_)) => true
348378
rule isIntrinsicFunction(_) => false [owise]
349379
380+
syntax String ::= getFunctionName(MonoItemKind) [function, total]
381+
//---------------------------------------------------------------
382+
rule getFunctionName(monoItemFn(symbol(NAME), _, _)) => NAME
383+
rule getFunctionName(monoItemStatic(symbol(NAME), _, _)) => NAME
384+
rule getFunctionName(monoItemGlobalAsm(_)) => ""
385+
rule getFunctionName(IntrinsicFunction(symbol(NAME))) => NAME
386+
387+
// Check whether a function name matches any filter in the break-on-functions list.
388+
syntax Bool ::= #functionNameMatchesEnv(String) [function, total]
389+
//----------------------------------------------------------------
390+
rule #functionNameMatchesEnv(NAME) => #functionNameMatchesEnvStr(NAME, #breakOnFunctionsString(0))
391+
392+
// The Int argument is unused; it exists only so the Haskell backend can
393+
// pattern-match on it and not error since zero-argument functions cannot use [owise].
394+
syntax String ::= #breakOnFunctionsString(Int) [function, total, symbol(breakOnFunctionsString)]
395+
//-----------------------------------------------------------------------------------------------
396+
rule #breakOnFunctionsString(_) => "" [owise] // This gets overridden by corresponding python function
397+
398+
syntax Bool ::= #functionNameMatchesEnvStr(String, String) [function, total]
399+
//--------------------------------------------------------------------------
400+
rule #functionNameMatchesEnvStr(_, "") => false
401+
rule #functionNameMatchesEnvStr(NAME, ENV) => #functionNameMatchesAnyList(NAME, #splitSemicolon(ENV))
402+
requires ENV =/=String ""
403+
404+
syntax List ::= #splitSemicolon(String) [function, total]
405+
//--------------------------------------------------------
406+
rule #splitSemicolon(S) => #splitSemicolonAux(S, findString(S, ";", 0))
407+
408+
syntax List ::= #splitSemicolonAux(String, Int) [function, total]
409+
//-----------------------------------------------------------------
410+
rule #splitSemicolonAux(S, -1) => ListItem(S)
411+
rule #splitSemicolonAux(S, I) =>
412+
ListItem(substrString(S, 0, I)) #splitSemicolon(substrString(S, I +Int 1, lengthString(S)))
413+
requires I >=Int 0
414+
415+
syntax Bool ::= #functionNameMatchesAnyList(String, List) [function, total]
416+
//-------------------------------------------------------------------------
417+
rule #functionNameMatchesAnyList(_, .List) => false
418+
rule #functionNameMatchesAnyList(NAME, ListItem(FILTER:String) REST) =>
419+
0 <=Int findString(NAME, FILTER, 0) orBool #functionNameMatchesAnyList(NAME, REST)
420+
rule #functionNameMatchesAnyList(_, _) => false [owise]
421+
350422
syntax KItem ::= #continueAt(MaybeBasicBlockIdx)
351423
rule <k> #continueAt(someBasicBlockIdx(TARGET)) => #execBlockIdx(TARGET) ... </k>
352424
rule <k> #continueAt(noBasicBlockIdx) => .K ... </k>

kmir/src/kmir/kmir.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def from_kompiled_kore(
6363
llvm_target: str | None = None,
6464
llvm_lib_target: str | None = None,
6565
haskell_target: str | None = None,
66+
break_on_function: list[str] | None = None,
6667
) -> KMIR:
6768
from .kompile import kompile_smir
6869

@@ -75,6 +76,7 @@ def from_kompiled_kore(
7576
llvm_target=llvm_target,
7677
llvm_lib_target=llvm_lib_target,
7778
haskell_target=haskell_target,
79+
break_on_function=break_on_function,
7880
)
7981
return kompiled_smir.create_kmir(bug_report_file=bug_report)
8082

kmir/src/kmir/kompile.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class KompileDigest:
6565
llvm_target: str
6666
llvm_lib_target: str
6767
haskell_target: str
68+
break_on_function: str
6869

6970
@staticmethod
7071
def load(target_dir: Path) -> KompileDigest:
@@ -80,6 +81,7 @@ def load(target_dir: Path) -> KompileDigest:
8081
llvm_target=data['llvm-target'],
8182
llvm_lib_target=data['llvm-lib-target'],
8283
haskell_target=data['haskell-target'],
84+
break_on_function=data.get('break-on-function', ''),
8385
)
8486

8587
def write(self, target_dir: Path) -> None:
@@ -91,6 +93,7 @@ def write(self, target_dir: Path) -> None:
9193
'llvm-target': self.llvm_target,
9294
'llvm-lib-target': self.llvm_lib_target,
9395
'haskell-target': self.haskell_target,
96+
'break-on-function': self.break_on_function,
9497
},
9598
),
9699
)
@@ -205,6 +208,7 @@ def kompile_smir(
205208
llvm_target: str | None = None,
206209
llvm_lib_target: str | None = None,
207210
haskell_target: str | None = None,
211+
break_on_function: list[str] | None = None,
208212
) -> KompiledSMIR:
209213
kompile_digest: KompileDigest | None = None
210214
try:
@@ -222,6 +226,7 @@ def kompile_smir(
222226
llvm_target=llvm_target,
223227
llvm_lib_target=llvm_lib_target,
224228
haskell_target=haskell_target,
229+
break_on_function=';'.join(break_on_function) if break_on_function else '',
225230
)
226231

227232
target_hs_path = target_dir / 'haskell'
@@ -242,7 +247,7 @@ def kompile_smir(
242247

243248
haskell_def_dir = kdist.which(haskell_target)
244249
kmir = KMIR(haskell_def_dir)
245-
smir_rules: list[Sentence] = list(make_kore_rules(kmir, smir_info))
250+
smir_rules: list[Sentence] = list(make_kore_rules(kmir, smir_info, break_on_function=break_on_function))
246251
_LOGGER.info(f'Generated {len(smir_rules)} function equations to add to `definition.kore')
247252

248253
# Load and convert extra module rules if provided
@@ -437,7 +442,9 @@ def _make_stratified_rules(
437442
return [*declarations, *dispatch, *defaults, *equations]
438443

439444

440-
def make_kore_rules(kmir: KMIR, smir_info: SMIRInfo) -> Sequence[Sentence]:
445+
def make_kore_rules(
446+
kmir: KMIR, smir_info: SMIRInfo, *, break_on_function: list[str] | None = None
447+
) -> Sequence[Sentence]:
441448
# kprint tool is too chatty
442449
kprint_logger = logging.getLogger('pyk.ktool.kprint')
443450
kprint_logger.setLevel(logging.WARNING)
@@ -489,7 +496,12 @@ def get_int_arg(app: KInner) -> int:
489496
kmir, 'lookupAlloc', 'AllocId', 'Evaluation', 'allocId', allocs, invalid_alloc_n
490497
)
491498

492-
return [*equations, *type_equations, *alloc_equations]
499+
# Generate break-on-function filter rule if filters are provided
500+
break_on_rules: list[Axiom] = []
501+
if break_on_function:
502+
break_on_rules.append(_mk_break_on_functions_rule(kmir, break_on_function))
503+
504+
return [*equations, *type_equations, *alloc_equations, *break_on_rules]
493505

494506

495507
def _functions(kmir: KMIR, smir_info: SMIRInfo) -> dict[int, KInner]:
@@ -544,6 +556,30 @@ def _mk_equation(kmir: KMIR, fun: str, arg: KInner, arg_sort: str, result: KInne
544556
return rule.to_axiom()
545557

546558

559+
def _mk_break_on_functions_rule(kmir: KMIR, break_on_function: list[str]) -> Axiom:
560+
"""Generate Kore rule for filtering function breaks: `#breakOnFunctionsString(0) => "filter1;filter2;..."`"""
561+
from pyk.kore.prelude import int_dv
562+
from pyk.kore.rule import FunctionRule
563+
564+
filter_string = ';'.join(break_on_function)
565+
fun_app = App('LblbreakOnFunctionsString', (), (int_dv(0),))
566+
result_kore = kmir.kast_to_kore(stringToken(filter_string), KSort('String'))
567+
568+
rule = FunctionRule(
569+
lhs=fun_app,
570+
rhs=result_kore,
571+
req=None,
572+
ens=None,
573+
sort=SortApp('SortString'),
574+
arg_sorts=(SortApp('SortInt'),),
575+
anti_left=None,
576+
priority=50,
577+
uid='breakOnFunctionsString-generated',
578+
label='breakOnFunctionsString-generated',
579+
)
580+
return rule.to_axiom()
581+
582+
547583
def _decode_alloc(smir_info: SMIRInfo, raw_alloc: Any) -> tuple[KInner, KInner]:
548584
from .decoding import UnableToDecodeValue, decode_alloc_or_unable
549585

kmir/src/kmir/options.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class ProveOpts(KMirOpts):
8585
break_every_terminator: bool
8686
break_every_step: bool
8787
terminate_on_thunk: bool
88+
break_on_function: list[str]
8889

8990
def __init__(
9091
self,
@@ -113,6 +114,7 @@ def __init__(
113114
break_every_terminator: bool = False,
114115
break_every_step: bool = False,
115116
terminate_on_thunk: bool = False,
117+
break_on_function: list[str] | None = None,
116118
) -> None:
117119
self.proof_dir = Path(proof_dir).resolve() if proof_dir is not None else None
118120
self.haskell_target = haskell_target
@@ -138,6 +140,7 @@ def __init__(
138140
self.break_every_terminator = break_every_terminator
139141
self.break_every_step = break_every_step
140142
self.terminate_on_thunk = terminate_on_thunk
143+
self.break_on_function = break_on_function if break_on_function is not None else []
141144

142145

143146
@dataclass
@@ -182,6 +185,7 @@ def __init__(
182185
break_every_step: bool = False,
183186
terminate_on_thunk: bool = False,
184187
add_module: Path | None = None,
188+
break_on_function: list[str] | None = None,
185189
) -> None:
186190
self.rs_file = rs_file
187191
self.proof_dir = Path(proof_dir).resolve() if proof_dir is not None else None
@@ -213,6 +217,7 @@ def __init__(
213217
self.break_every_step = break_every_step
214218
self.terminate_on_thunk = terminate_on_thunk
215219
self.add_module = add_module
220+
self.break_on_function = break_on_function if break_on_function is not None else []
216221

217222

218223
@dataclass
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#![feature(core_intrinsics)]
2+
3+
fn foo() {
4+
let x = std::hint::black_box(42);
5+
bar();
6+
assert!(x == 42);
7+
}
8+
9+
fn bar() {
10+
std::intrinsics::assert_inhabited::<i32>();
11+
}
12+
13+
fn main() {
14+
foo();
15+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
┌─ 1 (root, init)
3+
│ #execTerminator ( terminator ( ... kind: terminatorKindCall ( ... func: operandC
4+
│ span: src/rust/library/std/src/rt.rs:194
5+
6+
│ (7 steps)
7+
├─ 3
8+
│ #execTerminatorCall ( ty ( 31 ) , monoItemFn ( ... name: symbol ( "foo" ) , id:
9+
│ function: main
10+
│ span: /prove-rs/break-on-function.rs:4
11+
12+
│ (1 step)
13+
├─ 4
14+
│ #setUpCalleeData ( monoItemFn ( ... name: symbol ( "foo" ) , id: defId ( 7 ) , b
15+
│ function: foo
16+
│ span: /prove-rs/break-on-function.rs:4
17+
18+
│ (5 steps)
19+
├─ 5
20+
│ #execTerminatorCall ( ty ( 26 ) , monoItemFn ( ... name: symbol ( "std::hint::bl
21+
│ function: foo
22+
│ span: /rust/library/core/src/hint.rs:389
23+
24+
│ (1 step)
25+
├─ 6
26+
│ #setUpCalleeData ( monoItemFn ( ... name: symbol ( "std::hint::black_box::<i32>"
27+
│ function: std::hint::black_box::<i32>
28+
│ span: /rust/library/core/src/hint.rs:389
29+
30+
│ (12 steps)
31+
├─ 7
32+
│ #execTerminatorCall ( ty ( 25 ) , IntrinsicFunction ( symbol ( "black_box" ) ) ,
33+
│ function: std::hint::black_box::<i32>
34+
│ span: /rust/library/core/src/hint.rs:389
35+
36+
│ (1 step)
37+
├─ 8
38+
│ #execIntrinsic ( IntrinsicFunction ( symbol ( "black_box" ) ) , operandMove ( pl
39+
│ function: std::hint::black_box::<i32>
40+
│ span: /rust/library/core/src/hint.rs:389
41+
42+
│ (41 steps)
43+
├─ 9 (terminal)
44+
│ #EndProgram ~> .K
45+
│ function: main
46+
47+
┊ constraint: true
48+
┊ subst: ...
49+
└─ 2 (leaf, target, terminal)
50+
#EndProgram ~> .K

0 commit comments

Comments
 (0)