From 12f6a9e3caed7d69eb2b7c2228581c3d5d600e68 Mon Sep 17 00:00:00 2001 From: glihm Date: Wed, 21 Aug 2024 22:35:31 -0600 Subject: [PATCH] feat: add sierra to cairo debug information --- crates/dojo-lang/src/compiler.rs | 110 ++++++++++--- crates/dojo-lang/src/scarb_internal/debug.rs | 155 +++++++++++++++++++ crates/dojo-lang/src/scarb_internal/mod.rs | 1 + 3 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 crates/dojo-lang/src/scarb_internal/debug.rs diff --git a/crates/dojo-lang/src/compiler.rs b/crates/dojo-lang/src/compiler.rs index 30bdc18c74..142ef4726f 100644 --- a/crates/dojo-lang/src/compiler.rs +++ b/crates/dojo-lang/src/compiler.rs @@ -1,7 +1,6 @@ use std::collections::{BTreeMap, HashMap}; use std::fs; use std::io::Write; -use std::iter::zip; use std::ops::DerefMut; use anyhow::{anyhow, Context, Result}; @@ -28,7 +27,7 @@ use dojo_world::manifest::{ BASE_CONTRACT_TAG, BASE_DIR, BASE_QUALIFIED_PATH, CONTRACTS_DIR, MANIFESTS_DIR, MODELS_DIR, WORLD_CONTRACT_TAG, WORLD_QUALIFIED_PATH, }; -use itertools::Itertools; +use itertools::{izip, Itertools}; use scarb::compiler::helpers::{build_compiler_config, collect_main_crate_ids}; use scarb::compiler::{CairoCompilationUnit, CompilationUnitAttributes, Compiler}; use scarb::core::{PackageName, TargetKind, Workspace}; @@ -41,6 +40,7 @@ use starknet::core::types::Felt; use tracing::{debug, trace, trace_span}; use crate::plugin::{DojoAuxData, Model}; +use crate::scarb_internal::debug::SierraToCairoDebugInfo; const CAIRO_PATH_SEPARATOR: &str = "::"; @@ -88,6 +88,7 @@ impl Compiler for DojoCompiler { TargetKind::new("dojo") } + // TODO: refacto the main loop here, could be much more simpler and efficient. fn compile( &self, unit: CairoCompilationUnit, @@ -129,10 +130,27 @@ impl Compiler for DojoCompiler { compile_prepared_db(db, &contracts, compiler_config)? }; - let mut compiled_classes: HashMap = HashMap::new(); + // TODO: get the debug flag from the `dojo_.toml` file. + let with_debug_info = true; + let debug_info_classes: Vec> = if with_debug_info { + let debug_classes = + crate::scarb_internal::debug::compile_prepared_db_to_debug_info(db, &contracts)?; + + debug_classes + .into_iter() + .map(|d| Some(crate::scarb_internal::debug::get_sierra_to_cairo_debug_info(&d, db))) + .collect() + } else { + vec![None; contracts.len()] + }; + + let mut compiled_classes: HashMap< + String, + (Felt, ContractClass, Option), + > = HashMap::new(); let list_selector = ListSelector::default(); - for (decl, class) in zip(contracts, classes) { + for (decl, class, debug_info) in izip!(contracts, classes, debug_info_classes) { let contract_name = decl.submodule_id.name(db.upcast_mut()); // note that the qualified path is in snake case while @@ -164,7 +182,7 @@ impl Compiler for DojoCompiler { format!("problem computing class hash for contract `{}`", qualified_path.clone()) })?; - compiled_classes.insert(qualified_path, (class_hash, class)); + compiled_classes.insert(qualified_path, (class_hash, class, debug_info)); } update_files( @@ -256,7 +274,7 @@ fn update_files( ws: &Workspace<'_>, target_dir: &Filesystem, crate_ids: &[CrateId], - compiled_artifacts: HashMap, + compiled_artifacts: HashMap)>, external_contracts: Option>, ) -> anyhow::Result<()> { let profile_name = @@ -270,9 +288,9 @@ fn update_files( let manifest_dir = ws.manifest_path().parent().unwrap().to_path_buf(); fn get_compiled_artifact_from_map<'a>( - artifacts: &'a HashMap, + artifacts: &'a HashMap)>, qualified_artifact_path: &str, - ) -> anyhow::Result<&'a (Felt, ContractClass)> { + ) -> anyhow::Result<&'a (Felt, ContractClass, Option)> { artifacts.get(qualified_artifact_path).context(format!( "Contract `{qualified_artifact_path}` not found. Did you include `dojo` as a \ dependency?", @@ -285,7 +303,8 @@ fn update_files( for (qualified_path, tag) in [(WORLD_QUALIFIED_PATH, WORLD_CONTRACT_TAG), (BASE_QUALIFIED_PATH, BASE_CONTRACT_TAG)] { - let (hash, class) = get_compiled_artifact_from_map(&compiled_artifacts, qualified_path)?; + let (hash, class, debug_info) = + get_compiled_artifact_from_map(&compiled_artifacts, qualified_path)?; let filename = naming::get_filename_from_tag(tag); write_manifest_and_abi( &base_manifests_dir, @@ -304,6 +323,10 @@ fn update_files( &class.abi, )?; save_json_artifact_file(ws, target_dir, class, &filename, tag)?; + + if let Some(debug_info) = debug_info { + save_json_artifact_debug_file(ws, target_dir, debug_info, &filename, tag)?; + } } let mut models = BTreeMap::new(); @@ -380,7 +403,7 @@ fn update_files( std::fs::create_dir_all(&base_contracts_abis_dir)?; } - for (_, (manifest, class, module_id)) in contracts.iter_mut() { + for (_, (manifest, class, module_id, debug_info)) in contracts.iter_mut() { write_manifest_and_abi( &base_contracts_dir, &base_contracts_abis_dir, @@ -399,6 +422,16 @@ fn update_files( &manifest.inner.tag, )?; save_json_artifact_file(ws, &contracts_dir, class, &filename, &manifest.inner.tag)?; + + if let Some(debug_info) = debug_info { + save_json_artifact_debug_file( + ws, + &contracts_dir, + debug_info, + &filename, + &manifest.inner.tag, + )?; + } } let models_dir = target_dir.child(MODELS_DIR); @@ -417,7 +450,7 @@ fn update_files( std::fs::create_dir_all(&base_models_abis_dir)?; } - for (_, (manifest, class, module_id)) in models.iter_mut() { + for (_, (manifest, class, module_id, debug_info)) in models.iter_mut() { write_manifest_and_abi( &base_models_dir, &base_models_abis_dir, @@ -429,6 +462,16 @@ fn update_files( let filename = naming::get_filename_from_tag(&manifest.inner.tag); save_expanded_source_file(ws, *module_id, db, &models_dir, &filename, &manifest.inner.tag)?; save_json_artifact_file(ws, &models_dir, class, &filename, &manifest.inner.tag)?; + + if let Some(debug_info) = debug_info { + save_json_artifact_debug_file( + ws, + &models_dir, + debug_info, + &filename, + &manifest.inner.tag, + )?; + } } Ok(()) @@ -441,8 +484,10 @@ fn get_dojo_model_artifacts( db: &RootDatabase, aux_data: &Vec, module_id: ModuleId, - compiled_classes: &HashMap, -) -> anyhow::Result, ContractClass, ModuleId)>> { + compiled_classes: &HashMap)>, +) -> anyhow::Result< + HashMap, ContractClass, ModuleId, Option)>, +> { let mut models = HashMap::with_capacity(aux_data.len()); for model in aux_data { @@ -456,7 +501,7 @@ fn get_dojo_model_artifacts( let compiled_class = compiled_classes.get(&qualified_path).cloned(); let tag = naming::get_tag(&model.namespace, &model.name); - if let Some((class_hash, class)) = compiled_class { + if let Some((class_hash, class, debug_info)) = compiled_class { models.insert( qualified_path.clone(), ( @@ -473,6 +518,7 @@ fn get_dojo_model_artifacts( ), class, module_id, + debug_info.clone(), ), ); } else { @@ -489,9 +535,14 @@ fn get_dojo_contract_artifacts( db: &RootDatabase, module_id: &ModuleId, tag: &str, - compiled_classes: &HashMap, + compiled_classes: &HashMap)>, systems: &[String], -) -> anyhow::Result, ContractClass, ModuleId)>> { +) -> anyhow::Result< + HashMap< + String, + (Manifest, ContractClass, ModuleId, Option), + >, +> { let mut result = HashMap::new(); if !matches!(naming::get_name_from_tag(tag).as_str(), "world" | "resource_metadata" | "base") { @@ -501,7 +552,7 @@ fn get_dojo_contract_artifacts( let contract_qualified_path = format!("{}{}{}", module_id.full_path(db), CAIRO_PATH_SEPARATOR, contract_name); - if let Some((class_hash, class)) = + if let Some((class_hash, class, debug_info)) = compiled_classes.get(&contract_qualified_path.to_string()) { let manifest = Manifest::new( @@ -517,8 +568,10 @@ fn get_dojo_contract_artifacts( naming::get_filename_from_tag(tag), ); - result - .insert(contract_qualified_path.to_string(), (manifest, class.clone(), *module_id)); + result.insert( + contract_qualified_path.to_string(), + (manifest, class.clone(), *module_id, debug_info.clone()), + ); } } @@ -631,3 +684,22 @@ fn save_json_artifact_file( .with_context(|| format!("failed to serialize contract artifact: {contract_tag}"))?; Ok(()) } + +fn save_json_artifact_debug_file( + ws: &Workspace<'_>, + contract_dir: &Filesystem, + debug_info: &SierraToCairoDebugInfo, + contract_basename: &str, + contract_tag: &str, +) -> anyhow::Result<()> { + let mut file = contract_dir.open_rw( + format!("{contract_basename}.debug.json"), + "class file", + ws.config(), + )?; + + serde_json::to_writer_pretty(file.deref_mut(), debug_info) + .with_context(|| format!("failed to serialize contract debug artifact: {contract_tag}"))?; + + Ok(()) +} diff --git a/crates/dojo-lang/src/scarb_internal/debug.rs b/crates/dojo-lang/src/scarb_internal/debug.rs new file mode 100644 index 0000000000..50b9ba7e0a --- /dev/null +++ b/crates/dojo-lang/src/scarb_internal/debug.rs @@ -0,0 +1,155 @@ +use std::collections::HashMap; +use std::env; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use cairo_lang_compiler::db::RootDatabase; +use cairo_lang_diagnostics::ToOption; +use cairo_lang_filesystem::db::{get_originating_location, FilesGroup}; +use cairo_lang_filesystem::ids::{FileId, FileLongId}; +use cairo_lang_filesystem::span::TextSpan; +use cairo_lang_sierra_generator::db::SierraGenGroup; +use cairo_lang_sierra_generator::program_generator::{ + SierraProgramDebugInfo, SierraProgramWithDebug, +}; +use cairo_lang_starknet::compile::{extract_semantic_entrypoints, SemanticEntryPoints}; +use cairo_lang_starknet::contract::ContractDeclaration; +use itertools::{chain, Itertools}; +use serde::Serialize; + +pub fn compile_prepared_db_to_debug_info( + db: &RootDatabase, + contracts: &[&ContractDeclaration], + // mut compiler_config: CompilerConfig<'_>, +) -> Result> { + // compiler_config.diagnostics_reporter.ensure(db)?; + + contracts + .iter() + .map(|contract| compile_contract_with_prepared_and_checked_db_to_debug_info(db, contract)) + .try_collect() +} + +/// Compile declared Starknet contract. +/// +/// The `contract` value **must** come from `db`, for example as a result of calling +/// [`find_contracts`]. Does not check diagnostics, it is expected that they are checked by caller +/// of this function. +fn compile_contract_with_prepared_and_checked_db_to_debug_info( + db: &RootDatabase, + contract: &ContractDeclaration, +) -> Result { + let SemanticEntryPoints { external, l1_handler, constructor } = + extract_semantic_entrypoints(db, contract)?; + let SierraProgramWithDebug { program: _sierra_program, debug_info } = Arc::unwrap_or_clone( + db.get_sierra_program_for_functions( + chain!(&external, &l1_handler, &constructor).map(|f| f.value).collect(), + ) + .to_option() + .with_context(|| "Compilation failed without any diagnostics.")?, + ); + + Ok(debug_info) +} + +#[derive(Debug, Clone, Serialize)] +pub struct SierraToCairoDebugInfo { + pub sierra_statements_to_cairo_info: HashMap, +} + +/// Human readable position inside a file, in lines and characters. +#[derive(Debug, Serialize, Clone)] +pub struct TextPosition { + /// Line index, 0 based. + pub line: usize, + /// Character index inside the line, 0 based. + pub col: usize, +} + +#[derive(Debug, Serialize, Clone)] +pub struct Location { + pub start: TextPosition, + pub end: TextPosition, + pub file_path: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct SierraStatementToCairoDebugInfo { + pub cairo_locations: Vec, +} + +/// Returns a map from Sierra statement indexes to Cairo function names. +pub fn get_sierra_to_cairo_debug_info( + sierra_program_debug_info: &SierraProgramDebugInfo, + compiler_db: &RootDatabase, +) -> SierraToCairoDebugInfo { + let mut sierra_statements_to_cairo_info: HashMap = + HashMap::new(); + + for (statement_idx, locations) in + sierra_program_debug_info.statements_locations.locations.iter_sorted() + { + let mut cairo_locations: Vec = Vec::new(); + for location in locations { + let syntax_node = location.syntax_node(compiler_db); + let file_id = syntax_node.stable_ptr().file_id(compiler_db); + let _file_name = file_id.file_name(compiler_db); + let syntax_node_location_span = syntax_node.span_without_trivia(compiler_db); + + let (originating_file_id, originating_text_span) = + get_originating_location(compiler_db, file_id, syntax_node_location_span); + let cairo_location = get_location_from_text_span( + originating_text_span, + originating_file_id, + compiler_db, + ); + if let Some(cl) = cairo_location { + cairo_locations.push(cl); + } + } + sierra_statements_to_cairo_info + .insert(statement_idx.0, SierraStatementToCairoDebugInfo { cairo_locations }); + } + + SierraToCairoDebugInfo { sierra_statements_to_cairo_info } +} + +pub fn get_location_from_text_span( + text_span: TextSpan, + file_id: FileId, + compiler_db: &RootDatabase, +) -> Option { + let current_dir = env::current_dir().expect("Failed to get current directory"); + // dbg!(¤t_dir); + // let file_path = match compiler_db.lookup_intern_file(file_id) { + // FileLongId::OnDisk(path) => { + // path.strip_prefix(current_dir).expect("Failed to get relative + // path").to_path_buf().to_str().unwrap_or("").to_string() }, + // FileLongId::Virtual(_) => file_id.full_path(compiler_db), + // }; + let file_path = match compiler_db.lookup_intern_file(file_id) { + FileLongId::OnDisk(path) => match path.strip_prefix(¤t_dir) { + Ok(relative_path) => relative_path.to_str().unwrap_or("").to_string(), + Err(_) => { + return None; + } + }, + FileLongId::Virtual(_) => { + return None; + } + }; + + // let file_path = file_id.full_path(compiler_db); + + let start: Option = text_span + .start + .position_in_file(compiler_db, file_id) + .map(|s| TextPosition { line: s.line, col: s.col }); + + let end = text_span + .end + .position_in_file(compiler_db, file_id) + .map(|e| TextPosition { line: e.line, col: e.col }); + + start.zip(end).map(|(start, end)| Location { start, end, file_path }) +} diff --git a/crates/dojo-lang/src/scarb_internal/mod.rs b/crates/dojo-lang/src/scarb_internal/mod.rs index aa6110d742..15ba53e467 100644 --- a/crates/dojo-lang/src/scarb_internal/mod.rs +++ b/crates/dojo-lang/src/scarb_internal/mod.rs @@ -32,6 +32,7 @@ use tracing::trace; use crate::plugin::dojo_plugin_suite; pub(crate) const LOG_TARGET: &str = "dojo_lang::scarb_internal"; +pub mod debug; /// Compilation information of all the units found in the workspace. #[derive(Debug, Default)]