Skip to main content

solar_cli/
standard_json.rs

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/// A subset of the solc Standard JSON `settings` object.
55///
56/// Every field here is handled explicitly in [`compile`] — fields we don't act
57/// on yet are still parsed and bound (with a note) rather than silently dropped,
58/// so the set of recognized keys stays visible and intentional. Unknown keys are
59/// ignored by serde, matching solc.
60#[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    /// Optimizer settings. Only `enabled` is currently honored.
74    #[serde(default)]
75    optimizer: Option<Optimizer>,
76    /// Output metadata settings; bytecode metadata is not emitted yet.
77    #[serde(default)]
78    metadata: Option<Value>,
79    /// Library addresses for linking; linking is not supported yet.
80    #[serde(default)]
81    libraries: Option<Value>,
82    /// Whether to compile via the Yul IR pipeline. We have a single pipeline, so
83    /// there is nothing to switch.
84    #[serde(default, rename = "viaIR")]
85    via_ir: Option<bool>,
86}
87
88/// The solc Standard JSON `settings.optimizer` object.
89#[derive(Debug, Default, Deserialize)]
90#[serde(rename_all = "camelCase")]
91struct Optimizer {
92    /// Whether the optimizer is enabled. Mapped onto [`OptimizationMode::None`]
93    /// when disabled.
94    #[serde(default)]
95    enabled: bool,
96    /// Number of optimizer runs. The MIR optimizer has no runs parameter yet.
97    #[serde(default)]
98    runs: Option<u64>,
99    /// Fine-grained optimizer toggles. Not used yet.
100    #[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/// Result returned by a Standard JSON read callback.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub enum ReadCallbackResult {
122    /// The requested data was found.
123    Success(String),
124    /// The callback handled the request and returned an error.
125    Error(String),
126    /// The callback does not support this request kind.
127    Unsupported,
128}
129
130/// Callback used by Standard JSON compilation to retrieve extra input.
131pub trait StandardJsonReadCallback: Send + Sync + 'static {
132    /// Reads data for `kind`.
133    ///
134    /// The modern soljson API currently uses `source` for import resolution.
135    fn read(&self, kind: &str, data: &str) -> ReadCallbackResult;
136}
137
138/// Compiles Standard JSON input and returns Standard JSON output.
139pub 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    // Destructure `Settings` so every recognized field is handled explicitly;
231    // fields we don't act on yet are bound with a leading underscore and a note.
232    // Adding a field to `Settings` then forces a decision here instead of it
233    // being silently ignored.
234    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    // Map the solc optimizer toggle onto our MIR optimization objective. We only
274    // override when the input explicitly disables the optimizer, leaving the
275    // CLI-driven default otherwise. `runs` and `details` have no analogue in the
276    // MIR optimizer yet, so they're parsed but unused.
277    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                // Code generation is experimental and gated behind `-Zcodegen`;
315                // without it, no bytecode is produced even when requested.
316                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/// JSON string wrapper that borrows from the standard-json input when possible.
353///
354/// Serde's generic `Cow<'de, str>` implementation deserializes through the
355/// owned representation, so direct `Cow<'de, str>` fields allocate even when
356/// the JSON backend can provide `visit_borrowed_str`. `#[serde(borrow)]` on the
357/// containing fields is still needed to thread the input lifetime to this type,
358/// and this visitor is what selects `Cow::Borrowed` for unescaped strings and
359/// `Cow::Owned` when the deserializer has to materialize an escaped string.
360///
361/// See <https://github.com/serde-rs/serde/issues/1852> and
362/// <https://github.com/serde-rs/serde/issues/914>.
363#[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
590/// Returns the diagnostic message for an unsupported Standard JSON callback kind.
591pub(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    // In solc's output selection `evm.bytecode` is the full bytecode object
818    // (`object`, `opcodes`, `sourceMap`, `linkReferences`, ...) and
819    // `evm.bytecode.object` selects only the `object` hex sub-field. We match
820    // either selector and emit a `BytecodeOutput`; since we only populate
821    // `object` for now (the other sub-fields are left empty and skipped during
822    // serialization), the two selectors currently produce identical output.
823    // Honoring the finer-grained `.object`/`.opcodes`/`.sourceMap` selectors is
824    // part of the larger effort to match solc's input->output key mapping.
825    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}