1use indexmap::IndexMap;
2use serde::{
3 Deserialize, Serialize,
4 de::{self, Visitor},
5};
6use serde_json::{Map, Value, json};
7use solar_codegen::{EvmCodegen, lower};
8use solar_config::{
9 CompileOpts, CompilerStage, EvmVersion, ImportRemapping, Language, OptimizationMode,
10};
11use solar_data_structures::map::{FxBuildHasher, FxHashMap, FxHashSet};
12use solar_interface::{
13 Result, SourceMap,
14 diagnostics::{DiagCtxt, InMemoryEmitter, JsonEmitter, SolcDiagnostic},
15 source_map::FileLoader,
16};
17use solar_sema::hir::ContractId;
18use std::{
19 borrow::{Borrow, Cow},
20 collections::BTreeMap,
21 fmt,
22 fs::File,
23 io::{self, Read, Write},
24 ops::Deref,
25 path::{Path, PathBuf},
26 str::FromStr,
27 sync::Arc,
28};
29
30type FxIndexMap<K, V> = IndexMap<K, V, FxBuildHasher>;
31
32#[derive(Debug, Deserialize)]
33#[serde(rename_all = "camelCase")]
34struct CompilerInput<'a> {
35 #[serde(default = "default_language")]
36 language: CowStr<'a>,
37 #[serde(default)]
38 #[serde(borrow)]
39 sources: FxIndexMap<CowStr<'a>, SourceInput<'a>>,
40 #[serde(default)]
41 #[serde(borrow)]
42 settings: Settings<'a>,
43}
44
45#[derive(Debug, Deserialize)]
46struct SourceInput<'a> {
47 #[serde(borrow)]
48 content: Option<CowStr<'a>>,
49 #[serde(default)]
50 #[serde(borrow)]
51 urls: Vec<CowStr<'a>>,
52}
53
54#[derive(Debug, Default, Deserialize)]
61#[serde(rename_all = "camelCase")]
62struct Settings<'a> {
63 #[serde(default)]
64 #[serde(borrow)]
65 remappings: Vec<CowStr<'a>>,
66 #[serde(default)]
67 #[serde(borrow)]
68 output_selection: OutputSelection<'a>,
69 #[serde(borrow)]
70 stop_after: Option<CowStr<'a>>,
71 #[serde(borrow)]
72 evm_version: Option<CowStr<'a>>,
73 #[serde(default)]
75 optimizer: Option<Optimizer>,
76 #[serde(default)]
78 metadata: Option<Value>,
79 #[serde(default)]
81 libraries: Option<Value>,
82 #[serde(default, rename = "viaIR")]
85 via_ir: Option<bool>,
86}
87
88#[derive(Debug, Default, Deserialize)]
90#[serde(rename_all = "camelCase")]
91struct Optimizer {
92 #[serde(default)]
95 enabled: bool,
96 #[serde(default)]
98 runs: Option<u64>,
99 #[serde(default)]
101 details: Option<Value>,
102}
103
104#[derive(Debug, Default, Deserialize)]
105struct OutputSelection<'a>(
106 #[serde(borrow)] FxIndexMap<CowStr<'a>, FxIndexMap<CowStr<'a>, Vec<CowStr<'a>>>>,
107);
108
109#[derive(Debug, Default, Serialize)]
110struct CompilerOutput<'a> {
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 errors: Vec<SolcDiagnostic<'a>>,
113 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
114 sources: BTreeMap<String, SourceOutput>,
115 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
116 contracts: BTreeMap<String, BTreeMap<String, ContractOutput>>,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
121pub enum ReadCallbackResult {
122 Success(String),
124 Error(String),
126 Unsupported,
128}
129
130pub trait StandardJsonReadCallback: Send + Sync + 'static {
132 fn read(&self, kind: &str, data: &str) -> ReadCallbackResult;
136}
137
138pub fn compile_standard_json(
140 input: &str,
141 mut opts: CompileOpts,
142 read_callback: Option<Arc<dyn StandardJsonReadCallback>>,
143 out: &mut dyn Write,
144) {
145 let source_map = Arc::new(SourceMap::empty());
146 source_map.set_file_loader(StandardJsonFileLoader { read_callback });
147 let (emitter, diagnostics) = InMemoryEmitter::new();
148 let dcx = DiagCtxt::new(Box::new(emitter))
149 .with_flags(|flags| flags.update_from_opts(&opts))
150 .with_allowed_diagnostic_codes(opts.allow.iter().cloned());
151
152 let mut output = CompilerOutput::default();
153 let input = if opts.unstable.ui_testing {
154 Cow::Owned(strip_json_comments(input))
155 } else {
156 Cow::Borrowed(input)
157 };
158 match serde_json::from_str::<CompilerInput<'_>>(&input) {
159 Ok(compiler_input) => {
160 if opts.unstable.standard_json_stats {
161 print_standard_json_stats(&input, &compiler_input);
162 }
163 compile(compiler_input, &mut opts, Arc::clone(&source_map), dcx, &mut output);
164 }
165 Err(e) => {
166 dcx.err(format!("JSON parse error: {e}")).emit();
167 }
168 }
169
170 let mut emitter = JsonEmitter::new(Box::new(io::sink()), Arc::clone(&source_map))
171 .ui_testing(opts.unstable.ui_testing)
172 .human_kind(opts.error_format_human)
173 .terminal_width(opts.diagnostic_width);
174 let diagnostics = diagnostics.read();
175 output.errors =
176 diagnostics.iter().map(|diagnostic| emitter.solc_diagnostic(diagnostic)).collect();
177
178 if output.errors.iter().any(SolcDiagnostic::is_error) {
179 output.contracts.clear();
180 }
181
182 let result = if opts.pretty_json {
183 serde_json::to_writer_pretty(out, &output)
184 } else {
185 serde_json::to_writer(out, &output)
186 };
187 let _ = result;
188}
189
190pub(crate) fn run(opts: CompileOpts) -> io::Result<()> {
191 let stdout = io::stdout();
192 let mut stdout = io::BufWriter::new(stdout.lock());
193 let mut input = String::new();
194 let result = match opts.input.as_slice() {
195 [] => io::stdin().read_to_string(&mut input),
196 [arg] if arg == "-" => io::stdin().read_to_string(&mut input),
197 [path] => File::open(path).and_then(|mut file| file.read_to_string(&mut input)),
198 _ => unreachable!("standard JSON input count is validated during argument parsing"),
199 };
200 match result {
201 Ok(_) => compile_standard_json(&input, opts, None, &mut stdout),
202 Err(e) => standard_json_error_output(
203 format!("failed to read standard JSON input: {e}"),
204 &mut stdout,
205 )?,
206 }
207 stdout.write_all(b"\n")?;
208 stdout.flush()
209}
210
211fn standard_json_error_output(message: String, out: &mut dyn Write) -> io::Result<()> {
212 let output = json!({
213 "errors": [{
214 "severity": "error",
215 "type": "IOError",
216 "message": message,
217 }],
218 });
219 serde_json::to_writer(out, &output).map_err(io::Error::other)
220}
221
222fn compile(
223 input: CompilerInput<'_>,
224 opts: &mut CompileOpts,
225 source_map: Arc<SourceMap>,
226 dcx: DiagCtxt,
227 output: &mut CompilerOutput<'_>,
228) {
229 let CompilerInput { language, sources, settings } = input;
230 let Settings {
235 remappings,
236 output_selection,
237 stop_after,
238 evm_version,
239 optimizer,
240 metadata: _metadata,
241 libraries: _libraries,
242 via_ir: _via_ir,
243 } = settings;
244
245 let mut parsed_remappings = Vec::with_capacity(remappings.len());
246 for remapping in &remappings {
247 match remapping.parse::<ImportRemapping>() {
248 Ok(remapping) => parsed_remappings.push(remapping),
249 Err(e) => {
250 dcx.err(format!("invalid remapping `{remapping}`: {e}")).emit();
251 }
252 }
253 }
254 if dcx.has_errors().is_err() {
255 return;
256 }
257
258 opts.import_remappings = parsed_remappings;
259 opts.evm_version = evm_version
260 .as_deref()
261 .and_then(|version| EvmVersion::from_str(version).ok())
262 .unwrap_or(opts.evm_version);
263 opts.language = match language.as_ref() {
264 "Solidity" | "solidity" => Language::Solidity,
265 "Yul" | "yul" => Language::Yul,
266 language => {
267 dcx.err(format!("unsupported language `{language}`")).emit();
268 return;
269 }
270 };
271 opts.stop_after = stop_after.as_deref().and_then(|stage| CompilerStage::from_str(stage).ok());
272
273 if let Some(Optimizer { enabled: false, runs: _runs, details: _details }) = optimizer {
278 opts.optimization = OptimizationMode::None;
279 }
280
281 opts.input = sources.keys().map(ToString::to_string).collect();
282
283 let sess = solar_interface::Session::builder()
284 .source_map(Arc::clone(&source_map))
285 .dcx(dcx)
286 .opts(opts.clone())
287 .build();
288
289 let _ = crate::commands::compile::run_compiler_session_with(
290 sess,
291 |compiler| {
292 let compile_result = crate::commands::compile::run_pipeline(
293 compiler,
294 |pcx| {
295 let mut files = Vec::with_capacity(sources.len());
296 for (name, source) in sources {
297 let Some(content) = source.content else {
298 let message = if source.urls.is_empty() {
299 format!("source `{name}` is missing `content`")
300 } else {
301 format!("source URLs are not supported for `{name}`")
302 };
303 return Err(pcx.dcx().err(message).emit());
304 };
305 files.push((PathBuf::from(name.as_ref()), content));
306 }
307 pcx.par_load_files_with_contents(files)
308 },
309 |compiler| output.sources = source_outputs_from_compiler(compiler),
310 );
311
312 if compile_result.is_ok() && compiler.dcx().has_errors().is_ok() {
313 let gcx = compiler.gcx();
314 let bytecodes = if gcx.sess.opts.unstable.codegen
317 && needs_bytecode_output(gcx, &output_selection)
318 {
319 Some(generate_contract_bytecodes(gcx)?)
320 } else {
321 None
322 };
323
324 for (contract_id, contract) in gcx.hir.contracts_enumerated() {
325 let source = gcx.hir.source(contract.source);
326 let source_name = standard_json_source_name(&source.file.name);
327 let contract_name = contract.name.to_string();
328 let contract_output = make_contract_output(
329 gcx,
330 contract_id,
331 &output_selection,
332 &source_name,
333 &contract_name,
334 bytecodes.as_ref(),
335 );
336 if !contract_output.is_empty() {
337 output
338 .contracts
339 .entry(source_name)
340 .or_default()
341 .insert(contract_name, contract_output);
342 }
343 }
344 }
345
346 compile_result.map(|_| ())
347 },
348 false,
349 );
350}
351
352#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
364struct CowStr<'a>(Cow<'a, str>);
365
366impl CowStr<'_> {
367 fn as_cow(&self) -> &Cow<'_, str> {
368 &self.0
369 }
370}
371
372impl AsRef<str> for CowStr<'_> {
373 fn as_ref(&self) -> &str {
374 &self.0
375 }
376}
377
378impl Deref for CowStr<'_> {
379 type Target = str;
380
381 fn deref(&self) -> &Self::Target {
382 &self.0
383 }
384}
385
386impl Borrow<str> for CowStr<'_> {
387 fn borrow(&self) -> &str {
388 &self.0
389 }
390}
391
392impl fmt::Display for CowStr<'_> {
393 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394 f.write_str(&self.0)
395 }
396}
397
398impl From<CowStr<'_>> for String {
399 fn from(value: CowStr<'_>) -> Self {
400 value.0.into_owned()
401 }
402}
403
404impl<'de: 'a, 'a> Deserialize<'de> for CowStr<'a> {
405 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
406 where
407 D: serde::Deserializer<'de>,
408 {
409 struct CowStrVisitor;
410
411 impl<'de> Visitor<'de> for CowStrVisitor {
412 type Value = CowStr<'de>;
413
414 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
415 formatter.write_str("a JSON string")
416 }
417
418 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
419 where
420 E: de::Error,
421 {
422 Ok(CowStr(Cow::Borrowed(value)))
423 }
424
425 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
426 where
427 E: de::Error,
428 {
429 Ok(CowStr(Cow::Owned(value.to_string())))
430 }
431
432 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
433 where
434 E: de::Error,
435 {
436 Ok(CowStr(Cow::Owned(value)))
437 }
438 }
439
440 deserializer.deserialize_str(CowStrVisitor)
441 }
442}
443
444#[derive(Debug, Serialize)]
445struct SourceOutput {
446 id: u32,
447}
448
449#[derive(Debug, Default, Serialize)]
450#[serde(rename_all = "camelCase")]
451struct ContractOutput {
452 #[serde(skip_serializing_if = "Option::is_none")]
453 abi: Option<Value>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 metadata: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 userdoc: Option<Value>,
458 #[serde(skip_serializing_if = "Option::is_none")]
459 devdoc: Option<Value>,
460 #[serde(skip_serializing_if = "Option::is_none")]
461 storage_layout: Option<Value>,
462 #[serde(skip_serializing_if = "Option::is_none")]
463 evm: Option<EvmOutput>,
464}
465
466#[derive(Debug, Default, Serialize)]
467#[serde(rename_all = "camelCase")]
468struct EvmOutput {
469 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
470 method_identifiers: BTreeMap<String, String>,
471 #[serde(skip_serializing_if = "Option::is_none")]
472 bytecode: Option<BytecodeOutput>,
473 #[serde(skip_serializing_if = "Option::is_none")]
474 deployed_bytecode: Option<BytecodeOutput>,
475}
476
477#[derive(Debug, Default, Serialize)]
478#[serde(rename_all = "camelCase")]
479struct BytecodeOutput {
480 object: String,
481 #[serde(default, skip_serializing_if = "String::is_empty")]
482 opcodes: String,
483 #[serde(default, skip_serializing_if = "String::is_empty")]
484 source_map: String,
485 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
486 link_references: BTreeMap<String, Value>,
487 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
488 immutable_references: BTreeMap<String, Value>,
489}
490
491struct GeneratedBytecodes {
492 deployment: String,
493 runtime: String,
494}
495
496impl BytecodeOutput {
497 fn empty() -> Self {
498 Self::default()
499 }
500
501 fn new(object: String) -> Self {
502 Self { object, ..Self::default() }
503 }
504}
505
506impl OutputSelection<'_> {
507 fn selects(&self, source: &str, contract: &str, keys: &[&str]) -> bool {
508 self.source_maps(source).any(|contracts| {
509 contract_maps(contracts, contract).any(|items| {
510 items.iter().any(|item| {
511 item.as_ref() == "*"
512 || keys.iter().any(|key| {
513 item.as_ref() == *key
514 || key
515 .strip_prefix(item.as_ref())
516 .is_some_and(|rest| rest.starts_with('.'))
517 })
518 })
519 })
520 })
521 }
522
523 fn source_maps(
524 &self,
525 source: &str,
526 ) -> impl Iterator<Item = &FxIndexMap<CowStr<'_>, Vec<CowStr<'_>>>> {
527 [source, "*"].into_iter().filter_map(|source| self.0.get(source))
528 }
529}
530
531fn contract_maps<'a, 'b>(
532 contracts: &'a FxIndexMap<CowStr<'b>, Vec<CowStr<'b>>>,
533 contract: &'a str,
534) -> impl Iterator<Item = &'a Vec<CowStr<'b>>> {
535 [contract, "*"].into_iter().filter_map(|contract| contracts.get(contract))
536}
537
538fn default_language<'a>() -> CowStr<'a> {
539 CowStr(Cow::Borrowed("Solidity"))
540}
541
542struct StandardJsonFileLoader {
543 read_callback: Option<Arc<dyn StandardJsonReadCallback>>,
544}
545
546impl FileLoader for StandardJsonFileLoader {
547 fn canonicalize_path(&self, path: &Path) -> io::Result<PathBuf> {
548 if path.is_absolute()
549 && let Ok(cwd) = std::env::current_dir()
550 && let Ok(path) = path.strip_prefix(cwd)
551 {
552 Ok(path.to_path_buf())
553 } else {
554 Ok(path.to_path_buf())
555 }
556 }
557
558 fn load_stdin(&self) -> io::Result<String> {
559 self.read_source(Path::new("stdin"))
560 }
561
562 fn load_file(&self, path: &Path) -> io::Result<String> {
563 self.read_source(path)
564 }
565
566 fn load_binary_file(&self, path: &Path) -> io::Result<Vec<u8>> {
567 Err(disallowed_io(path))
568 }
569}
570
571impl StandardJsonFileLoader {
572 fn read_source(&self, path: &Path) -> io::Result<String> {
573 let Some(read_callback) = &self.read_callback else {
574 return Err(io::Error::new(
575 io::ErrorKind::NotFound,
576 "File import callback not supported",
577 ));
578 };
579 let data = callback_path(path);
580 match read_callback.read("source", &data) {
581 ReadCallbackResult::Success(contents) => Ok(contents),
582 ReadCallbackResult::Error(error) => Err(io::Error::other(error)),
583 ReadCallbackResult::Unsupported => {
584 Err(io::Error::new(io::ErrorKind::Unsupported, unsupported_callback_kind("source")))
585 }
586 }
587 }
588}
589
590pub(crate) fn unsupported_callback_kind(kind: &str) -> String {
592 format!("Callback kind `{kind}` is not supported")
593}
594
595fn callback_path(path: &Path) -> Cow<'_, str> {
596 let path = if path.is_absolute()
597 && let Ok(cwd) = std::env::current_dir()
598 && let Ok(path) = path.strip_prefix(cwd)
599 {
600 path
601 } else {
602 path
603 };
604 path.to_string_lossy()
605}
606
607fn standard_json_source_name(name: &solar_interface::source_map::FileName) -> String {
608 name.display().to_string().replace('\\', "/")
609}
610
611fn disallowed_io(path: &Path) -> io::Error {
612 io::Error::new(
613 io::ErrorKind::PermissionDenied,
614 format!("standard JSON mode cannot read `{}` from the filesystem", path.display()),
615 )
616}
617
618#[derive(Default)]
619struct JsonTreeStats {
620 nodes: usize,
621 objects: usize,
622 arrays: usize,
623 strings: usize,
624 numbers: usize,
625 bools: usize,
626 nulls: usize,
627 object_entries: usize,
628 array_elements: usize,
629 object_key_bytes: usize,
630 string_bytes: usize,
631}
632
633impl JsonTreeStats {
634 fn add_value(&mut self, value: &Value) {
635 self.nodes += 1;
636 match value {
637 Value::Null => self.nulls += 1,
638 Value::Bool(_) => self.bools += 1,
639 Value::Number(_) => self.numbers += 1,
640 Value::String(value) => {
641 self.strings += 1;
642 self.string_bytes += value.len();
643 }
644 Value::Array(values) => {
645 self.arrays += 1;
646 self.array_elements += values.len();
647 for value in values {
648 self.add_value(value);
649 }
650 }
651 Value::Object(values) => self.add_object(values),
652 }
653 }
654
655 fn add_object(&mut self, values: &Map<String, Value>) {
656 self.objects += 1;
657 self.object_entries += values.len();
658 for (key, value) in values {
659 self.object_key_bytes += key.len();
660 self.add_value(value);
661 }
662 }
663}
664
665#[derive(Default)]
666struct InputCowStats {
667 borrowed: usize,
668 borrowed_bytes: usize,
669 owned: usize,
670 owned_bytes: usize,
671}
672
673impl InputCowStats {
674 fn add(&mut self, value: &CowStr<'_>) {
675 match value.as_cow() {
676 Cow::Borrowed(value) => {
677 self.borrowed += 1;
678 self.borrowed_bytes += value.len();
679 }
680 Cow::Owned(value) => {
681 self.owned += 1;
682 self.owned_bytes += value.len();
683 }
684 }
685 }
686}
687
688fn print_standard_json_stats(raw_input: &str, input: &CompilerInput<'_>) {
689 let mut tree = JsonTreeStats::default();
690 match serde_json::from_str::<Value>(raw_input) {
691 Ok(value) => tree.add_value(&value),
692 Err(error) => {
693 eprintln!("standard-json-stats: failed to parse JSON tree: {error}");
694 return;
695 }
696 }
697
698 let mut cows = InputCowStats::default();
699 count_input_cows(input, &mut cows);
700
701 let source_content_count =
702 input.sources.values().filter(|source| source.content.is_some()).count();
703 let source_content_bytes = input
704 .sources
705 .values()
706 .filter_map(|source| source.content.as_ref())
707 .map(|content| content.len())
708 .sum::<usize>();
709 let source_url_count = input.sources.values().map(|source| source.urls.len()).sum::<usize>();
710
711 eprintln!(
712 "standard-json-stats: input_bytes={} nodes={} objects={} arrays={} strings={} numbers={} bools={} nulls={} object_entries={} array_elements={} object_key_bytes={} string_bytes={}",
713 raw_input.len(),
714 tree.nodes,
715 tree.objects,
716 tree.arrays,
717 tree.strings,
718 tree.numbers,
719 tree.bools,
720 tree.nulls,
721 tree.object_entries,
722 tree.array_elements,
723 tree.object_key_bytes,
724 tree.string_bytes,
725 );
726 eprintln!(
727 "standard-json-stats: sources={} source_content_count={} source_content_bytes={} source_url_count={} remappings={} output_selection_sources={}",
728 input.sources.len(),
729 source_content_count,
730 source_content_bytes,
731 source_url_count,
732 input.settings.remappings.len(),
733 input.settings.output_selection.0.len(),
734 );
735 eprintln!(
736 "standard-json-stats: cow_borrowed={} cow_borrowed_bytes={} cow_owned={} cow_owned_bytes={}",
737 cows.borrowed, cows.borrowed_bytes, cows.owned, cows.owned_bytes,
738 );
739}
740
741fn count_input_cows(input: &CompilerInput<'_>, stats: &mut InputCowStats) {
742 stats.add(&input.language);
743 for (name, source) in &input.sources {
744 stats.add(name);
745 if let Some(content) = &source.content {
746 stats.add(content);
747 }
748 for url in &source.urls {
749 stats.add(url);
750 }
751 }
752 for remapping in &input.settings.remappings {
753 stats.add(remapping);
754 }
755 if let Some(stop_after) = &input.settings.stop_after {
756 stats.add(stop_after);
757 }
758 if let Some(evm_version) = &input.settings.evm_version {
759 stats.add(evm_version);
760 }
761 for (source, contracts) in &input.settings.output_selection.0 {
762 stats.add(source);
763 for (contract, items) in contracts {
764 stats.add(contract);
765 for item in items {
766 stats.add(item);
767 }
768 }
769 }
770}
771
772fn source_outputs_from_compiler(
773 compiler: &solar_sema::CompilerRef<'_>,
774) -> BTreeMap<String, SourceOutput> {
775 compiler
776 .gcx()
777 .sources
778 .iter_enumerated()
779 .map(|(id, source)| {
780 (standard_json_source_name(&source.file.name), SourceOutput { id: id.index() as u32 })
781 })
782 .collect()
783}
784
785fn make_contract_output(
786 gcx: solar_sema::Gcx<'_>,
787 contract_id: solar_sema::hir::ContractId,
788 output_selection: &OutputSelection<'_>,
789 source_name: &str,
790 contract_name: &str,
791 bytecodes: Option<&FxHashMap<ContractId, GeneratedBytecodes>>,
792) -> ContractOutput {
793 let mut output = ContractOutput::default();
794
795 if output_selection.selects(source_name, contract_name, &["abi"]) {
796 output.abi = Some(serde_json::to_value(gcx.contract_abi(contract_id)).unwrap());
797 }
798 if output_selection.selects(source_name, contract_name, &["userdoc"]) {
799 output.userdoc = Some(json!({ "kind": "user", "methods": {}, "version": 1 }));
800 }
801 if output_selection.selects(source_name, contract_name, &["devdoc"]) {
802 output.devdoc = Some(json!({ "kind": "dev", "methods": {}, "version": 1 }));
803 }
804 if output_selection.selects(source_name, contract_name, &["storageLayout"]) {
805 output.storage_layout = Some(json!({ "storage": [], "types": {} }));
806 }
807
808 let mut evm = EvmOutput::default();
809 if output_selection.selects(source_name, contract_name, &["evm.methodIdentifiers"]) {
810 for function in gcx.interface_functions(contract_id) {
811 evm.method_identifiers.insert(
812 gcx.item_signature(function.id.into()).to_string(),
813 alloy_primitives::hex::encode(function.selector),
814 );
815 }
816 }
817 if output_selection.selects(
826 source_name,
827 contract_name,
828 &["evm.bytecode", "evm.bytecode.object"],
829 ) {
830 evm.bytecode = Some(
831 bytecodes
832 .and_then(|bytecodes| bytecodes.get(&contract_id))
833 .map(|bytecodes| BytecodeOutput::new(bytecodes.deployment.clone()))
834 .unwrap_or_else(BytecodeOutput::empty),
835 );
836 }
837 if output_selection.selects(
838 source_name,
839 contract_name,
840 &["evm.deployedBytecode", "evm.deployedBytecode.object"],
841 ) {
842 evm.deployed_bytecode = Some(
843 bytecodes
844 .and_then(|bytecodes| bytecodes.get(&contract_id))
845 .map(|bytecodes| BytecodeOutput::new(bytecodes.runtime.clone()))
846 .unwrap_or_else(BytecodeOutput::empty),
847 );
848 }
849 if !evm.is_empty() {
850 output.evm = Some(evm);
851 }
852
853 output
854}
855
856fn needs_bytecode_output(gcx: solar_sema::Gcx<'_>, output_selection: &OutputSelection<'_>) -> bool {
857 gcx.hir.contracts_enumerated().any(|(_, contract)| {
858 let source = gcx.hir.source(contract.source);
859 let source_name = source.file.name.display().to_string();
860 let contract_name = contract.name.to_string();
861 output_selection.selects(
862 &source_name,
863 &contract_name,
864 &[
865 "evm.bytecode",
866 "evm.bytecode.object",
867 "evm.deployedBytecode",
868 "evm.deployedBytecode.object",
869 ],
870 )
871 })
872}
873
874fn generate_contract_bytecodes(
875 gcx: solar_sema::Gcx<'_>,
876) -> Result<FxHashMap<ContractId, GeneratedBytecodes>> {
877 let mut all_bytecodes = FxHashMap::default();
878 let mut visiting = FxHashSet::default();
879 for contract_id in gcx.hir.contract_ids() {
880 let contract = gcx.hir.contract(contract_id);
881 if !contract.kind.is_interface() && !contract.kind.is_abstract_contract() {
882 ensure_contract_bytecode(gcx, contract_id, &mut all_bytecodes, &mut visiting)?;
883 }
884 }
885
886 let mut bytecodes = FxHashMap::default();
887 for contract_id in gcx.hir.contract_ids() {
888 let contract = gcx.hir.contract(contract_id);
889 if !contract.kind.is_interface() && !contract.kind.is_abstract_contract() {
890 let mut module = lower::lower_contract_with_bytecodes(gcx, contract_id, &all_bytecodes);
891 gcx.dcx().has_errors()?;
892 let mut codegen = EvmCodegen::new(gcx);
893 let (deployment, runtime) = codegen.generate_deployment_bytecode(&mut module);
894 bytecodes.insert(
895 contract_id,
896 GeneratedBytecodes {
897 deployment: alloy_primitives::hex::encode(deployment),
898 runtime: alloy_primitives::hex::encode(runtime),
899 },
900 );
901 }
902 }
903
904 Ok(bytecodes)
905}
906
907fn ensure_contract_bytecode(
908 gcx: solar_sema::Gcx<'_>,
909 contract_id: ContractId,
910 all_bytecodes: &mut FxHashMap<ContractId, Vec<u8>>,
911 visiting: &mut FxHashSet<ContractId>,
912) -> Result {
913 let contract = gcx.hir.contract(contract_id);
914
915 if all_bytecodes.contains_key(&contract_id) {
916 return Ok(());
917 }
918
919 if contract.kind.is_interface() || contract.kind.is_abstract_contract() {
920 return Err(gcx
921 .dcx()
922 .err("cannot generate creation bytecode for non-deployable contract")
923 .span(contract.span)
924 .emit());
925 }
926
927 if !visiting.insert(contract_id) {
928 return Err(gcx
929 .dcx()
930 .err("recursive contract creation bytecode dependency")
931 .span(contract.span)
932 .emit());
933 }
934
935 for dep in lower::contract_bytecode_dependencies(gcx, contract_id) {
936 ensure_contract_bytecode(gcx, dep, all_bytecodes, visiting)?;
937 }
938
939 let mut module = lower::lower_contract_with_bytecodes(gcx, contract_id, all_bytecodes);
940 gcx.dcx().has_errors()?;
941 let mut codegen = EvmCodegen::new(gcx);
942 let (deployment, _) = codegen.generate_deployment_bytecode(&mut module);
943 all_bytecodes.insert(contract_id, deployment);
944 visiting.remove(&contract_id);
945
946 Ok(())
947}
948
949impl ContractOutput {
950 fn is_empty(&self) -> bool {
951 self.abi.is_none()
952 && self.metadata.is_none()
953 && self.userdoc.is_none()
954 && self.devdoc.is_none()
955 && self.storage_layout.is_none()
956 && self.evm.is_none()
957 }
958}
959
960impl EvmOutput {
961 fn is_empty(&self) -> bool {
962 self.method_identifiers.is_empty()
963 && self.bytecode.is_none()
964 && self.deployed_bytecode.is_none()
965 }
966}
967
968fn strip_json_comments(input: &str) -> String {
969 let mut out = String::with_capacity(input.len());
970 let mut chars = input.chars().peekable();
971 let mut in_string = false;
972 let mut escaped = false;
973
974 while let Some(ch) = chars.next() {
975 if in_string {
976 out.push(ch);
977 if escaped {
978 escaped = false;
979 } else if ch == '\\' {
980 escaped = true;
981 } else if ch == '"' {
982 in_string = false;
983 }
984 continue;
985 }
986
987 match ch {
988 '"' => {
989 in_string = true;
990 out.push(ch);
991 }
992 '/' if chars.peek() == Some(&'/') => {
993 chars.next();
994 for ch in chars.by_ref() {
995 if ch == '\n' {
996 out.push('\n');
997 break;
998 }
999 }
1000 }
1001 '/' if chars.peek() == Some(&'*') => {
1002 chars.next();
1003 let mut prev = '\0';
1004 for ch in chars.by_ref() {
1005 if ch == '\n' {
1006 out.push('\n');
1007 }
1008 if prev == '*' && ch == '/' {
1009 break;
1010 }
1011 prev = ch;
1012 }
1013 }
1014 _ => out.push(ch),
1015 }
1016 }
1017
1018 out
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024 use snapbox::{IntoData as _, assert_data_eq, str};
1025 use std::collections::BTreeMap;
1026
1027 struct Sources(BTreeMap<String, String>);
1028
1029 impl StandardJsonReadCallback for Sources {
1030 fn read(&self, kind: &str, data: &str) -> ReadCallbackResult {
1031 if kind != "source" {
1032 return ReadCallbackResult::Unsupported;
1033 }
1034 self.0.get(data).cloned().map_or_else(
1035 || ReadCallbackResult::Error(format!("source `{data}` not found")),
1036 ReadCallbackResult::Success,
1037 )
1038 }
1039 }
1040
1041 fn compile(input: &str, callback: Option<Arc<dyn StandardJsonReadCallback>>) -> String {
1042 compile_with(input, callback, false)
1043 }
1044
1045 fn compile_with_typeck(
1046 input: &str,
1047 callback: Option<Arc<dyn StandardJsonReadCallback>>,
1048 ) -> String {
1049 compile_with(input, callback, true)
1050 }
1051
1052 fn compile_with(
1053 input: &str,
1054 callback: Option<Arc<dyn StandardJsonReadCallback>>,
1055 typeck: bool,
1056 ) -> String {
1057 let mut output = Vec::new();
1058 let opts = CompileOpts { unstable: test_unstable_opts(typeck), ..test_opts() };
1059 compile_standard_json(input, opts, callback, &mut output);
1060 normalize_manifest_dir(String::from_utf8(output).unwrap())
1061 }
1062
1063 fn assert_json(actual: &str, expected: impl snapbox::IntoData) {
1064 let actual = normalize_manifest_dir(actual.to_owned());
1065 assert_data_eq!(actual.into_data().is_json(), expected.into_data().is_json());
1066 }
1067
1068 fn test_opts() -> CompileOpts {
1069 CompileOpts { pretty_json: true, ..CompileOpts::default() }
1070 }
1071
1072 fn test_unstable_opts(typeck: bool) -> solar_config::UnstableOpts {
1073 solar_config::UnstableOpts { ui_testing: true, typeck, ..Default::default() }
1074 }
1075
1076 fn normalize_manifest_dir(mut output: String) -> String {
1077 output = output.replace("\\\\", "/");
1078 output = output.replace("\\/", "/");
1079 let native = env!("CARGO_MANIFEST_DIR");
1080 let slash = native.replace('\\', "/");
1081 let stripped = slash.strip_prefix("//?/").unwrap_or(&slash).to_string();
1082 let mut prefixes = vec![native.to_string(), slash, stripped.clone()];
1083 if let Some((drive, rest)) = stripped.split_once(':') {
1084 prefixes.push(format!("{}:{rest}", drive.to_ascii_uppercase()));
1085 prefixes.push(format!("{}:{rest}", drive.to_ascii_lowercase()));
1086 }
1087 prefixes.dedup();
1088 for prefix in prefixes {
1089 output = output.replace(&prefix, "ROOT");
1090 }
1091 while let Some(end) = output.find("/crates/cli") {
1092 let end = end + "/crates/cli".len();
1093 let start = output[..end].rfind('"').map_or(0, |i| i + 1);
1094 output.replace_range(start..end, "ROOT");
1095 }
1096 output
1097 }
1098
1099 #[test]
1100 fn normalize_manifest_dir_rewrites_windows_paths() {
1101 assert_eq!(
1102 normalize_manifest_dir(r#"{"D:/a/solar/solar/crates/cli/B.sol":{}}"#.to_string()),
1103 r#"{"ROOT/B.sol":{}}"#,
1104 );
1105 assert_eq!(
1106 normalize_manifest_dir(r#"{"D:\\a\\solar\\solar\\crates\\cli\\B.sol":{}}"#.to_string()),
1107 r#"{"ROOT/B.sol":{}}"#,
1108 );
1109 }
1110
1111 #[test]
1112 fn compile_without_imports() {
1113 assert_json(
1114 &compile(
1115 r#"{
1116 "language": "Solidity",
1117 "sources": {
1118 "A.sol": {
1119 "content": "contract A { function f() public pure returns (uint) { return 1; } }"
1120 }
1121 },
1122 "settings": {
1123 "outputSelection": { "*": { "*": ["abi"] } }
1124 }
1125 }"#,
1126 None,
1127 ),
1128 str![[r#"
1129{
1130 "sources": {
1131 "A.sol": {
1132 "id": 0
1133 }
1134 },
1135 "contracts": {
1136 "A.sol": {
1137 "A": {
1138 "abi": [
1139 {
1140 "inputs": [],
1141 "name": "f",
1142 "outputs": [
1143 {
1144 "internalType": "uint256",
1145 "name": "",
1146 "type": "uint256"
1147 }
1148 ],
1149 "stateMutability": "pure",
1150 "type": "function"
1151 }
1152 ]
1153 }
1154 }
1155 }
1156}
1157"#]],
1158 );
1159 }
1160
1161 #[test]
1162 fn type_errors_are_reported() {
1163 assert_json(
1164 &compile_with_typeck(
1165 r#"{
1166 "language": "Solidity",
1167 "sources": {
1168 "A.sol": {
1169 "content": "contract A { function f() public pure { uint x = true; } }"
1170 }
1171 },
1172 "settings": {
1173 "outputSelection": { "*": { "*": ["abi"] } }
1174 }
1175 }"#,
1176 None,
1177 ),
1178 str![[r#"
1179{
1180 "errors": [
1181 {
1182 "component": "general",
1183 "errorCode": null,
1184 "formattedMessage": "error: mismatched types\n ╭▸ A.sol:1:50\n │\nLL │ contract A { function f() public pure { uint x = true; } }\n ╰╴ ━━━━ expected `uint256`, found `bool`\n\n",
1185 "message": "mismatched types",
1186 "secondarySourceLocations": [],
1187 "severity": "error",
1188 "sourceLocation": {
1189 "end": 53,
1190 "file": "A.sol",
1191 "start": 49
1192 },
1193 "type": "Exception"
1194 }
1195 ],
1196 "sources": {
1197 "A.sol": {
1198 "id": 0
1199 }
1200 }
1201}
1202"#]],
1203 );
1204 }
1205
1206 #[test]
1207 fn import_callback_resolves_source() {
1208 let mut sources = BTreeMap::new();
1209 sources.insert(
1210 "B.sol".to_string(),
1211 "contract B { function g() public pure returns (uint) { return 2; } }".to_string(),
1212 );
1213
1214 assert_json(
1215 &compile(
1216 r#"{
1217 "language": "Solidity",
1218 "sources": {
1219 "A.sol": {
1220 "content": "import \"B.sol\"; contract A is B {}"
1221 }
1222 },
1223 "settings": {
1224 "outputSelection": { "*": { "*": ["abi"] } }
1225 }
1226 }"#,
1227 Some(Arc::new(Sources(sources))),
1228 ),
1229 str![[r#"
1230{
1231 "contracts": {
1232 "A.sol": {
1233 "A": {
1234 "abi": [
1235 {
1236 "inputs": [],
1237 "name": "g",
1238 "outputs": [
1239 {
1240 "internalType": "uint256",
1241 "name": "",
1242 "type": "uint256"
1243 }
1244 ],
1245 "stateMutability": "pure",
1246 "type": "function"
1247 }
1248 ]
1249 }
1250 },
1251 "ROOT/B.sol": {
1252 "B": {
1253 "abi": [
1254 {
1255 "inputs": [],
1256 "name": "g",
1257 "outputs": [
1258 {
1259 "internalType": "uint256",
1260 "name": "",
1261 "type": "uint256"
1262 }
1263 ],
1264 "stateMutability": "pure",
1265 "type": "function"
1266 }
1267 ]
1268 }
1269 }
1270 },
1271 "sources": {
1272 "A.sol": {
1273 "id": 1
1274 },
1275 "ROOT/B.sol": {
1276 "id": 0
1277 }
1278 }
1279}
1280"#]],
1281 );
1282 }
1283
1284 #[test]
1285 fn missing_import_callback_is_reported() {
1286 assert_json(
1287 &compile(
1288 r#"{
1289 "language": "Solidity",
1290 "sources": {
1291 "A.sol": {
1292 "content": "import \"Missing.sol\"; contract A {}"
1293 }
1294 }
1295 }"#,
1296 None,
1297 ),
1298 str![[r#"
1299{
1300 "errors": [
1301 {
1302 "component": "general",
1303 "errorCode": null,
1304 "formattedMessage": "error: couldn't read Missing.sol: File import callback not supported\n ╭▸ A.sol:1:8\n │\nLL │ import \"Missing.sol\"; contract A {}\n ╰╴ ━━━━━━━━━━━━━\n\n",
1305 "message": "couldn't read Missing.sol: File import callback not supported",
1306 "secondarySourceLocations": [],
1307 "severity": "error",
1308 "sourceLocation": {
1309 "end": 20,
1310 "file": "A.sol",
1311 "start": 7
1312 },
1313 "type": "Exception"
1314 }
1315 ],
1316 "sources": {
1317 "A.sol": {
1318 "id": 0
1319 }
1320 }
1321}
1322"#]],
1323 );
1324 }
1325}