Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 10 additions & 11 deletions Ix/Aiur/Stages/Codegen.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,8 +398,9 @@ private def emitCall (out : Nat) (callee : FunIdx) (args : Array ValIdx)
s!"\{ let __args: [G; IN_{callee}] = {argsStr};" ++
s!" let __cu = {cuExpr};" ++
s!" if let Some(result) = record.function_queries[{callee}].get_mut(&__args[..]) \{" ++
bumpStmt ++
retExpr ++
s!" if !__cu && *result.multiplicity == G::ZERO \{" ++
s!" aiur_fn_{callee}(__args, record, io_buffer, false)?" ++
s!" } else \{" ++ bumpStmt ++ retExpr ++ " }" ++
s!" } else \{ aiur_fn_{callee}(__args, record, io_buffer, __cu)? } }"
let mut stmts : Array RustStmt := #[
.letStmt false "__r_arr" (some s!"[G; OUT_{callee}]") (.lit blockExpr)
Expand DownExpand Up@@ -816,15 +817,13 @@ partial def emitCtrl (funIdx : FunIdx) (mcLabel? : Option String)
let outArr : RustStmt :=
.letStmt false "__ret" (some s!"[G; OUT_{funIdx}]")
(.arrayLit (outs.map valVar))
let insertCall : RustStmt :=
.exprStmt (.call
(.field
(.index (.field (.var "record") "function_queries")
(.lit (toString funIdx)))
"insert")
#[.ref (.index (.var "inp") (.lit "..")),
.ref (.index (.var "__ret") (.lit "..")),
gFromBool (.lit "!unconstrained")])
let insertCall : RustStmt := .exprStmt (.lit <|
s!"if let Some(result) = record.function_queries[{funIdx}].get_mut(&inp[..]) \{" ++
" debug_assert_eq!(result.output, &__ret[..]);" ++
" if !unconstrained { *result.multiplicity += G::ONE; }" ++
" } else {" ++
s!" record.function_queries[{funIdx}].insert(&inp[..], &__ret[..], G::from_bool(!unconstrained));" ++
" }")
-- Wrap in Ok(...) since fn now returns Result<[G; OUT_N], ExecError>.
return #[outArr, insertCall,
.returnStmt (.call (.var "Ok") #[.var "__ret"])]
Expand Down
23 changes: 23 additions & 0 deletions Tests/Aiur/Aiur.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,25 @@ def toplevel := ⟦
}
}

-- Regression: a constrained call must recursively promote an identical
-- query previously cached by an unconstrained call. `promotion_nested`
-- calls another function so promoting only its own multiplicity leaves
-- the nested function channel unbalanced.
fn promotion_leaf(x: G) -> G {
x + 1
}

fn promotion_nested(x: G) -> G {
promotion_leaf(x)
}

pub fn unconstrained_call_promotion(x: G) -> G {
let hinted = #promotion_nested(x);
let constrained = promotion_nested(x);
assert_eq!(hinted, constrained);
constrained
}

---------------------------------------------------------------------------
-- IO
---------------------------------------------------------------------------
Expand DownExpand Up@@ -794,6 +813,10 @@ def aiurTestCases : List AiurTestCase := [
-- Unconstrained recursion: mixed constrained/unconstrained calls
.prove `unconstrained_fibonacci #[6] #[13],

-- A constrained call reuses an identical unconstrained cached query and
-- must recursively promote the nested callee's lookup multiplicity.
.prove `unconstrained_call_promotion #[3] #[4],

-- IO
{ functionName := `read_write_io
inputIOBuffer :=
Expand Down
50 changes: 37 additions & 13 deletions crates/aiur/src/execute.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -327,13 +327,26 @@ impl Function {
},
ExecEntry::Op(Op::Call(callee_idx, args, _, op_unconstrained)) => {
let args: Vec<G> = args.iter().map(|i| map[*i]).collect();
if let Some(result) =
record.function_queries[*callee_idx].get_mut(&args)
{
if !unconstrained && !op_unconstrained {
*result.multiplicity += G::ONE;
}
map.extend_from_slice(result.output);
let callee_unconstrained = unconstrained || *op_unconstrained;
let cached_output = record.function_queries[*callee_idx]
.get_mut(&args)
.and_then(|result| {
// A zero-multiplicity entry was computed only as an
// unconstrained hint. Promoting just this row would omit all
// of the callee's child lookups and unbalance their channels;
// replay the body constrained so promotion recurses through
// the whole dependency tree.
if !callee_unconstrained && result.multiplicity.is_zero() {
None
} else {
if !callee_unconstrained {
*result.multiplicity += G::ONE;
}
Some(result.output.to_vec())
}
});
if let Some(output) = cached_output {
map.extend(output);
} else {
let saved_map = std::mem::replace(&mut map, args);
callers_states_stack.push(CallerState {
Expand All@@ -343,7 +356,7 @@ impl Function {
continuation_depth: continuation_stack.len(),
});
fun_idx = *callee_idx;
unconstrained = unconstrained || *op_unconstrained;
unconstrained = callee_unconstrained;
push_block_exec_entries!(&toplevel.functions[fun_idx].body);
}
},
Expand DownExpand Up@@ -699,11 +712,22 @@ impl Function {
// Register the query.
let input_size = toplevel.functions[fun_idx].layout.input_size;
let output = output.iter().map(|i| map[*i]).collect::<Vec<_>>();
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
if let Some(result) =
record.function_queries[fun_idx].get_mut(&map[..input_size])
{
// The only ordinary way to execute an already cached function
// is constrained promotion of an unconstrained hint entry.
debug_assert_eq!(result.output, output);
if !unconstrained {
*result.multiplicity += G::ONE;
}
} else {
record.function_queries[fun_idx].insert(
&map[..input_size],
&output,
G::from_bool(!unconstrained),
);
}
if let Some(CallerState {
fun_idx: caller_idx,
map: caller_map,
Expand Down
76 changes: 76 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -562,6 +562,82 @@ mod tests {
);
}

/// Exercise promotion of a cached unconstrained call through nested calls.
///
/// `f` first computes `g(x)` as an unconstrained hint, then calls `g(x)`
/// constrained. Since `g` calls `h`, promoting the cached `g` query must
/// replay its body and promote the cached `h` query as well. Merely bumping
/// `g`'s multiplicity leaves the `g -> h` function channel unbalanced.
fn unconstrained_call_promotion_toplevel() -> Toplevel {
let f = Function {
body: Block {
ops: vec![
Op::Call(1, vec![0], 1, true),
Op::Call(1, vec![0], 1, false),
],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 3,
lookups: 2,
},
entry: true,
constrained: true,
};

let g = Function {
body: Block {
ops: vec![Op::Call(2, vec![0], 1, false)],
ctrl: Ctrl::Return(0, vec![1]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 2,
lookups: 2,
},
entry: false,
constrained: true,
};

let h = Function {
body: Block {
ops: vec![Op::Const(G::ONE), Op::Add(0, 1)],
ctrl: Ctrl::Return(0, vec![2]),
},
layout: FunctionLayout {
input_size: 1,
selectors: 1,
auxiliaries: 1,
lookups: 1,
},
entry: false,
constrained: true,
};

Toplevel { functions: vec![f, g, h], memory_sizes: vec![] }
}

#[test]
fn prove_verify_promotes_nested_unconstrained_call() {
let (cp, fp) = test_parameters();
let system =
AiurSystem::build(unconstrained_call_promotion_toplevel(), cp, fp);
let input = [G::from_u64(3)];
let mut io_buffer = empty_io_buffer();

let (claim, proof) = system.prove(0, &input, &mut io_buffer);
assert_eq!(
claim,
vec![function_channel(), G::ZERO, input[0], input[0] + G::ONE]
);
system
.verify(&claim, &proof)
.expect("nested constrained promotion must balance function channels");
}

#[test]
fn prove_verify_mul_roundtrip() {
let (cp, fp) = test_parameters();
Expand Down
Loading