Skip to content

Commit

Permalink
chore(bind-json): replace solang with solar (#9616)
Browse files Browse the repository at this point in the history
  • Loading branch information
yash-atreya authored Jan 5, 2025
1 parent 2e9d849 commit a5c5be5
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 74 deletions.
160 changes: 86 additions & 74 deletions crates/forge/bin/cmd/bind_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ use foundry_compilers::{
};
use foundry_config::Config;
use itertools::Itertools;
use rayon::prelude::*;
use solang_parser::pt as solang_ast;
use solar_ast::{
ast::{self, Arena, FunctionKind, Span, VarMut},
interface::source_map::FileName,
visit::Visit,
};
use solar_parse::{interface::Session, Parser as SolarParser};
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
fmt::Write,
fmt::{self, Write},
path::PathBuf,
sync::Arc,
};
Expand Down Expand Up @@ -85,85 +88,94 @@ impl BindJsonArgs {
.unwrap()
.1;

// Insert empty bindings file
let sess = Session::builder().with_stderr_emitter().build();
let result = sess.enter(|| -> solar_parse::interface::Result<()> {
// TODO: Switch back to par_iter_mut and `enter_parallel` after solar update.
sources.0.iter_mut().try_for_each(|(path, source)| {
let mut content = Arc::try_unwrap(std::mem::take(&mut source.content)).unwrap();

let arena = Arena::new();
let mut parser = SolarParser::from_source_code(
&sess,
&arena,
FileName::Real(path.clone()),
content.to_string(),
)?;
let ast = parser.parse_file().map_err(|e| e.emit())?;

let mut visitor = PreprocessorVisitor::new();
visitor.visit_source_unit(&ast);
visitor.update(&sess, &mut content);

source.content = Arc::new(content);
Ok(())
})
});
eyre::ensure!(result.is_ok(), "failed parsing");

// Insert empty bindings file.
sources.insert(target_path.clone(), Source::new("library JsonBindings {}"));

let sources = Sources(
sources
.0
.into_par_iter()
.map(|(path, source)| {
let mut locs_to_update = Vec::new();
let mut content = Arc::unwrap_or_clone(source.content);
let (parsed, _) = solang_parser::parse(&content, 0)
.map_err(|errors| eyre::eyre!("Parser failed: {errors:?}"))?;

// All function definitions in the file
let mut functions = Vec::new();

for part in &parsed.0 {
if let solang_ast::SourceUnitPart::FunctionDefinition(def) = part {
functions.push(def);
}
if let solang_ast::SourceUnitPart::ContractDefinition(contract) = part {
for part in &contract.parts {
match part {
solang_ast::ContractPart::FunctionDefinition(def) => {
functions.push(def);
}
// Remove `immutable` attributes
solang_ast::ContractPart::VariableDefinition(def) => {
for attr in &def.attrs {
if let solang_ast::VariableAttribute::Immutable(loc) =
attr
{
locs_to_update.push((
loc.start(),
loc.end(),
String::new(),
));
}
}
}
_ => {}
}
}
};
}
Ok(PreprocessedState { sources, target_path, project, config })
}
}

for def in functions {
// If there's no body block, keep the function as is
let Some(solang_ast::Statement::Block { loc, .. }) = def.body else {
continue;
};
let new_body = match def.ty {
solang_ast::FunctionTy::Modifier => "{ _; }",
_ => "{ revert(); }",
};
let start = loc.start();
let end = loc.end();
locs_to_update.push((start, end + 1, new_body.to_string()));
}
struct PreprocessorVisitor {
updates: Vec<(Span, &'static str)>,
}

impl PreprocessorVisitor {
fn new() -> Self {
Self { updates: Vec::new() }
}

locs_to_update.sort_by_key(|(start, _, _)| *start);
fn update(mut self, sess: &Session, content: &mut String) {
if self.updates.is_empty() {
return;
}

let mut shift = 0_i64;
let sf = sess.source_map().lookup_source_file(self.updates[0].0.lo());
let base = sf.start_pos.0;

for (start, end, new) in locs_to_update {
let start = ((start as i64) - shift) as usize;
let end = ((end as i64) - shift) as usize;
self.updates.sort_by_key(|(span, _)| span.lo());
let mut shift = 0_i64;
for (span, new) in self.updates {
let lo = span.lo() - base;
let hi = span.hi() - base;
let start = ((lo.0 as i64) - shift) as usize;
let end = ((hi.0 as i64) - shift) as usize;

content.replace_range(start..end, new.as_str());
shift += (end - start) as i64;
shift -= new.len() as i64;
}
content.replace_range(start..end, new);
shift += (end - start) as i64;
shift -= new.len() as i64;
}
}
}

Ok((path, Source::new(content)))
})
.collect::<Result<BTreeMap<_, _>>>()?,
);
impl<'ast> Visit<'ast> for PreprocessorVisitor {
fn visit_item_function(&mut self, func: &'ast ast::ItemFunction<'ast>) {
// Replace function bodies with a noop statement.
if let Some(block) = &func.body {
if !block.is_empty() {
let span = block.first().unwrap().span.to(block.last().unwrap().span);
let new_body = match func.kind {
FunctionKind::Modifier => "_;",
_ => "revert();",
};
self.updates.push((span, new_body));
}
}

Ok(PreprocessedState { sources, target_path, project, config })
self.walk_item_function(func)
}

fn visit_variable_definition(&mut self, var: &'ast ast::VariableDefinition<'ast>) {
// Remove `immutable` attributes.
if let Some(VarMut::Immutable) = var.mutability {
self.updates.push((var.span, ""));
}

self.walk_variable_definition(var)
}
}

Expand Down
71 changes: 71 additions & 0 deletions crates/forge/tests/cli/bind_json.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use foundry_test_utils::snapbox;

// tests complete bind-json workflow
// ensures that we can run forge-bind even if files are depending on yet non-existent bindings and
// that generated bindings are correct
Expand Down Expand Up @@ -50,5 +52,74 @@ contract BindJsonTest is Test {
.unwrap();

cmd.arg("bind-json").assert_success();

snapbox::assert_data_eq!(
snapbox::Data::read_from(&prj.root().join("utils/JsonBindings.sol"), None),
snapbox::str![[r#"
// Automatically generated by forge bind-json.
pragma solidity >=0.6.2 <0.9.0;
pragma experimental ABIEncoderV2;
import {BindJsonTest, TopLevelStruct} from "test/JsonBindings.sol";
interface Vm {
function parseJsonTypeArray(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory);
function parseJsonType(string calldata json, string calldata typeDescription) external pure returns (bytes memory);
function parseJsonType(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory);
function serializeJsonType(string calldata typeDescription, bytes memory value) external pure returns (string memory json);
function serializeJsonType(string calldata objectKey, string calldata valueKey, string calldata typeDescription, bytes memory value) external returns (string memory json);
}
library JsonBindings {
Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
string constant schema_TopLevelStruct = "TopLevelStruct(uint256 param1,int8 param2)";
string constant schema_ContractLevelStruct = "ContractLevelStruct(address[][] param1,address addrParam)";
function serialize(TopLevelStruct memory value) internal pure returns (string memory) {
return vm.serializeJsonType(schema_TopLevelStruct, abi.encode(value));
}
function serialize(TopLevelStruct memory value, string memory objectKey, string memory valueKey) internal returns (string memory) {
return vm.serializeJsonType(objectKey, valueKey, schema_TopLevelStruct, abi.encode(value));
}
function deserializeTopLevelStruct(string memory json) public pure returns (TopLevelStruct memory) {
return abi.decode(vm.parseJsonType(json, schema_TopLevelStruct), (TopLevelStruct));
}
function deserializeTopLevelStruct(string memory json, string memory path) public pure returns (TopLevelStruct memory) {
return abi.decode(vm.parseJsonType(json, path, schema_TopLevelStruct), (TopLevelStruct));
}
function deserializeTopLevelStructArray(string memory json, string memory path) public pure returns (TopLevelStruct[] memory) {
return abi.decode(vm.parseJsonTypeArray(json, path, schema_TopLevelStruct), (TopLevelStruct[]));
}
function serialize(BindJsonTest.ContractLevelStruct memory value) internal pure returns (string memory) {
return vm.serializeJsonType(schema_ContractLevelStruct, abi.encode(value));
}
function serialize(BindJsonTest.ContractLevelStruct memory value, string memory objectKey, string memory valueKey) internal returns (string memory) {
return vm.serializeJsonType(objectKey, valueKey, schema_ContractLevelStruct, abi.encode(value));
}
function deserializeContractLevelStruct(string memory json) public pure returns (BindJsonTest.ContractLevelStruct memory) {
return abi.decode(vm.parseJsonType(json, schema_ContractLevelStruct), (BindJsonTest.ContractLevelStruct));
}
function deserializeContractLevelStruct(string memory json, string memory path) public pure returns (BindJsonTest.ContractLevelStruct memory) {
return abi.decode(vm.parseJsonType(json, path, schema_ContractLevelStruct), (BindJsonTest.ContractLevelStruct));
}
function deserializeContractLevelStructArray(string memory json, string memory path) public pure returns (BindJsonTest.ContractLevelStruct[] memory) {
return abi.decode(vm.parseJsonTypeArray(json, path, schema_ContractLevelStruct), (BindJsonTest.ContractLevelStruct[]));
}
}
"#]],
);

cmd.forge_fuse().args(["test"]).assert_success();
});

0 comments on commit a5c5be5

Please sign in to comment.