use std::io::{self, Write};
use std::path::{Path, PathBuf};
use anyhow::Context;
use cairo_lang_compiler::diagnostics::DiagnosticsReporter;
use cairo_lang_compiler::project::{check_compiler_path, setup_project};
use cairo_lang_debug::debug::DebugWithDb;
use cairo_lang_executable::compile::{
CompileExecutableResult, ExecutableConfig, compile_executable_in_prepared_db, prepare_db,
};
use cairo_lang_executable::executable::{EntryPointKind, Executable, NOT_RETURNING_HEADER_SIZE};
use cairo_lang_execute_utils::{program_and_hints_from_executable, user_args_from_flags};
use cairo_lang_filesystem::ids::CrateInput;
use cairo_lang_runnable_utils::builder::RunnableBuilder;
use cairo_lang_runner::casm_run::format_for_panic;
use cairo_lang_runner::clap::RunProfilerConfigArg;
use cairo_lang_runner::profiling::{
ProfilingInfo, ProfilingInfoProcessor, ProfilingInfoProcessorParams,
};
use cairo_lang_runner::{Arg, CairoHintProcessor, ProfilingInfoCollectionConfig};
use cairo_lang_sierra_generator::debug_info::SierraProgramDebugInfo;
use cairo_lang_sierra_generator::replace_ids::replace_sierra_ids_in_program;
use cairo_vm::cairo_run::{CairoRunConfig, cairo_run_program};
use cairo_vm::types::layout::CairoLayoutParams;
use cairo_vm::types::layout_name::LayoutName;
use cairo_vm::vm::errors::cairo_run_errors::CairoRunError;
use cairo_vm::vm::trace::trace_entry::RelocatedTraceEntry;
use clap::Parser;
use num_bigint::BigInt;
use salsa::Database;
#[derive(Parser, Debug)]
#[command(version, verbatim_doc_comment)]
struct Args {
input_path: PathBuf,
#[arg(long, requires = "output_path")]
build_only: bool,
#[arg(long, required_unless_present_any(["standalone", "run_profiler"]))]
output_path: Option<PathBuf>,
#[arg(long, default_value_t = false, conflicts_with = "build_only")]
prebuilt: bool,
#[arg(short, long, default_value_t, value_enum, conflicts_with = "prebuilt")]
run_profiler: RunProfilerConfigArg,
#[command(flatten)]
build: BuildArgs,
#[command(flatten)]
run: RunArgs,
#[arg(long, default_value_t = false)]
print_bytecode_size: bool,
}
#[derive(Parser, Debug)]
struct BuildArgs {
#[arg(long, conflicts_with = "prebuilt")]
single_file: bool,
#[arg(long, conflicts_with = "prebuilt")]
allow_warnings: bool,
#[arg(long, conflicts_with = "prebuilt")]
ignore_warnings: bool,
#[arg(long, conflicts_with = "prebuilt")]
allow_syscalls: bool,
#[arg(long, conflicts_with = "prebuilt")]
unsafe_panic: bool,
#[arg(long, conflicts_with = "prebuilt")]
executable: Option<String>,
}
#[derive(Parser, Debug)]
struct RunArgs {
#[command(flatten)]
args: SerializedArgs,
#[arg(long, default_value_t = false, conflicts_with = "build_only")]
print_outputs: bool,
#[arg(long, default_value = "all_cairo_stwo", value_enum, conflicts_with = "build_only")]
layout: LayoutName,
#[arg(long, required_if_eq("layout", "dynamic"))]
cairo_layout_params_file: Option<PathBuf>,
#[arg(
long,
default_value_t = false,
conflicts_with_all = ["build_only", "output_path"],
requires_ifs = [("standalone", "air_public_input"), ("standalone", "air_private_input")],
)]
standalone: bool,
#[arg(long, conflicts_with = "build_only")]
secure_run: Option<bool>,
#[arg(long, conflicts_with = "build_only")]
allow_missing_builtins: Option<bool>,
#[arg(long, conflicts_with = "build_only")]
disable_trace_padding: Option<bool>,
#[command(flatten)]
proof_outputs: ProofOutputArgs,
}
#[derive(Parser, Debug)]
#[command(group = clap::ArgGroup::new("serialized-args").multiple(false).conflicts_with("build_only"))]
struct SerializedArgs {
#[arg(
long = "args",
group = "serialized-args",
value_delimiter = ',',
conflicts_with = "build_only"
)]
as_list: Vec<BigInt>,
#[arg(long = "args-file", group = "serialized-args", conflicts_with = "build_only")]
as_file: Option<PathBuf>,
}
#[derive(Parser, Debug)]
struct ProofOutputArgs {
#[arg(long, conflicts_with = "build_only")]
trace_file: Option<PathBuf>,
#[arg(long, conflicts_with = "build_only")]
memory_file: Option<PathBuf>,
#[arg(long, conflicts_with = "build_only")]
air_public_input: Option<PathBuf>,
#[arg(
long,
conflicts_with = "build_only",
requires_ifs = [("standalone", "trace_file"), ("standalone", "memory_file")]
)]
air_private_input: Option<PathBuf>,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let mut diagnostics_reporter = DiagnosticsReporter::stderr();
let (db, opt_debug_data, executable) = {
if args.prebuilt {
let executable: Executable =
serde_json::from_reader(std::fs::File::open(&args.input_path)?)
.with_context(|| "Failed reading prebuilt executable.")?;
(None, None, Some(executable))
} else {
check_compiler_path(args.build.single_file, &args.input_path)?;
if args.build.allow_warnings {
diagnostics_reporter = diagnostics_reporter.allow_warnings();
}
if args.build.ignore_warnings {
diagnostics_reporter = diagnostics_reporter.ignore_all_warnings();
}
let config = ExecutableConfig {
allow_syscalls: args.build.allow_syscalls,
unsafe_panic: args.build.unsafe_panic,
builtin_list: None,
};
let mut db = prepare_db(&config)?;
let main_crate_inputs = setup_project(&mut db, Path::new(&args.input_path))?;
(Some(db), Some((main_crate_inputs, config)), None)
}
};
let (opt_debug_data, executable) = if let Some((main_crate_inputs, config)) = opt_debug_data {
let db = db.as_ref().expect("db was just created in compilation path");
let main_crate_ids = CrateInput::into_crate_ids(db, main_crate_inputs);
let CompileExecutableResult { compiled_function, builder, debug_info } =
compile_executable_in_prepared_db(
db,
args.build.executable.as_deref(),
main_crate_ids,
diagnostics_reporter,
config,
)?;
let wrapper_len =
compiled_function.wrapper.header.iter().map(|insn| insn.body.op_size()).sum::<usize>();
let header_len = NOT_RETURNING_HEADER_SIZE + wrapper_len;
let executable = Executable::new(compiled_function);
(Some(DebugData { db, builder, debug_info, header_len }), executable)
} else {
(None, executable.unwrap())
};
if args.print_bytecode_size {
println!("Bytecode size (felt252 count): {}", executable.program.bytecode.len());
};
if args.build_only {
let path = args.output_path.with_context(|| "No output path provided.")?;
serde_json::to_writer(std::fs::File::create(path)?, &executable)
.with_context(|| "Failed writing executable.")?;
return Ok(());
}
let entry_point_kind =
if args.run.standalone { EntryPointKind::Standalone } else { EntryPointKind::Bootloader };
let entrypoint = executable
.entrypoints
.iter()
.find(|e| e.kind == entry_point_kind)
.with_context(|| format!("{entry_point_kind:?} entrypoint not found"))?;
let (program, string_to_hint) = program_and_hints_from_executable(&executable, entrypoint)?;
let user_args = user_args_from_flags(args.run.args.as_file.as_ref(), &args.run.args.as_list)?;
let mut hint_processor = CairoHintProcessor {
runner: None,
user_args: vec![vec![Arg::Array(user_args)]],
string_to_hint,
starknet_state: Default::default(),
run_resources: Default::default(),
syscalls_used_resources: Default::default(),
no_temporary_segments: false,
markers: Default::default(),
panic_traceback: Default::default(),
};
let dynamic_layout_params = match args.run.cairo_layout_params_file {
Some(file) => Some(CairoLayoutParams::from_file(&file)?),
None => None,
};
let trace_enabled = args.run_profiler != RunProfilerConfigArg::None
|| args.run.proof_outputs.trace_file.is_some();
let cairo_run_config = CairoRunConfig {
trace_enabled,
relocate_mem: args.run.proof_outputs.memory_file.is_some(),
relocate_trace: trace_enabled,
layout: args.run.layout,
dynamic_layout_params,
proof_mode: args.run.standalone,
fill_holes: args.run.standalone,
secure_run: args.run.secure_run,
allow_missing_builtins: args.run.allow_missing_builtins,
disable_trace_padding: args.run.disable_trace_padding.unwrap_or_default(),
..Default::default()
};
let mut runner = match cairo_run_program(&program, &cairo_run_config, &mut hint_processor) {
Ok(runner) => runner,
Err(err) => {
if let Some(panic_data) = hint_processor.markers.last() {
return Err(err).with_context(|| format_for_panic(panic_data.iter().copied()));
};
if let Some(err_location) = try_get_error_location(&opt_debug_data, &err) {
return Err(err).context(err_location);
}
return Err(err).context("Failed running program.");
}
};
if args.run.print_outputs {
let mut output_buffer = "Program Output:\n".to_string();
runner.vm.write_output(&mut output_buffer)?;
print!("{output_buffer}");
}
if let Some(trace_path) = &args.run.proof_outputs.trace_file {
let relocated_trace =
runner.relocated_trace.as_ref().with_context(|| "Trace not relocated.")?;
write_relocated_trace(trace_path, relocated_trace)?;
}
if let Some(memory_path) = &args.run.proof_outputs.memory_file {
write_relocated_memory(memory_path, &runner.relocated_memory)?;
}
if let Some(file_path) = args.run.proof_outputs.air_public_input {
let json = runner.get_air_public_input()?.serialize_json()?;
std::fs::write(file_path, json)?;
}
if let (Some(file_path), Some(trace_file), Some(memory_file)) = (
args.run.proof_outputs.air_private_input,
args.run.proof_outputs.trace_file,
args.run.proof_outputs.memory_file,
) {
let absolute = |path_buf: PathBuf| {
path_buf.as_path().canonicalize().unwrap_or(path_buf).to_string_lossy().to_string()
};
let json = runner
.get_air_private_input()
.to_serializable(absolute(trace_file), absolute(memory_file))
.serialize_json()
.with_context(|| "Failed serializing private input")?;
std::fs::write(file_path, json)?;
}
if let Some(file_name) = args.output_path {
runner
.get_cairo_pie()
.with_context(|| "Failed getting cairo pie")?
.write_zip_file(&file_name, true)?
}
if let Ok(profiler_config) = &args.run_profiler.try_into() {
let DebugData { db, builder, debug_info, header_len } =
opt_debug_data.expect("debug data should be available when profiling");
let trace = runner.relocated_trace.as_ref().with_context(|| "Trace not relocated.")?;
let first_pc = trace.first().unwrap().pc;
assert_eq!(first_pc - entrypoint.offset, 1);
let load_offset = 1 + header_len;
let info = ProfilingInfo::from_trace(
&builder,
load_offset,
&ProfilingInfoCollectionConfig::from_profiler_config(profiler_config),
trace,
);
let processed_profiling_info = ProfilingInfoProcessor::new(
Some(db),
&replace_sierra_ids_in_program(db, builder.sierra_program()),
debug_info.statements_locations.get_statements_functions_map_for_tests(db),
)
.process(&info, &ProfilingInfoProcessorParams::from_profiler_config(profiler_config));
print!("{processed_profiling_info}");
}
Ok(())
}
fn try_get_error_location(
opt_debug_data: &Option<DebugData<'_>>,
err: &CairoRunError,
) -> Option<String> {
let DebugData { db, builder, debug_info, header_len } = opt_debug_data.as_ref()?;
let CairoRunError::VmException(err) = err else { return None };
if err.pc.segment_index != 0 {
return None;
}
let pc = err.pc.offset.checked_sub(*header_len)?;
let stmt_idx = builder.casm_program().sierra_statement_index_by_pc(pc);
let loc = debug_info.statements_locations.statement_diagnostic_location(*db, stmt_idx)?;
Some(format!("#{stmt_idx} {:?}", loc.debug(*db)))
}
struct DebugData<'a> {
db: &'a dyn Database,
builder: RunnableBuilder,
debug_info: SierraProgramDebugInfo<'a>,
header_len: usize,
}
fn write_relocated_trace(
path: &PathBuf,
relocated_trace: &[RelocatedTraceEntry],
) -> Result<(), io::Error> {
let mut writer = io::BufWriter::with_capacity(3 * 1024 * 1024, std::fs::File::create(path)?);
for entry in relocated_trace {
write_usize_as_u64(&mut writer, entry.ap)?;
write_usize_as_u64(&mut writer, entry.fp)?;
write_usize_as_u64(&mut writer, entry.pc)?;
}
writer.flush()
}
fn write_relocated_memory(
path: &PathBuf,
relocated_memory: &[Option<cairo_vm::Felt252>],
) -> Result<(), io::Error> {
let mut writer = io::BufWriter::with_capacity(5 * 1024 * 1024, std::fs::File::create(path)?);
for (i, memory_cell) in relocated_memory.iter().enumerate() {
match memory_cell {
None => {}
Some(unwrapped_memory_cell) => {
write_usize_as_u64(&mut writer, i)?;
writer.write_all(&unwrapped_memory_cell.to_bytes_le())?;
}
}
}
writer.flush()
}
fn write_usize_as_u64(
writer: &mut io::BufWriter<std::fs::File>,
value: usize,
) -> Result<(), io::Error> {
writer.write_all(&(value as u64).to_le_bytes())
}