Skip to content

Commit 21cc66c

Browse files
Add mock libfunc debug utility (#1342)
* Implement mock functions * Add some documentation * Add example * Ignore compilation --------- Co-authored-by: Gabriel Bosio <38794644+gabrielbosio@users.noreply.github.com>
1 parent 59ac3dc commit 21cc66c

File tree

2 files changed

+200
-5
lines changed

2 files changed

+200
-5
lines changed

docs/debugging.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,3 +440,27 @@ In the `scripts` folder of starknet-replay, you can find useful scripts for debu
440440
> ./scripts/string-to-felt.sh "u256_mul Overflow"
441441
753235365f6d756c204f766572666c6f77
442442
```
443+
444+
## Debugging Compilation
445+
446+
If we encounter contracts/programs that take too long to compile, the first step is to pinpoint what is causing the long compilation times.
447+
448+
If we find that a particular libfunc is taking too much time to compile/optimize, we should consider moving that libfunc to the runtime. First, we need to check if it would give any improvements at all. To do this, we can "fake" a runtime call to trick the compiler into thinking that a particular libfunc is implemented externally. If we just "delete" the libfunc implementation, we may allow the compiler to optimize a lot of instructions away. This would hide the actual problem.
449+
450+
For details on how to do this, see the debugging functions `build_mock_runtime_call` and `build_mock_libfunc`. The latter is fully generic, and can be used as a replacement for any libfunc implementation.
451+
452+
For example, to check if the `eval_circuit` libfunc is taking too much time to compile, just replace this:
453+
```rust,ignore
454+
// at src/libfuncs/circuit.rs
455+
CircuitConcreteLibfunc::Eval(info) => {
456+
build_eval(context, registry, entry, location, helper, metadata, info)
457+
}
458+
```
459+
With this:
460+
```rust,ignore
461+
CircuitConcreteLibfunc::Eval(info) => {
462+
build_mock_libfunc(context, registry, entry, location, helper, metadata, info.signature())
463+
}
464+
```
465+
466+
Note that sometimes the problem is not a libfunc, but the actual types involved. In these cases mocking a libunc may not help, as doing so would have to operate with those complex types anyway (particularly, loading them from pointers).

src/libfuncs.rs

Lines changed: 176 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
metadata::MetadataStorage,
88
native_panic,
99
types::TypeBuilder,
10+
utils::ProgramRegistryExt,
1011
};
1112
use bumpalo::Bump;
1213
use cairo_lang_sierra::{
@@ -16,21 +17,32 @@ use cairo_lang_sierra::{
1617
signed::{Sint16Traits, Sint32Traits, Sint64Traits, Sint8Traits},
1718
unsigned::{Uint16Traits, Uint32Traits, Uint64Traits, Uint8Traits},
1819
},
19-
lib_func::ParamSignature,
20+
lib_func::{BranchSignature, ParamSignature},
2021
starknet::StarknetTypeConcrete,
2122
ConcreteLibfunc,
2223
},
2324
ids::FunctionId,
2425
program_registry::ProgramRegistry,
2526
};
27+
use itertools::Itertools;
2628
use melior::{
27-
dialect::{arith, cf},
28-
helpers::{ArithBlockExt, BuiltinBlockExt},
29-
ir::{Block, BlockLike, BlockRef, Location, Module, Region, Value},
29+
dialect::{arith, cf, llvm, ods},
30+
helpers::{ArithBlockExt, BuiltinBlockExt, LlvmBlockExt},
31+
ir::{
32+
attribute::{FlatSymbolRefAttribute, StringAttribute, TypeAttribute},
33+
operation::OperationBuilder,
34+
r#type::IntegerType,
35+
Attribute, Block, BlockLike, BlockRef, Location, Module, Region, Value,
36+
},
3037
Context,
3138
};
3239
use num_bigint::BigInt;
33-
use std::{cell::Cell, error::Error, ops::Deref};
40+
use std::{
41+
cell::Cell,
42+
error::Error,
43+
ops::Deref,
44+
sync::atomic::{AtomicBool, Ordering},
45+
};
3446

3547
mod array;
3648
mod r#bool;
@@ -556,3 +568,162 @@ fn build_noop<'ctx, 'this, const N: usize, const PROCESS_BUILTINS: bool>(
556568

557569
helper.br(entry, 0, &params, location)
558570
}
571+
572+
/// This function builds a fake libfunc implementation, by mocking a call to a
573+
/// runtime function.
574+
///
575+
/// Useful to trick MLIR into thinking that it cannot optimize an unimplemented libfunc.
576+
///
577+
/// This function is for debugging only, and should never be used.
578+
#[allow(dead_code)]
579+
pub fn build_mock_libfunc<'ctx, 'this>(
580+
context: &'ctx Context,
581+
registry: &ProgramRegistry<CoreType, CoreLibfunc>,
582+
entry: &'this Block<'ctx>,
583+
location: Location<'ctx>,
584+
helper: &LibfuncHelper<'ctx, 'this>,
585+
metadata: &mut MetadataStorage,
586+
branch_signatures: &[BranchSignature],
587+
) -> Result<()> {
588+
let mut args = Vec::new();
589+
for arg_idx in 0..entry.argument_count() {
590+
args.push(entry.arg(arg_idx)?);
591+
}
592+
593+
let flag_type = IntegerType::new(context, 8).into();
594+
let ptr_type = llvm::r#type::pointer(context, 0);
595+
let result_type = llvm::r#type::r#struct(context, &[flag_type, ptr_type], false);
596+
597+
// Mock a runtime call, and pass all libfunc arguments.
598+
let result_ptr = build_mock_runtime_call(context, helper, entry, &args, location)?;
599+
600+
// We read the result as a structure, with a flag and a pointer.
601+
// The flag determines which libfunc branch should we jump to.
602+
let result = entry.load(context, location, result_ptr, result_type)?;
603+
let flag = entry.extract_value(context, location, result, flag_type, 0)?;
604+
let payload_ptr = entry.extract_value(context, location, result, ptr_type, 1)?;
605+
606+
let branches_idxs = (0..branch_signatures.len()).collect_vec();
607+
608+
// We will build one block per branch + a default block, and will use the
609+
// flag to determine to which block to jump to.
610+
611+
// We assume that the flag is within the number of branches
612+
// So the default block will be unreachable.
613+
let default_block = {
614+
let block = helper.append_block(Block::new(&[]));
615+
block.append_operation(llvm::unreachable(location));
616+
block
617+
};
618+
619+
// For each branch, we build a block that will build the return arguments.
620+
let mut destinations = Vec::new();
621+
for &branch_idx in &branches_idxs {
622+
let block = helper.append_block(Block::new(&[]));
623+
624+
// We build all the required types.
625+
let mut branch_types = Vec::new();
626+
for branch_var in &branch_signatures[branch_idx].vars {
627+
let branch_var_type = registry.build_type(context, helper, metadata, &branch_var.ty)?;
628+
branch_types.push(branch_var_type);
629+
}
630+
631+
// The runtime call payload will be interpreted as a structure with as
632+
// many pointers as there are output variables.
633+
let branch_type = llvm::r#type::r#struct(
634+
context,
635+
&(0..branch_types.len()).map(|_| ptr_type).collect_vec(),
636+
false,
637+
);
638+
639+
let branch_result = block.load(context, location, payload_ptr, branch_type)?;
640+
641+
// We load each pointer to get the actual value we want to return.
642+
let mut branch_results = Vec::new();
643+
for (var_idx, var_type) in branch_types.iter().enumerate() {
644+
let var_ptr =
645+
block.extract_value(context, location, branch_result, ptr_type, var_idx)?;
646+
let var = block.load(context, location, var_ptr, *var_type)?;
647+
648+
branch_results.push(var);
649+
}
650+
651+
// We jump to the target branch.
652+
helper.br(block, branch_idx, &branch_results, location)?;
653+
654+
let operands: &[Value] = &[];
655+
destinations.push((block, operands));
656+
}
657+
658+
// Switch to the target block according to the flag.
659+
entry.append_operation(cf::switch(
660+
context,
661+
&branches_idxs.iter().map(|&x| x as i64).collect_vec(),
662+
flag,
663+
flag_type,
664+
(default_block, &[]),
665+
&destinations[..],
666+
location,
667+
)?);
668+
669+
Ok(())
670+
}
671+
672+
/// This function builds a fake call to a runtime variable.
673+
///
674+
/// Useful to trick MLIR into thinking that it cannot optimize an unimplemented feature.
675+
///
676+
/// This function is for debugging only, and should never be used.
677+
#[allow(dead_code)]
678+
pub fn build_mock_runtime_call<'c, 'a>(
679+
context: &'c Context,
680+
module: &Module,
681+
block: &'a Block<'c>,
682+
args: &[Value<'c, 'a>],
683+
location: Location<'c>,
684+
) -> Result<Value<'c, 'a>> {
685+
let ptr_type = llvm::r#type::pointer(context, 0);
686+
687+
// First, declare the global if not declared.
688+
// This should be added to the `RuntimeBindings` metadata, to ensure that
689+
// it is declared once per module. Here we use a static for simplicity, but
690+
// will fail if a single process is used to compile multiple modules.
691+
static MOCK_RUNTIME_SYMBOL_DECLARED: AtomicBool = AtomicBool::new(false);
692+
if !MOCK_RUNTIME_SYMBOL_DECLARED.swap(true, Ordering::Relaxed) {
693+
module.body().append_operation(
694+
ods::llvm::mlir_global(
695+
context,
696+
Region::new(),
697+
TypeAttribute::new(ptr_type),
698+
StringAttribute::new(context, "cairo_native__mock"),
699+
Attribute::parse(context, "#llvm.linkage<weak>")
700+
.ok_or(CoreLibfuncBuilderError::ParseAttributeError)?,
701+
location,
702+
)
703+
.into(),
704+
);
705+
}
706+
707+
// Obtain a pointer to the global. The global would contain a pointer to a function.
708+
let function_ptr_ptr = block.append_op_result(
709+
ods::llvm::mlir_addressof(
710+
context,
711+
ptr_type,
712+
FlatSymbolRefAttribute::new(context, "cairo_native__mock"),
713+
location,
714+
)
715+
.into(),
716+
)?;
717+
718+
// Load the function pointer, and call the function
719+
let function_ptr = block.load(context, location, function_ptr_ptr, ptr_type)?;
720+
let result = block.append_op_result(
721+
OperationBuilder::new("llvm.call", location)
722+
.add_operands(&[function_ptr])
723+
.add_operands(args)
724+
.add_results(&[llvm::r#type::pointer(context, 0)])
725+
.build()?,
726+
)?;
727+
728+
Ok(result)
729+
}

0 commit comments

Comments
 (0)