Rust · 33551 bytes Raw Blame History
1 //! Bench-facing capture helpers.
2 //!
3 //! This module exposes a stable-ish API for collecting intermediate artifacts
4 //! from the compiler pipeline so the external `afs-tests` runner can assert on
5 //! more than just final program output.
6
7 use std::collections::{BTreeMap, BTreeSet};
8 use std::fmt::Write;
9 use std::fs;
10 use std::path::{Path, PathBuf};
11 use std::process::Command;
12 use std::sync::atomic::{AtomicU64, Ordering};
13 use std::time::SystemTime;
14
15 use crate::codegen::mir::{
16 ArmCond, ConstPoolEntry, MBlockId, MachineFunction, MachineInst, MachineOperand, PhysReg,
17 RegClass,
18 };
19 use crate::codegen::{emit, isel, linearscan};
20 use crate::driver::OptLevel;
21 use crate::ir::{lower, printer as ir_printer, verify};
22 use crate::lexer::{detect_source_form, tokenize, SourceForm, Token};
23 use crate::opt::pipeline::OptLevel as IrOptLevel;
24 use crate::opt::{build_i128_pipeline, build_pipeline};
25 use crate::parser::Parser;
26 use crate::sema::{resolve, validate};
27
28 static CAPTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
29
30 /// Capturable compiler stages for the bench.
31 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32 pub enum Stage {
33 Preprocess,
34 Tokens,
35 Ast,
36 Sema,
37 Ir,
38 OptIr,
39 Mir,
40 Regalloc,
41 Asm,
42 Obj,
43 Run,
44 }
45
46 impl Stage {
47 pub const ALL: [Stage; 11] = [
48 Stage::Preprocess,
49 Stage::Tokens,
50 Stage::Ast,
51 Stage::Sema,
52 Stage::Ir,
53 Stage::OptIr,
54 Stage::Mir,
55 Stage::Regalloc,
56 Stage::Asm,
57 Stage::Obj,
58 Stage::Run,
59 ];
60
61 pub fn parse(name: &str) -> Option<Self> {
62 match name.trim().to_ascii_lowercase().as_str() {
63 "preprocess" => Some(Self::Preprocess),
64 "tokens" => Some(Self::Tokens),
65 "ast" => Some(Self::Ast),
66 "sema" => Some(Self::Sema),
67 "ir" => Some(Self::Ir),
68 "optir" => Some(Self::OptIr),
69 "mir" => Some(Self::Mir),
70 "regalloc" => Some(Self::Regalloc),
71 "asm" => Some(Self::Asm),
72 "obj" => Some(Self::Obj),
73 "run" => Some(Self::Run),
74 _ => None,
75 }
76 }
77
78 pub fn as_str(&self) -> &'static str {
79 match self {
80 Self::Preprocess => "preprocess",
81 Self::Tokens => "tokens",
82 Self::Ast => "ast",
83 Self::Sema => "sema",
84 Self::Ir => "ir",
85 Self::OptIr => "optir",
86 Self::Mir => "mir",
87 Self::Regalloc => "regalloc",
88 Self::Asm => "asm",
89 Self::Obj => "obj",
90 Self::Run => "run",
91 }
92 }
93 }
94
95 /// Failure point while trying to capture compiler stages.
96 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
97 pub enum FailureStage {
98 Preprocess,
99 Lexer,
100 Parser,
101 Sema,
102 Ir,
103 Obj,
104 Run,
105 }
106
107 impl FailureStage {
108 pub fn parse(name: &str) -> Option<Self> {
109 match name.trim().to_ascii_lowercase().as_str() {
110 "preprocess" => Some(Self::Preprocess),
111 "lexer" | "tokens" => Some(Self::Lexer),
112 "parser" | "parse" | "ast" => Some(Self::Parser),
113 "sema" => Some(Self::Sema),
114 "ir" | "optir" => Some(Self::Ir),
115 "obj" | "asm" => Some(Self::Obj),
116 "run" => Some(Self::Run),
117 _ => None,
118 }
119 }
120
121 pub fn as_str(&self) -> &'static str {
122 match self {
123 Self::Preprocess => "preprocess",
124 Self::Lexer => "lexer",
125 Self::Parser => "parser",
126 Self::Sema => "sema",
127 Self::Ir => "ir",
128 Self::Obj => "obj",
129 Self::Run => "run",
130 }
131 }
132 }
133
134 /// A stage capture request for one input source file.
135 #[derive(Debug, Clone)]
136 pub struct CaptureRequest {
137 pub input: PathBuf,
138 pub requested: BTreeSet<Stage>,
139 pub opt_level: OptLevel,
140 }
141
142 impl CaptureRequest {
143 pub fn new(input: impl Into<PathBuf>) -> Self {
144 Self {
145 input: input.into(),
146 requested: BTreeSet::new(),
147 opt_level: OptLevel::O0,
148 }
149 }
150
151 pub fn with_stage(mut self, stage: Stage) -> Self {
152 self.requested.insert(stage);
153 self
154 }
155
156 pub fn with_all_stages(mut self) -> Self {
157 self.requested.extend(Stage::ALL);
158 self
159 }
160
161 pub fn with_opt_level(mut self, opt_level: OptLevel) -> Self {
162 self.opt_level = opt_level;
163 self
164 }
165 }
166
167 /// Captured data for a single run.
168 #[derive(Debug, Clone)]
169 pub struct CaptureResult {
170 pub input: PathBuf,
171 pub opt_level: OptLevel,
172 pub stages: BTreeMap<Stage, CapturedStage>,
173 }
174
175 impl CaptureResult {
176 pub fn get(&self, stage: Stage) -> Option<&CapturedStage> {
177 self.stages.get(&stage)
178 }
179 }
180
181 /// Failed stage capture with partial artifacts preserved.
182 #[derive(Debug, Clone)]
183 pub struct CaptureFailure {
184 pub input: PathBuf,
185 pub opt_level: OptLevel,
186 pub stage: FailureStage,
187 pub detail: String,
188 pub stages: BTreeMap<Stage, CapturedStage>,
189 }
190
191 impl CaptureFailure {
192 pub fn partial_result(&self) -> CaptureResult {
193 CaptureResult {
194 input: self.input.clone(),
195 opt_level: self.opt_level,
196 stages: self.stages.clone(),
197 }
198 }
199 }
200
201 impl std::fmt::Display for CaptureFailure {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 write!(f, "{}: {}", self.stage.as_str(), self.detail)
204 }
205 }
206
207 impl std::error::Error for CaptureFailure {}
208
209 /// Captured artifact content.
210 #[derive(Debug, Clone)]
211 pub enum CapturedStage {
212 Text(String),
213 Run(RunCapture),
214 }
215
216 impl CapturedStage {
217 pub fn as_text(&self) -> Option<&str> {
218 match self {
219 Self::Text(text) => Some(text),
220 Self::Run(_) => None,
221 }
222 }
223
224 pub fn as_run(&self) -> Option<&RunCapture> {
225 match self {
226 Self::Text(_) => None,
227 Self::Run(run) => Some(run),
228 }
229 }
230 }
231
232 /// Final executable output.
233 #[derive(Debug, Clone)]
234 pub struct RunCapture {
235 pub exit_code: i32,
236 pub stdout: String,
237 pub stderr: String,
238 pub files: BTreeMap<String, Vec<u8>>,
239 }
240
241 /// Capture the requested stages for one source file.
242 pub fn capture_from_path(request: &CaptureRequest) -> Result<CaptureResult, CaptureFailure> {
243 let mut stages = BTreeMap::new();
244 let wants = |stage| request.requested.contains(&stage);
245 let needs_backend = request.requested.iter().any(|stage| {
246 matches!(
247 stage,
248 Stage::Mir | Stage::Regalloc | Stage::Asm | Stage::Obj | Stage::Run
249 )
250 });
251 let input = request.input.clone();
252 let source_form = detect_source_form(&input.to_string_lossy());
253
254 let source = fs::read_to_string(&input).map_err(|e| CaptureFailure {
255 input: input.clone(),
256 opt_level: request.opt_level,
257 stage: FailureStage::Preprocess,
258 detail: format!("cannot read '{}': {}", input.display(), e),
259 stages: stages.clone(),
260 })?;
261
262 let pp_config = crate::preprocess::PreprocConfig {
263 filename: input.to_string_lossy().into_owned(),
264 fixed_form: matches!(source_form, SourceForm::FixedForm),
265 ..crate::preprocess::PreprocConfig::default()
266 };
267 let pp_result =
268 crate::preprocess::preprocess(&source, &pp_config).map_err(|e| CaptureFailure {
269 input: input.clone(),
270 opt_level: request.opt_level,
271 stage: FailureStage::Preprocess,
272 detail: e.to_string(),
273 stages: stages.clone(),
274 })?;
275 let preprocessed = pp_result.text;
276 if wants(Stage::Preprocess) {
277 stages.insert(Stage::Preprocess, CapturedStage::Text(preprocessed.clone()));
278 }
279
280 let tokens = tokenize(&preprocessed, 0, source_form).map_err(|e| CaptureFailure {
281 input: input.clone(),
282 opt_level: request.opt_level,
283 stage: FailureStage::Lexer,
284 detail: format!(
285 "{}:{}: lexer error: {}",
286 input.display(),
287 e.span.start.line,
288 e.msg
289 ),
290 stages: stages.clone(),
291 })?;
292 if wants(Stage::Tokens) {
293 stages.insert(Stage::Tokens, CapturedStage::Text(format_tokens(&tokens)));
294 }
295
296 let mut parser = Parser::new(&tokens);
297 let units = parser.parse_file().map_err(|e| CaptureFailure {
298 input: input.clone(),
299 opt_level: request.opt_level,
300 stage: FailureStage::Parser,
301 detail: format!(
302 "{}:{}:{}: parse error: {}",
303 input.display(),
304 e.span.start.line,
305 e.span.start.col,
306 e.msg
307 ),
308 stages: stages.clone(),
309 })?;
310 if wants(Stage::Ast) {
311 stages.insert(Stage::Ast, CapturedStage::Text(format!("{:#?}", units)));
312 }
313
314 let rr = resolve::resolve_file(&units, &[]).map_err(|e| CaptureFailure {
315 input: input.clone(),
316 opt_level: request.opt_level,
317 stage: FailureStage::Sema,
318 detail: format!("{}:{}: {}", input.display(), e.span.start.line, e.msg),
319 stages: stages.clone(),
320 })?;
321 let st = rr.st;
322 let type_layouts = rr.type_layouts;
323 let diags = validate::validate_file(&units, &st);
324 if wants(Stage::Sema) {
325 stages.insert(
326 Stage::Sema,
327 CapturedStage::Text(format_sema_snapshot(&st, &type_layouts, &diags)),
328 );
329 }
330 let sema_errors: Vec<_> = diags
331 .iter()
332 .filter(|d| d.kind == validate::DiagKind::Error)
333 .collect();
334 if !sema_errors.is_empty() {
335 return Err(CaptureFailure {
336 input: input.clone(),
337 opt_level: request.opt_level,
338 stage: FailureStage::Sema,
339 detail: format_diagnostics(&input, &sema_errors),
340 stages,
341 });
342 }
343
344 let (ir_module, _module_globals) = lower::lower_file(
345 &units,
346 &st,
347 &type_layouts,
348 std::collections::HashMap::new(),
349 std::collections::HashMap::new(),
350 std::collections::HashMap::new(),
351 std::collections::HashMap::new(),
352 );
353 let ir_errors = verify::verify_module(&ir_module);
354 if !ir_errors.is_empty() {
355 let msg = ir_errors
356 .iter()
357 .map(|e| e.to_string())
358 .collect::<Vec<_>>()
359 .join("\n");
360 return Err(CaptureFailure {
361 input: input.clone(),
362 opt_level: request.opt_level,
363 stage: FailureStage::Ir,
364 detail: format!("internal error: IR verification failed:\n{}", msg),
365 stages,
366 });
367 }
368
369 let ir_text = ir_printer::print_module(&ir_module);
370 if wants(Stage::Ir) {
371 stages.insert(Stage::Ir, CapturedStage::Text(ir_text.clone()));
372 }
373 let module_has_i128 = ir_module.contains_i128();
374 let needs_optimized_pipeline = request.requested.iter().any(|stage| {
375 matches!(
376 stage,
377 Stage::OptIr | Stage::Mir | Stage::Regalloc | Stage::Asm | Stage::Obj | Stage::Run
378 )
379 }) && request.opt_level != OptLevel::O0;
380
381 let optimized_module = if needs_optimized_pipeline {
382 let mut optimized = ir_module.clone();
383 let ir_opt = match request.opt_level {
384 OptLevel::O0 => IrOptLevel::O0,
385 OptLevel::O1 => IrOptLevel::O1,
386 OptLevel::O2 => IrOptLevel::O2,
387 OptLevel::O3 => IrOptLevel::O3,
388 OptLevel::Os => IrOptLevel::Os,
389 OptLevel::Ofast => IrOptLevel::Ofast,
390 };
391 let pm = if ir_module.contains_i128_outside_globals() && request.opt_level != OptLevel::O0 {
392 build_i128_pipeline(ir_opt).ok_or_else(|| CaptureFailure {
393 input: input.clone(),
394 opt_level: request.opt_level,
395 stage: FailureStage::Ir,
396 detail: format!(
397 "integer(16) / i128 optimization at {} is not yet supported; capture raw IR or use a supported opt level",
398 request.opt_level.as_flag()
399 ),
400 stages: stages.clone(),
401 })?
402 } else {
403 build_pipeline(ir_opt)
404 };
405 pm.run(&mut optimized);
406 Some(optimized)
407 } else {
408 None
409 };
410
411 if wants(Stage::OptIr) {
412 let opt_ir_text = if let Some(module) = optimized_module.as_ref() {
413 ir_printer::print_module(module)
414 } else {
415 ir_text.clone()
416 };
417 stages.insert(Stage::OptIr, CapturedStage::Text(opt_ir_text));
418 }
419
420 if !needs_backend {
421 return Ok(CaptureResult {
422 input,
423 opt_level: request.opt_level,
424 stages,
425 });
426 }
427
428 let backend_ir = optimized_module.as_ref().unwrap_or(&ir_module);
429 if module_has_i128 && !backend_ir.i128_backend_o0_supported() {
430 return Err(CaptureFailure {
431 input: input.clone(),
432 opt_level: request.opt_level,
433 stage: FailureStage::Ir,
434 detail:
435 "backend does not yet support integer(16) / i128 codegen; capture IR at O0 for now"
436 .into(),
437 stages,
438 });
439 }
440 let machine_funcs = isel::select_module(backend_ir);
441 if wants(Stage::Mir) {
442 stages.insert(
443 Stage::Mir,
444 CapturedStage::Text(format_machine_functions(&machine_funcs)),
445 );
446 }
447
448 let mut allocated = machine_funcs.clone();
449 // Backend peephole at O2+ (must run BEFORE regalloc — it
450 // operates on vregs).
451 if request.opt_level >= OptLevel::O2 {
452 for mf in &mut allocated {
453 crate::codegen::peephole::run_peephole(mf);
454 }
455 }
456 for mf in &mut allocated {
457 let liveness = crate::codegen::liveness::compute_liveness(mf);
458 let result = linearscan::linear_scan(mf);
459 linearscan::apply_allocation(mf, &result, &liveness);
460 // Post-allocation passes — must mirror the driver pipeline so
461 // captured asm matches the binary the user actually ships.
462 // parallelize_call_arg_moves in particular fixes a w0/w1
463 // clobber pattern visible at high register pressure.
464 linearscan::parallelize_entry_arg_moves(mf);
465 linearscan::parallelize_call_arg_moves(mf);
466 linearscan::insert_split_bridges(mf, &result.split_records);
467 linearscan::insert_callee_saves(mf, &result.callee_saved_used);
468 linearscan::coalesce_moves(mf);
469 if request.opt_level >= OptLevel::O1 {
470 crate::codegen::tailcall::tail_call_opt(mf);
471 }
472 crate::codegen::relax_branches::relax_branches(mf);
473 }
474 if wants(Stage::Regalloc) {
475 stages.insert(
476 Stage::Regalloc,
477 CapturedStage::Text(format_machine_functions(&allocated)),
478 );
479 }
480
481 let asm_text = emit_module_asm(backend_ir, &allocated);
482 if wants(Stage::Asm) {
483 stages.insert(Stage::Asm, CapturedStage::Text(asm_text.clone()));
484 }
485
486 if wants(Stage::Obj) || wants(Stage::Run) {
487 let temp_root = next_temp_root("afs_tests_capture");
488 let asm_path = temp_root.join("input.s");
489 let obj_path = temp_root.join("output.o");
490 let bin_path = temp_root.join("output");
491
492 fs::create_dir_all(&temp_root).map_err(|e| CaptureFailure {
493 input: input.clone(),
494 opt_level: request.opt_level,
495 stage: FailureStage::Obj,
496 detail: format!("cannot create temp dir '{}': {}", temp_root.display(), e),
497 stages: stages.clone(),
498 })?;
499 fs::write(&asm_path, &asm_text).map_err(|e| CaptureFailure {
500 input: input.clone(),
501 opt_level: request.opt_level,
502 stage: FailureStage::Obj,
503 detail: format!("cannot write temp assembly '{}': {}", asm_path.display(), e),
504 stages: stages.clone(),
505 })?;
506
507 assemble_with_system(&asm_path, &obj_path).map_err(|detail| CaptureFailure {
508 input: input.clone(),
509 opt_level: request.opt_level,
510 stage: FailureStage::Obj,
511 detail,
512 stages: stages.clone(),
513 })?;
514
515 if wants(Stage::Obj) {
516 let snapshot = object_snapshot(&obj_path).map_err(|detail| CaptureFailure {
517 input: input.clone(),
518 opt_level: request.opt_level,
519 stage: FailureStage::Obj,
520 detail,
521 stages: stages.clone(),
522 })?;
523 stages.insert(Stage::Obj, CapturedStage::Text(snapshot));
524 }
525
526 if wants(Stage::Run) {
527 link_with_runtime(&obj_path, &bin_path).map_err(|detail| CaptureFailure {
528 input: input.clone(),
529 opt_level: request.opt_level,
530 stage: FailureStage::Run,
531 detail,
532 stages: stages.clone(),
533 })?;
534 let sandbox = temp_root.join("run_sandbox");
535 fs::create_dir_all(&sandbox).map_err(|e| CaptureFailure {
536 input: input.clone(),
537 opt_level: request.opt_level,
538 stage: FailureStage::Run,
539 detail: format!("cannot create run sandbox '{}': {}", sandbox.display(), e),
540 stages: stages.clone(),
541 })?;
542 let output = Command::new(&bin_path)
543 .current_dir(&sandbox)
544 .output()
545 .map_err(|e| CaptureFailure {
546 input: input.clone(),
547 opt_level: request.opt_level,
548 stage: FailureStage::Run,
549 detail: format!("cannot run '{}': {}", bin_path.display(), e),
550 stages: stages.clone(),
551 })?;
552 let files = snapshot_sandbox_files(&sandbox).map_err(|detail| CaptureFailure {
553 input: input.clone(),
554 opt_level: request.opt_level,
555 stage: FailureStage::Run,
556 detail,
557 stages: stages.clone(),
558 })?;
559 stages.insert(
560 Stage::Run,
561 CapturedStage::Run(RunCapture {
562 exit_code: output.status.code().unwrap_or(-1),
563 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
564 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
565 files,
566 }),
567 );
568 }
569
570 let _ = fs::remove_dir_all(&temp_root);
571 }
572
573 Ok(CaptureResult {
574 input,
575 opt_level: request.opt_level,
576 stages,
577 })
578 }
579
580 fn collect_sandbox_files(
581 root: &Path,
582 dir: &Path,
583 out: &mut BTreeMap<String, Vec<u8>>,
584 ) -> std::io::Result<()> {
585 for entry in fs::read_dir(dir)? {
586 let entry = entry?;
587 let path = entry.path();
588 if entry.file_type()?.is_dir() {
589 collect_sandbox_files(root, &path, out)?;
590 } else {
591 let rel = path.strip_prefix(root).unwrap();
592 out.insert(rel.to_string_lossy().replace('\\', "/"), fs::read(&path)?);
593 }
594 }
595 Ok(())
596 }
597
598 fn snapshot_sandbox_files(sandbox: &Path) -> Result<BTreeMap<String, Vec<u8>>, String> {
599 let mut files = BTreeMap::new();
600 collect_sandbox_files(sandbox, sandbox, &mut files)
601 .map_err(|e| format!("cannot snapshot sandbox '{}': {}", sandbox.display(), e))?;
602 Ok(files)
603 }
604
605 fn format_tokens(tokens: &[Token]) -> String {
606 let mut out = String::new();
607 for token in tokens {
608 let _ = writeln!(
609 out,
610 "{}:{}-{}:{}\t{}\t{:?}",
611 token.span.start.line,
612 token.span.start.col,
613 token.span.end.line,
614 token.span.end.col,
615 token.kind,
616 token.text
617 );
618 }
619 out
620 }
621
622 fn format_sema_snapshot(
623 st: &crate::sema::symtab::SymbolTable,
624 type_layouts: &crate::sema::type_layout::TypeLayoutRegistry,
625 diags: &[validate::Diagnostic],
626 ) -> String {
627 let mut out = String::new();
628 if diags.is_empty() {
629 let _ = writeln!(out, "diagnostics: none");
630 } else {
631 let _ = writeln!(out, "diagnostics:");
632 for diag in diags {
633 let _ = writeln!(out, " {}", diag);
634 }
635 }
636 let _ = writeln!(out, "\nsymbol_table:\n{:#?}", st);
637 let _ = writeln!(out, "\ntype_layouts:\n{:#?}", type_layouts);
638 out
639 }
640
641 fn format_diagnostics(path: &Path, diags: &[&validate::Diagnostic]) -> String {
642 diags
643 .iter()
644 .map(|diag| {
645 format!(
646 "{}:{}:{}: {}",
647 path.display(),
648 diag.span.start.line,
649 diag.span.start.col,
650 diag.msg
651 )
652 })
653 .collect::<Vec<_>>()
654 .join("\n")
655 }
656
657 fn format_machine_functions(machine_funcs: &[MachineFunction]) -> String {
658 let mut out = String::new();
659 for (index, mf) in machine_funcs.iter().enumerate() {
660 if index > 0 {
661 out.push('\n');
662 }
663
664 let _ = writeln!(out, "function {}:", mf.name);
665 let _ = writeln!(out, " frame_size: {}", mf.frame.size);
666
667 if mf.frame.locals.is_empty() {
668 let _ = writeln!(out, " locals: none");
669 } else {
670 let locals = mf
671 .frame
672 .locals
673 .iter()
674 .map(|slot| format!("[fp{:+}]:{}", slot.offset, slot.size))
675 .collect::<Vec<_>>()
676 .join(", ");
677 let _ = writeln!(out, " locals: {}", locals);
678 }
679
680 if mf.vregs.is_empty() {
681 let _ = writeln!(out, " vregs: none");
682 } else {
683 let vregs = mf
684 .vregs
685 .iter()
686 .map(|vreg| format!("v{}:{}", vreg.id.0, format_reg_class(vreg.class)))
687 .collect::<Vec<_>>()
688 .join(", ");
689 let _ = writeln!(out, " vregs: {}", vregs);
690 }
691
692 for block in &mf.blocks {
693 let _ = writeln!(out, " block {}:", block.label);
694 if block.insts.is_empty() {
695 let _ = writeln!(out, " <empty>");
696 } else {
697 for inst in &block.insts {
698 let _ = writeln!(out, " {}", format_machine_inst(mf, inst));
699 }
700 }
701 }
702
703 if mf.const_pool.is_empty() {
704 let _ = writeln!(out, " const_pool: none");
705 } else {
706 let _ = writeln!(out, " const_pool:");
707 for (pool_index, entry) in mf.const_pool.iter().enumerate() {
708 let _ = writeln!(
709 out,
710 " [{}] {}",
711 pool_index,
712 format_const_pool_entry(entry)
713 );
714 }
715 }
716 }
717 out
718 }
719
720 fn format_machine_inst(mf: &MachineFunction, inst: &MachineInst) -> String {
721 let opcode = format!("{:?}", inst.opcode).to_ascii_lowercase();
722 let operands = inst
723 .operands
724 .iter()
725 .map(|operand| format_machine_operand(mf, operand))
726 .collect::<Vec<_>>();
727 if operands.is_empty() {
728 opcode
729 } else {
730 format!("{} {}", opcode, operands.join(", "))
731 }
732 }
733
734 fn format_machine_operand(mf: &MachineFunction, operand: &MachineOperand) -> String {
735 match operand {
736 MachineOperand::VReg(vreg) => format!("v{}", vreg.0),
737 MachineOperand::PhysReg(reg) => format_phys_reg(*reg),
738 MachineOperand::Imm(value) => {
739 if *value == -1 {
740 "#frame-16".to_string()
741 } else {
742 format!("#{}", value)
743 }
744 }
745 MachineOperand::FrameSlot(offset) => format!("[fp{:+}]", offset),
746 MachineOperand::Cond(cond) => format_cond(*cond).to_string(),
747 MachineOperand::BlockRef(block_id) => block_label(mf, *block_id),
748 MachineOperand::Extern(name) => format!("extern {}", name),
749 MachineOperand::ConstPool(index) => format!("constpool[{}]", index),
750 MachineOperand::Shift(bits) => format!("lsl #{}", bits),
751 MachineOperand::GlobalLabel(name) => format!("global:{}", name),
752 }
753 }
754
755 fn format_phys_reg(reg: PhysReg) -> String {
756 match reg {
757 PhysReg::Gp(num) => format!("x{}", num),
758 PhysReg::Gp32(num) => format!("w{}", num),
759 PhysReg::Fp(num) => format!("d{}", num),
760 PhysReg::Fp32(num) => format!("s{}", num),
761 PhysReg::Sp => "sp".to_string(),
762 PhysReg::Xzr => "xzr".to_string(),
763 PhysReg::Wzr => "wzr".to_string(),
764 }
765 }
766
767 fn format_reg_class(class: RegClass) -> &'static str {
768 match class {
769 RegClass::Gp64 => "gp64",
770 RegClass::Gp32 => "gp32",
771 RegClass::Fp64 => "fp64",
772 RegClass::Fp32 => "fp32",
773 RegClass::V128 => "v128",
774 }
775 }
776
777 fn format_cond(cond: ArmCond) -> &'static str {
778 match cond {
779 ArmCond::Eq => "eq",
780 ArmCond::Ne => "ne",
781 ArmCond::Hs => "hs",
782 ArmCond::Lo => "lo",
783 ArmCond::Mi => "mi",
784 ArmCond::Pl => "pl",
785 ArmCond::Hi => "hi",
786 ArmCond::Ls => "ls",
787 ArmCond::Ge => "ge",
788 ArmCond::Lt => "lt",
789 ArmCond::Gt => "gt",
790 ArmCond::Le => "le",
791 }
792 }
793
794 fn block_label(mf: &MachineFunction, block_id: MBlockId) -> String {
795 mf.blocks
796 .iter()
797 .find(|block| block.id == block_id)
798 .map(|block| block.label.clone())
799 .unwrap_or_else(|| format!("block#{}", block_id.0))
800 }
801
802 fn format_const_pool_entry(entry: &ConstPoolEntry) -> String {
803 match entry {
804 ConstPoolEntry::F32(value) => format!("f32 {}", value),
805 ConstPoolEntry::F64(value) => format!("f64 {}", value),
806 ConstPoolEntry::I64(value) => format!("i64 {}", value),
807 ConstPoolEntry::Bytes(bytes) => format!("bytes {:?}", escape_const_bytes(bytes)),
808 }
809 }
810
811 fn escape_const_bytes(bytes: &[u8]) -> String {
812 let mut out = String::new();
813 for &byte in bytes {
814 match byte {
815 b'\\' => out.push_str("\\\\"),
816 b'"' => out.push_str("\\\""),
817 b'\n' => out.push_str("\\n"),
818 b'\t' => out.push_str("\\t"),
819 b if b.is_ascii_graphic() || b == b' ' => out.push(b as char),
820 other => {
821 let _ = write!(out, "\\x{:02x}", other);
822 }
823 }
824 }
825 out
826 }
827
828 fn emit_module_asm(module: &crate::ir::inst::Module, allocated: &[MachineFunction]) -> String {
829 let mut asm_text = String::new();
830 asm_text.push_str(".section __TEXT,__text,regular,pure_instructions\n");
831 for mf in allocated {
832 asm_text.push_str(".section __TEXT,__text,regular,pure_instructions\n");
833 asm_text.push_str(&emit::emit_function(mf));
834 asm_text.push('\n');
835 }
836
837 if !module.globals.is_empty() {
838 asm_text.push_str(&emit::emit_globals(&module.globals));
839 asm_text.push('\n');
840 }
841
842 if let Some(user_func) = main_wrapper_target(allocated) {
843 if user_func != "main" {
844 let _ = write!(
845 asm_text,
846 "\n.section __TEXT,__text,regular,pure_instructions\n\
847 .globl _main\n\
848 .p2align 2\n\
849 _main:\n\
850 stp x29, x30, [sp, #-16]!\n\
851 mov x29, sp\n\
852 bl _afs_program_init\n\
853 bl _{}\n\
854 bl _afs_program_finalize\n\
855 mov x0, #0\n\
856 ldp x29, x30, [sp], #16\n\
857 ret\n",
858 user_func
859 );
860 }
861 }
862
863 asm_text
864 }
865
866 fn main_wrapper_target(allocated: &[MachineFunction]) -> Option<&str> {
867 allocated
868 .iter()
869 .find(|func| func.name.starts_with("__prog_"))
870 .or_else(|| allocated.iter().find(|func| func.name != "main"))
871 .map(|func| func.name.as_str())
872 }
873
874 fn next_temp_root(prefix: &str) -> PathBuf {
875 let id = CAPTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
876 std::env::temp_dir().join(format!("{}_{}_{}", prefix, std::process::id(), id))
877 }
878
879 fn assemble_with_system(asm_path: &Path, obj_path: &Path) -> Result<(), String> {
880 let output = Command::new("as")
881 .args([
882 "-o",
883 obj_path.to_str().unwrap_or("output.o"),
884 asm_path.to_str().unwrap_or("input.s"),
885 ])
886 .output()
887 .map_err(|e| format!("cannot run assembler: {}", e))?;
888 if output.status.success() {
889 Ok(())
890 } else {
891 Err(format!(
892 "assembler failed:\n{}",
893 String::from_utf8_lossy(&output.stderr)
894 ))
895 }
896 }
897
898 fn link_with_runtime(obj: &Path, output: &Path) -> Result<(), String> {
899 let rt_path = find_runtime_lib()?;
900 let sdk = Command::new("xcrun")
901 .args(["--show-sdk-path"])
902 .output()
903 .map_err(|e| format!("cannot run xcrun: {}", e))?;
904 if !sdk.status.success() {
905 return Err(format!(
906 "xcrun failed:\n{}",
907 String::from_utf8_lossy(&sdk.stderr)
908 ));
909 }
910 let sysroot = String::from_utf8_lossy(&sdk.stdout).trim().to_string();
911
912 let ld = Command::new("ld")
913 .args([
914 obj.to_str().unwrap(),
915 &rt_path,
916 "-lSystem",
917 "-syslibroot",
918 &sysroot,
919 "-e",
920 "_main",
921 "-o",
922 output.to_str().unwrap(),
923 ])
924 .output()
925 .map_err(|e| format!("cannot run linker: {}", e))?;
926 if ld.status.success() {
927 Ok(())
928 } else {
929 Err(format!(
930 "linker failed:\n{}",
931 String::from_utf8_lossy(&ld.stderr)
932 ))
933 }
934 }
935
936 fn find_runtime_lib() -> Result<String, String> {
937 if let Some(workspace_root) = find_workspace_root() {
938 maybe_refresh_runtime_lib(&workspace_root)?;
939 for candidate in [
940 workspace_root.join("target/debug/libarmfortas_rt.a"),
941 workspace_root.join("target/release/libarmfortas_rt.a"),
942 ] {
943 if candidate.exists() {
944 return Ok(candidate.to_string_lossy().into_owned());
945 }
946 }
947 }
948
949 if let Ok(exe) = std::env::current_exe() {
950 if let Some(dir) = exe.parent() {
951 let candidate = dir.join("libarmfortas_rt.a");
952 if candidate.exists() {
953 return Ok(candidate.to_string_lossy().into_owned());
954 }
955 }
956 }
957
958 Err("cannot find libarmfortas_rt.a — build with 'cargo build -p armfortas-rt'".into())
959 }
960
961 fn maybe_refresh_runtime_lib(workspace_root: &Path) -> Result<(), String> {
962 let runtime_dir = workspace_root.join("runtime");
963 if !runtime_dir.join("Cargo.toml").exists() {
964 return Ok(());
965 }
966
967 let Some(source_mtime) = newest_mtime(&runtime_dir) else {
968 return Ok(());
969 };
970 let debug_archive = workspace_root.join("target/debug/libarmfortas_rt.a");
971 let archive_mtime = fs::metadata(&debug_archive)
972 .ok()
973 .and_then(|meta| meta.modified().ok());
974
975 if archive_mtime.is_some_and(|mtime| mtime >= source_mtime) {
976 return Ok(());
977 }
978
979 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
980 let output = Command::new(cargo)
981 .current_dir(workspace_root)
982 .args(["build", "-p", "armfortas-rt"])
983 .output()
984 .map_err(|e| format!("cannot rebuild libarmfortas_rt.a: {}", e))?;
985 if output.status.success() {
986 Ok(())
987 } else {
988 Err(format!(
989 "cannot rebuild libarmfortas_rt.a:\n{}",
990 String::from_utf8_lossy(&output.stderr)
991 ))
992 }
993 }
994
995 fn newest_mtime(path: &Path) -> Option<SystemTime> {
996 let meta = fs::metadata(path).ok()?;
997 let mut newest = meta.modified().ok()?;
998 if meta.is_dir() {
999 for entry in fs::read_dir(path).ok()? {
1000 let entry = entry.ok()?;
1001 let child = newest_mtime(&entry.path())?;
1002 if child > newest {
1003 newest = child;
1004 }
1005 }
1006 }
1007 Some(newest)
1008 }
1009
1010 fn find_workspace_root() -> Option<PathBuf> {
1011 let mut bases = Vec::new();
1012 if let Ok(cwd) = std::env::current_dir() {
1013 bases.push(cwd);
1014 }
1015 if let Ok(exe) = std::env::current_exe() {
1016 if let Some(dir) = exe.parent() {
1017 bases.push(dir.to_path_buf());
1018 }
1019 }
1020
1021 for base in bases {
1022 for ancestor in base.ancestors() {
1023 if ancestor.join("Cargo.toml").exists() && ancestor.join("runtime/Cargo.toml").exists()
1024 {
1025 return Some(ancestor.to_path_buf());
1026 }
1027 }
1028 }
1029 None
1030 }
1031
1032 fn object_snapshot(path: &Path) -> Result<String, String> {
1033 let text = normalize_tool_output(&tool_output("otool", &["-t", path.to_str().unwrap()])?);
1034 let load = normalize_tool_output(&tool_output("otool", &["-l", path.to_str().unwrap()])?);
1035 let relocs = normalize_tool_output(&tool_output("otool", &["-rv", path.to_str().unwrap()])?);
1036 let symbols = normalize_tool_output(&tool_output("nm", &["-m", path.to_str().unwrap()])?);
1037
1038 Ok(format!(
1039 "== text ==\n{}\n\n== load_commands ==\n{}\n\n== relocations ==\n{}\n\n== symbols ==\n{}",
1040 text, load, relocs, symbols
1041 ))
1042 }
1043
1044 fn tool_output(tool: &str, args: &[&str]) -> Result<String, String> {
1045 let output = Command::new(tool)
1046 .args(args)
1047 .output()
1048 .map_err(|e| format!("cannot run {}: {}", tool, e))?;
1049 if output.status.success() {
1050 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
1051 } else {
1052 Err(format!(
1053 "{} failed:\n{}",
1054 tool,
1055 String::from_utf8_lossy(&output.stderr)
1056 ))
1057 }
1058 }
1059
1060 fn normalize_tool_output(text: &str) -> String {
1061 text.lines()
1062 .filter(|line| !line.trim_end().ends_with(".o:"))
1063 .map(str::trim_end)
1064 .collect::<Vec<_>>()
1065 .join("\n")
1066 }
1067