-
Notifications
You must be signed in to change notification settings - Fork 84
MIR: Enforce disjointness from precondition allocations and statics #2678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| MIR_SRCS=test | ||
| include ../support/mir-blobs.mk |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| versions | ||
| rustc 1.86.0-nightly (9cd60bd2c 2025-02-15) | ||
| mir-json mtime: Oct 6 02:23:13 2025 | ||
| mir-json version from cargo: mir-json v0.1.0 | ||
| probable mir-json commit: ce3ab88fd9df99b7c9bff6fd521926c46c62bf25 from Wed Oct 1 15:06:10 2025 and/or Wed Oct 1 15:06:10 2025 | ||
| versions-notes | ||
| Generated by update-from.sh version 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // Aliasing input and output | ||
|
|
||
| pub fn foo_in_out(x: *const u32) -> *const u32 { | ||
| x | ||
| } | ||
|
|
||
| pub fn bar_in_out(x: *const u32) -> bool { | ||
| x == foo_in_out(x) | ||
| } | ||
|
|
||
| // Aliasing input and mutable static | ||
|
|
||
| static mut GLOB_MUT: u32 = 0; | ||
|
|
||
| pub fn foo_in_static_mut(x: *mut u32) -> bool { | ||
| x == &raw mut GLOB_MUT | ||
| } | ||
|
|
||
| pub fn bar_in_static_mut() -> bool { | ||
| foo_in_static_mut(&raw mut GLOB_MUT) | ||
| } | ||
|
|
||
| // Aliasing input and immutable static | ||
|
|
||
| static GLOB: u32 = 0; | ||
|
|
||
| pub fn foo_in_static(x: *const u32) -> bool { | ||
| x == &raw const GLOB | ||
| } | ||
|
|
||
| pub fn bar_in_static() -> bool { | ||
| foo_in_static(&raw const GLOB) | ||
| } | ||
|
|
||
| // Aliasing output and mutable static | ||
|
|
||
| pub fn foo_out_static_mut() -> *mut u32 { | ||
| &raw mut GLOB_MUT | ||
| } | ||
|
|
||
| pub fn bar_out_static_mut() -> bool { | ||
| &raw mut GLOB_MUT == foo_out_static_mut() | ||
| } | ||
|
|
||
| // Aliasing output and immutable static | ||
|
|
||
| pub fn foo_out_static() -> *const u32 { | ||
| &raw const GLOB | ||
| } | ||
|
|
||
| pub fn bar_out_static() -> bool { | ||
| &raw const GLOB == foo_out_static() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| enable_experimental; | ||
|
|
||
| m <- mir_load_module "test.linked-mir.json"; | ||
|
|
||
| // Aliasing input and output | ||
|
|
||
| let foo_in_out_spec = do { | ||
| x <- mir_alloc_raw_ptr_const mir_u32; | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func [x]; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| y <- mir_alloc_raw_ptr_const mir_u32; | ||
| mir_return y; | ||
| }; | ||
|
|
||
| fails (mir_verify m "test::foo_in_out" [] false foo_in_out_spec z3); | ||
|
|
||
| /* | ||
| let bar_in_out_spec0 = do { | ||
| x <- mir_alloc_raw_ptr_const mir_u32; | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func [x]; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ False }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_in_out" [foo_in_out_ov] false bar_in_out_spec0 z3; | ||
|
|
||
| let bar_in_out_spec1 = do { | ||
| x <- mir_alloc_raw_ptr_const mir_u32; | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func [x]; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ True }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_in_out" [] false bar_in_out_spec1 z3; | ||
| */ | ||
|
|
||
| // Aliasing input and mutable static | ||
|
|
||
| let foo_in_static_mut_spec = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| x <- mir_alloc_raw_ptr_mut mir_u32; | ||
| x_contents <- mir_fresh_var "x" mir_u32; | ||
| mir_points_to x (mir_term x_contents); | ||
|
|
||
| mir_execute_func [x]; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_points_to x (mir_term x_contents); | ||
| mir_return (mir_term {{ False }}); | ||
| }; | ||
|
|
||
| foo_in_static_mut_ov <- mir_verify m "test::foo_in_static_mut" [] false foo_in_static_mut_spec z3; | ||
|
|
||
| let bar_in_static_mut_spec0 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ False }}); | ||
| }; | ||
|
|
||
| fails (mir_verify m "test::bar_in_static_mut" [foo_in_static_mut_ov] false bar_in_static_mut_spec0 z3); | ||
|
|
||
| let bar_in_static_mut_spec1 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ True }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_in_static_mut" [] false bar_in_static_mut_spec1 z3; | ||
|
|
||
| // Aliasing input and immutable static | ||
|
|
||
| let foo_in_static_spec = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| x <- mir_alloc_raw_ptr_const mir_u32; | ||
| x_contents <- mir_fresh_var "x" mir_u32; | ||
| mir_points_to x (mir_term x_contents); | ||
|
|
||
| mir_execute_func [x]; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ False }}); | ||
| }; | ||
|
|
||
| foo_in_static_ov <- mir_verify m "test::foo_in_static" [] false foo_in_static_spec z3; | ||
|
|
||
| let bar_in_static_spec0 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ False }}); | ||
| }; | ||
|
|
||
| fails (mir_verify m "test::bar_in_static" [foo_in_static_ov] false bar_in_static_spec0 z3); | ||
|
|
||
| let bar_in_static_spec1 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ True }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_in_static" [] false bar_in_static_spec1 z3; | ||
|
|
||
| // Aliasing output and mutable static | ||
|
|
||
| let foo_out_static_mut_spec = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| y <- mir_alloc_raw_ptr_mut mir_u32; | ||
| mir_return y; | ||
| }; | ||
|
|
||
| fails (mir_verify m "test::foo_out_static_mut" [] false foo_out_static_mut_spec z3); | ||
|
|
||
| /* | ||
| let bar_out_static_mut_spec0 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ False }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_out_static_mut" [foo_out_static_mut_ov] false bar_out_static_mut_spec0 z3; | ||
|
|
||
| let bar_out_static_mut_spec1 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ True }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_out_static_mut" [] false bar_out_static_mut_spec1 z3; | ||
| */ | ||
|
|
||
| // Aliasing output and immutable static | ||
|
|
||
| let foo_out_static_spec = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| y <- mir_alloc_raw_ptr_const mir_u32; | ||
| mir_return y; | ||
| }; | ||
|
|
||
| fails (mir_verify m "test::foo_out_static" [] false foo_out_static_spec z3); | ||
|
|
||
| /* | ||
| let bar_out_static_spec0 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ False }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_out_static" [foo_out_static_ov] false bar_out_static_spec0 z3; | ||
|
|
||
| let bar_out_static_spec1 = do { | ||
| glob_mut_contents <- mir_fresh_var "GLOB_MUT" mir_u32; | ||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
|
|
||
| mir_execute_func []; | ||
|
|
||
| mir_points_to (mir_static "test::GLOB_MUT") (mir_term glob_mut_contents); | ||
| mir_return (mir_term {{ True }}); | ||
| }; | ||
|
|
||
| mir_verify m "test::bar_out_static" [] false bar_out_static_spec1 z3; | ||
| */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| set -e | ||
|
|
||
| $SAW test.saw |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ import Data.IORef (IORef, modifyIORef) | |
| import Data.List (tails) | ||
| import qualified Data.List.NonEmpty as NE | ||
| import qualified Data.Map as Map | ||
| import Data.Map (Map) | ||
| import Data.Maybe (catMaybes) | ||
| import qualified Data.Parameterized.Classes as PC | ||
| import qualified Data.Parameterized.Context as Ctx | ||
|
|
@@ -531,30 +532,44 @@ decodeMIRVal col ty (Crucible.AnyValue repr rv) | |
| Just Refl -> Just (MIRVal shp rv) | ||
| Nothing -> Nothing | ||
|
|
||
| -- | Generate assertions that all of the memory allocations matched by | ||
| -- an override's precondition are disjoint. | ||
| -- | Generate assertions that all of the memory allocations matched by an | ||
| -- override's precondition are disjoint from each other, and from all statics | ||
| -- and any extra allocations passed in. | ||
| enforceDisjointness :: | ||
| MIRCrucibleContext -> W4.ProgramLoc -> StateSpec -> OverrideMatcher MIR w () | ||
| enforceDisjointness cc loc ss = | ||
| MIRCrucibleContext -> | ||
| W4.ProgramLoc -> | ||
| -- | Additional allocations to check disjointness from (from prestate) | ||
| Map AllocIndex (Some MirAllocSpec) -> | ||
| StateSpec -> | ||
| OverrideMatcher MIR w () | ||
| enforceDisjointness cc loc extras ss = | ||
| do let sym = cc^.mccSym | ||
| sub <- OM (use setupValueSub) | ||
| let mems = Map.elems $ Map.intersectionWith (,) (view MS.csAllocs ss) sub | ||
| let mems = Map.elems $ Map.intersection sub (view MS.csAllocs ss) | ||
| let mems2 = Map.elems $ Map.intersection sub extras | ||
|
|
||
| let colState = cc ^. mccRustModule . Mir.rmCS | ||
| let statics = Map.elems $ | ||
| Map.intersectionWith (staticMirPointer sym) | ||
| (colState ^. Mir.collection ^. Mir.statics) | ||
| (colState ^. Mir.staticMap) | ||
|
|
||
| let md = MS.ConditionMetadata | ||
| { MS.conditionLoc = loc | ||
| , MS.conditionTags = mempty | ||
| , MS.conditionType = "memory region disjointness" | ||
| , MS.conditionContext = "" | ||
| } | ||
| -- Ensure that all regions are disjoint from each other. | ||
| -- Ensure that all regions are disjoint from each other and extras and | ||
| -- statics | ||
|
Comment on lines
+563
to
+564
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this only require that mutable regions be disjoint from each other? If I understand correctly, this is checking for all memory regions (both immutable and mutable), which may be responsible for some of the spurious test suite failures seen in CI (e.g.,
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This checks that the regions in question (either precondition allocs or postcondition allocs) are disjoint from each other, and from all extras and statics, but not that the extras and statics are disjoint from each other. As noted in #2665, for MIR we need to check disjointness from immutable statics as well. All 5 test cases, 2 of which involve only immutable statics, lead to unsoundness with the current The test failures seem to be because input and output allocations are indeed aliasing with immutable statics, which includes when you take a reference to a constant like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sorry, let me clarify: we should relax this check to allow immutable references to alias each other, while still requiring immutable raw pointers to be disjoint. (Note that all of the examples in #2665 use raw pointers rather than references.) For instance,
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure we can do that... it seems like this still leads to unsoundness: pub fn foo_out_static() -> &'static u32 {
&42
}
pub fn bar_out_static() -> bool {
let x: &u32 = &42;
x as *const u32 == foo_out_static() as *const u32
}The raw pointers exist only inside the function, so we would need to do a check in crucible for the reference to pointer cast or something.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear, you're saying that if you verified
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow! This is quite tricky. First, a minor correction: I think you want your Rust code to look like this: pub fn foo_out_static() -> &'static u32 {
&42
}
pub fn bar_out_static() -> bool {
let x: &u32 = &42;
std::ptr::eq(x, foo_out_static()) // This line is different
}
My first inclination is to wonder is how the LLVM backend (which explicitly allows immutable pointers to alias each other) deals with this. Here is a port of your program to C: // test.c
#include <stdint.h>
const static uint32_t FORTY_TWO = 42;
const uint32_t* foo_out_static(void) {
return &FORTY_TWO;
}
int bar_out_static(void) {
const uint32_t* x = &FORTY_TWO;
return x == foo_out_static();
}SAW does reject using What an interesting error message! This arises from this code in the eval (LLVM_PtrEq mvar (regValue -> x) (regValue -> y)) = do
mem <- getMem mvar
liftIO $ do
...
v3 <- G.notAliasable sym x y (memImplHeap mem)
...
unless (laxConstantEquality ?memOpts) $
do let allocs_doc = G.ppAllocs (G.memAllocs (memImplHeap mem))
let x_doc = G.ppPtr x
let y_doc = G.ppPtr y
-- TODO: Is this undefined behavior? If so, add to the UB module
assert bak v3 $
AssertFailureSimError
"Const pointers compared for equality"
(unlines [ show x_doc
, show y_doc
, show allocs_doc
])
ptrEq sym PtrWidth x yThis is interesting because, as far as my reading of the C standard goes, this is actually not undefined behavior. Namely, I believe it is fine to check two pointers for equality provided that they point to the same type and come from the same allocation region. This code was introduced in GaloisInc/crucible@58e2be3, whose commit message notes:
This is a pretty brutal solution to the problem. I suppose we could adopt a similar solution in Rust, but that would reject many seemingly valid uses of the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the topic of brutal solutions to problems, we might want to re-examine if we should permit things like this: This is rather fishy, since it is using a On the other hand, if we do reject things like this, we had better have a viable migration story. In the LLVM version of the spec, it suffices to change the details slightly: This only works because Perhaps we could devise a command that lets users look up static items by the parent functions/values, but I don't yet know what a good API for this command would be...
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The I agree that we shouldn't do what the LLVM backend does. In addition to what you mention, I also agree that we should probably reject Maybe we can have something like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, my mistake! (I was confused by the code used in the
This is not guaranteed per this section of the Rust Reference (emphasis mine):
Here is an example of this happening in practice. You need a Cargo workspace that looks like this: With the following contents: # Cargo.toml
[workspace]
members = ["a", "b", "c"]
default-workspace-members = ["c"]# a/Cargo.toml
[package]
name = "a"
version = "0.1.0"
edition = "2024"
[dependencies]// a/src/lib.rs
pub fn f() -> &'static u32 {
&42
}# b/Cargo.toml
[package]
name = "b"
version = "0.1.0"
edition = "2024"
[dependencies]// b/src/lib.rs
pub fn f() -> &'static u32 {
&42
}# c/Cargo.toml
[package]
name = "c"
version = "0.1.0"
edition = "2024"
[dependencies]
a = { path = "../a" }
b = { path = "../b" }// c/src/lib.rs
use a;
use b;
pub fn g() -> u32 {
*a::f() + *b::f()
}Compiling this with This contains two promoted static items with the same types and values. Granted, I had to resort to putting the static items in different crates in order for this to happen, but it wouldn't surprise me if there were a clever way to observe something similar within a single crate.
Indeed, I think we need something like This isn't perfect (there is still a possibility that
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| sequence_ | ||
| [ do c <- liftIO $ W4.notPred sym =<< equalRefsPred cc p q | ||
| addAssert c md a | ||
|
|
||
| | let a = Crucible.SimError loc $ | ||
| Crucible.AssertFailureSimError "Memory regions not disjoint" "" | ||
| , (_, Some p) : ps <- tails mems | ||
| , (_, Some q) <- ps | ||
| , Some p : ps <- tails mems | ||
| , Some q <- ps ++ mems2 ++ statics | ||
| ] | ||
|
|
||
| -- | Perform an allocation as indicated by a 'mir_alloc' | ||
|
|
@@ -948,14 +963,15 @@ learnCond :: | |
| MIRCrucibleContext -> | ||
| CrucibleMethodSpecIR -> | ||
| MS.PrePost -> | ||
| Map AllocIndex (Some MirAllocSpec) -> | ||
| StateSpec -> | ||
| OverrideMatcher MIR w () | ||
| learnCond opts sc cc cs prepost ss = | ||
| learnCond opts sc cc cs prepost extras ss = | ||
| do let loc = cs ^. MS.csLoc | ||
| matchPointsTos opts sc cc cs prepost (ss ^. MS.csPointsTos) | ||
| F.traverse_ (learnSetupCondition opts sc cc cs prepost) (ss ^. MS.csConditions) | ||
| assertTermEqualities sc cc | ||
| enforceDisjointness cc loc ss | ||
| enforceDisjointness cc loc extras ss | ||
| enforceCompleteSubstitution loc ss | ||
|
|
||
| -- | Process a "mir_equal" statement from the precondition | ||
|
|
@@ -1768,7 +1784,7 @@ methodSpecHandler_prestate opts sc cc args cs = | |
|
|
||
| sequence_ [ matchArg opts sc cc cs MS.PreState md x z | (x, z) <- xs] | ||
|
|
||
| learnCond opts sc cc cs MS.PreState (cs ^. MS.csPreState) | ||
| learnCond opts sc cc cs MS.PreState Map.empty (cs ^. MS.csPreState) | ||
|
|
||
| -- | Try to translate the spec\'s 'SetupValue' into a 'MIRVal', pretty-print | ||
| -- the 'MIRVal'. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.