Skip to main content

runar_compiler_rust/
lib.rs

1//! Rúnar Compiler (Rust) — library root.
2//!
3//! Full compilation pipeline:
4//!   - IR consumer mode: accepts ANF IR JSON, emits Bitcoin Script.
5//!   - Source mode: compiles `.runar.ts` source files through all passes.
6
7pub mod artifact;
8pub mod codegen;
9pub mod frontend;
10pub mod ir;
11
12use artifact::{assemble_artifact, RunarArtifact};
13use codegen::emit::emit;
14use codegen::optimizer::optimize_stack_ops;
15use codegen::stack::lower_to_stack;
16use ir::loader::{load_ir, load_ir_from_str};
17
18use std::path::Path;
19
20/// Options controlling the compilation pipeline.
21#[derive(Debug, Clone)]
22pub struct CompileOptions {
23    /// When true, skip the constant-folding optimisation pass.
24    pub disable_constant_folding: bool,
25    /// Stop compilation after the parse pass (pass 1).
26    pub parse_only: bool,
27    /// Stop compilation after the validate pass (pass 2).
28    pub validate_only: bool,
29    /// Stop compilation after the type-check pass (pass 3).
30    pub typecheck_only: bool,
31    /// Bake property values into the locking script (replaces OP_0 placeholders).
32    /// Keys are property names; values are JSON values (string, number, bool).
33    pub constructor_args: std::collections::HashMap<String, serde_json::Value>,
34}
35
36impl Default for CompileOptions {
37    fn default() -> Self {
38        Self {
39            disable_constant_folding: false,
40            parse_only: false,
41            validate_only: false,
42            typecheck_only: false,
43            constructor_args: std::collections::HashMap::new(),
44        }
45    }
46}
47
48/// Apply constructor args by setting ANF property initial_value fields.
49fn apply_constructor_args(program: &mut ir::ANFProgram, args: &std::collections::HashMap<String, serde_json::Value>) {
50    if args.is_empty() {
51        return;
52    }
53    for prop in &mut program.properties {
54        if let Some(val) = args.get(&prop.name) {
55            prop.initial_value = Some(val.clone());
56        }
57    }
58}
59
60// ---------------------------------------------------------------------------
61// CompileResult — rich compilation output (mirrors TypeScript CompileResult)
62// ---------------------------------------------------------------------------
63
64/// Rich compilation result that collects ALL diagnostics from ALL passes
65/// and returns partial results as they become available.
66///
67/// Unlike the `Result<RunarArtifact, String>` API, `CompileResult` never
68/// returns an error — all errors are captured in the `diagnostics` vector.
69pub struct CompileResult {
70    /// The parsed AST (available after pass 1 — parse).
71    pub contract: Option<frontend::ast::ContractNode>,
72    /// The A-Normal Form IR (available after pass 4 — ANF lowering).
73    pub anf: Option<ir::ANFProgram>,
74    /// ALL diagnostics from ALL passes (errors + warnings).
75    pub diagnostics: Vec<frontend::diagnostic::Diagnostic>,
76    /// True only if there are no error-severity diagnostics.
77    pub success: bool,
78    /// The final compiled artifact (available if compilation succeeds).
79    pub artifact: Option<RunarArtifact>,
80    /// The hex-encoded Bitcoin Script (available if compilation succeeds).
81    pub script_hex: Option<String>,
82    /// The human-readable ASM (available if compilation succeeds).
83    pub script_asm: Option<String>,
84}
85
86impl CompileResult {
87    fn new() -> Self {
88        Self {
89            contract: None,
90            anf: None,
91            diagnostics: Vec::new(),
92            success: false,
93            artifact: None,
94            script_hex: None,
95            script_asm: None,
96        }
97    }
98
99    fn has_errors(&self) -> bool {
100        self.diagnostics.iter().any(|d| d.severity == frontend::diagnostic::Severity::Error)
101    }
102}
103
104/// Compile from an ANF IR JSON file on disk.
105pub fn compile_from_ir(path: &Path) -> Result<RunarArtifact, String> {
106    compile_from_ir_with_options(path, &CompileOptions::default())
107}
108
109/// Compile from an ANF IR JSON file on disk, with options.
110pub fn compile_from_ir_with_options(path: &Path, opts: &CompileOptions) -> Result<RunarArtifact, String> {
111    let program = load_ir(path)?;
112    compile_from_program_with_options(&program, opts)
113}
114
115/// Compile from an ANF IR JSON string.
116pub fn compile_from_ir_str(json_str: &str) -> Result<RunarArtifact, String> {
117    compile_from_ir_str_with_options(json_str, &CompileOptions::default())
118}
119
120/// Compile from an ANF IR JSON string, with options.
121pub fn compile_from_ir_str_with_options(json_str: &str, opts: &CompileOptions) -> Result<RunarArtifact, String> {
122    let program = load_ir_from_str(json_str)?;
123    compile_from_program_with_options(&program, opts)
124}
125
126/// Compile from a `.runar.ts` source file on disk.
127pub fn compile_from_source(path: &Path) -> Result<RunarArtifact, String> {
128    compile_from_source_with_options(path, &CompileOptions::default())
129}
130
131/// Compile from a `.runar.ts` source file on disk, with options.
132pub fn compile_from_source_with_options(path: &Path, opts: &CompileOptions) -> Result<RunarArtifact, String> {
133    let source = std::fs::read_to_string(path)
134        .map_err(|e| format!("reading source file: {}", e))?;
135    let file_name = path
136        .file_name()
137        .map(|n| n.to_string_lossy().to_string())
138        .unwrap_or_else(|| "contract.ts".to_string());
139    compile_from_source_str_with_options(&source, Some(&file_name), opts)
140}
141
142/// Compile from a `.runar.ts` source string.
143pub fn compile_from_source_str(
144    source: &str,
145    file_name: Option<&str>,
146) -> Result<RunarArtifact, String> {
147    compile_from_source_str_with_options(source, file_name, &CompileOptions::default())
148}
149
150/// Compile from a `.runar.ts` source string, with options.
151pub fn compile_from_source_str_with_options(
152    source: &str,
153    file_name: Option<&str>,
154    opts: &CompileOptions,
155) -> Result<RunarArtifact, String> {
156    // Pass 1: Parse (auto-selects parser based on file extension)
157    let parse_result = frontend::parser::parse_source(source, file_name);
158    if !parse_result.errors.is_empty() {
159        let error_msgs: Vec<String> = parse_result.errors.iter().map(|e| e.to_string()).collect();
160        return Err(format!("Parse errors:\n  {}", error_msgs.join("\n  ")));
161    }
162
163    let contract = parse_result
164        .contract
165        .ok_or_else(|| "No contract found in source file".to_string())?;
166
167    // Pass 2: Validate
168    let validation = frontend::validator::validate(&contract);
169    if !validation.errors.is_empty() {
170        return Err(format!(
171            "Validation errors:\n  {}",
172            validation.error_strings().join("\n  ")
173        ));
174    }
175    for w in &validation.warnings {
176        eprintln!("Validation warning: {}", w);
177    }
178
179    // Pass 3: Type-check
180    let tc_result = frontend::typecheck::typecheck(&contract);
181    if !tc_result.errors.is_empty() {
182        return Err(format!(
183            "Type-check errors:\n  {}",
184            tc_result.error_strings().join("\n  ")
185        ));
186    }
187
188    // Pass 4: ANF Lower
189    let mut anf_program = frontend::anf_lower::lower_to_anf(&contract);
190
191    // Bake constructor args into ANF properties.
192    apply_constructor_args(&mut anf_program, &opts.constructor_args);
193
194    // Pass 4.25: Constant folding (optional)
195    if !opts.disable_constant_folding {
196        anf_program = frontend::constant_fold::fold_constants(&anf_program);
197    }
198
199    // Pass 4.5: EC optimization
200    let anf_program = frontend::anf_optimize::optimize_ec(anf_program);
201
202    // Passes 5-6: Backend (stack lowering + emit)
203    // Constant folding already ran above; skip it in compile_from_program.
204    let backend_opts = CompileOptions { disable_constant_folding: true, ..Default::default() };
205    compile_from_program_with_options(&anf_program, &backend_opts)
206}
207
208/// Compile from a `.runar.ts` source file to ANF IR only (passes 1-4).
209pub fn compile_source_to_ir(path: &Path) -> Result<ir::ANFProgram, String> {
210    compile_source_to_ir_with_options(path, &CompileOptions::default())
211}
212
213/// Compile from a `.runar.ts` source file to ANF IR only (passes 1-4), with options.
214pub fn compile_source_to_ir_with_options(path: &Path, opts: &CompileOptions) -> Result<ir::ANFProgram, String> {
215    let source = std::fs::read_to_string(path)
216        .map_err(|e| format!("reading source file: {}", e))?;
217    let file_name = path
218        .file_name()
219        .map(|n| n.to_string_lossy().to_string())
220        .unwrap_or_else(|| "contract.ts".to_string());
221    compile_source_str_to_ir_with_options(&source, Some(&file_name), opts)
222}
223
224/// Compile from a `.runar.ts` source string to ANF IR only (passes 1-4).
225pub fn compile_source_str_to_ir(
226    source: &str,
227    file_name: Option<&str>,
228) -> Result<ir::ANFProgram, String> {
229    compile_source_str_to_ir_with_options(source, file_name, &CompileOptions::default())
230}
231
232/// Compile from a `.runar.ts` source string to ANF IR only (passes 1-4), with options.
233pub fn compile_source_str_to_ir_with_options(
234    source: &str,
235    file_name: Option<&str>,
236    opts: &CompileOptions,
237) -> Result<ir::ANFProgram, String> {
238    let parse_result = frontend::parser::parse_source(source, file_name);
239    if !parse_result.errors.is_empty() {
240        let error_msgs: Vec<String> = parse_result.errors.iter().map(|e| e.to_string()).collect();
241        return Err(format!("Parse errors:\n  {}", error_msgs.join("\n  ")));
242    }
243
244    let contract = parse_result
245        .contract
246        .ok_or_else(|| "No contract found in source file".to_string())?;
247
248    let validation = frontend::validator::validate(&contract);
249    if !validation.errors.is_empty() {
250        return Err(format!(
251            "Validation errors:\n  {}",
252            validation.error_strings().join("\n  ")
253        ));
254    }
255
256    let tc_result = frontend::typecheck::typecheck(&contract);
257    if !tc_result.errors.is_empty() {
258        return Err(format!(
259            "Type-check errors:\n  {}",
260            tc_result.error_strings().join("\n  ")
261        ));
262    }
263
264    let mut anf_program = frontend::anf_lower::lower_to_anf(&contract);
265
266    // Bake constructor args into ANF properties.
267    apply_constructor_args(&mut anf_program, &opts.constructor_args);
268
269    // Pass 4.25: Constant folding (optional)
270    if !opts.disable_constant_folding {
271        anf_program = frontend::constant_fold::fold_constants(&anf_program);
272    }
273
274    Ok(frontend::anf_optimize::optimize_ec(anf_program))
275}
276
277/// Run only the parse + validate passes on a source string.
278/// Returns `(errors, warnings)`. Exposed for testing warnings.
279pub fn frontend_validate(source: &str, file_name: Option<&str>) -> (Vec<String>, Vec<String>) {
280    let parse_result = frontend::parser::parse_source(source, file_name);
281    if !parse_result.errors.is_empty() {
282        return (parse_result.error_strings(), vec![]);
283    }
284    let contract = match parse_result.contract {
285        Some(c) => c,
286        None => return (vec!["No contract found".to_string()], vec![]),
287    };
288    let result = frontend::validator::validate(&contract);
289    (result.error_strings(), result.warning_strings())
290}
291
292/// Compile a parsed ANF program to a Rúnar artifact.
293pub fn compile_from_program(program: &ir::ANFProgram) -> Result<RunarArtifact, String> {
294    compile_from_program_with_options(program, &CompileOptions::default())
295}
296
297/// Compile a parsed ANF program to a Rúnar artifact, with options.
298pub fn compile_from_program_with_options(program: &ir::ANFProgram, opts: &CompileOptions) -> Result<RunarArtifact, String> {
299    // Pass 4.25: Constant folding (optional, in case we receive unoptimized ANF from IR)
300    let mut program = program.clone();
301    if !opts.disable_constant_folding {
302        program = frontend::constant_fold::fold_constants(&program);
303    }
304
305    // Pass 4.5: EC optimization (in case we receive unoptimized ANF from IR)
306    let optimized = frontend::anf_optimize::optimize_ec(program);
307
308    // Pass 5: Stack lowering
309    let mut stack_methods = lower_to_stack(&optimized)?;
310
311    // Peephole optimization — runs on Stack IR before emission.
312    // Note: source_locs must be resized to match the new ops length since the
313    // peephole optimizer may combine adjacent ops (reducing the count).
314    for method in &mut stack_methods {
315        let new_ops = optimize_stack_ops(&method.ops);
316        // After optimization the ops array may have a different length, so rebuild
317        // source_locs with the same length (None for new/merged ops).
318        method.source_locs = vec![None; new_ops.len()];
319        method.ops = new_ops;
320    }
321
322    // Pass 6: Emit
323    let emit_result = emit(&stack_methods)?;
324
325    let artifact = assemble_artifact(
326        &optimized,
327        &emit_result.script_hex,
328        &emit_result.script_asm,
329        emit_result.constructor_slots,
330        emit_result.code_separator_index,
331        emit_result.code_separator_indices,
332        true, // include ANF IR for SDK state auto-computation
333        emit_result.source_map,
334    );
335    Ok(artifact)
336}
337
338// ---------------------------------------------------------------------------
339// CompileResult API — collect all diagnostics, return partial results
340// ---------------------------------------------------------------------------
341
342/// Compile from a source string, collecting ALL diagnostics from ALL passes
343/// and returning partial results as they become available.
344///
345/// Unlike `compile_from_source_str_with_options`, this function never returns
346/// an error — all errors are captured in `CompileResult.diagnostics`.
347pub fn compile_from_source_str_with_result(
348    source: &str,
349    file_name: Option<&str>,
350    opts: &CompileOptions,
351) -> CompileResult {
352    use frontend::diagnostic::Diagnostic;
353
354    let mut result = CompileResult::new();
355
356    // Pass 1: Parse (auto-selects parser based on file extension)
357    let parse_result = frontend::parser::parse_source(source, file_name);
358    result.diagnostics.extend(parse_result.errors);
359    result.contract = parse_result.contract;
360
361    if result.has_errors() || result.contract.is_none() {
362        if result.contract.is_none() && !result.has_errors() {
363            result.diagnostics.push(Diagnostic::error(
364                "No contract found in source file",
365                None,
366            ));
367        }
368        return result;
369    }
370
371    if opts.parse_only {
372        result.success = !result.has_errors();
373        return result;
374    }
375
376    // Pass 2: Validate
377    let contract = result.contract.as_ref().unwrap();
378    let validation = frontend::validator::validate(contract);
379    result.diagnostics.extend(validation.errors);
380    result.diagnostics.extend(validation.warnings);
381
382    if result.has_errors() {
383        return result;
384    }
385
386    if opts.validate_only {
387        result.success = !result.has_errors();
388        return result;
389    }
390
391    // Pass 3: Type-check
392    let tc_result = frontend::typecheck::typecheck(contract);
393    result.diagnostics.extend(tc_result.errors);
394
395    if result.has_errors() {
396        return result;
397    }
398
399    if opts.typecheck_only {
400        result.success = !result.has_errors();
401        return result;
402    }
403
404    // Pass 4: ANF lowering
405    let mut anf_program = frontend::anf_lower::lower_to_anf(contract);
406
407    // Bake constructor args into ANF properties.
408    apply_constructor_args(&mut anf_program, &opts.constructor_args);
409
410    // Pass 4.25: Constant folding (optional)
411    if !opts.disable_constant_folding {
412        anf_program = frontend::constant_fold::fold_constants(&anf_program);
413    }
414
415    // Pass 4.5: EC optimization
416    anf_program = frontend::anf_optimize::optimize_ec(anf_program);
417    result.anf = Some(anf_program.clone());
418
419    // Pass 5: Stack lowering (catch panics)
420    let stack_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
421        lower_to_stack(&anf_program)
422    }));
423
424    let mut stack_methods = match stack_result {
425        Ok(Ok(methods)) => methods,
426        Ok(Err(e)) => {
427            result.diagnostics.push(Diagnostic::error(
428                format!("stack lowering: {}", e),
429                None,
430            ));
431            return result;
432        }
433        Err(panic_val) => {
434            let msg = if let Some(s) = panic_val.downcast_ref::<&str>() {
435                format!("stack lowering panic: {}", s)
436            } else if let Some(s) = panic_val.downcast_ref::<String>() {
437                format!("stack lowering panic: {}", s)
438            } else {
439                "stack lowering panic: unknown error".to_string()
440            };
441            result.diagnostics.push(Diagnostic::error(msg, None));
442            return result;
443        }
444    };
445
446    // Peephole optimization
447    for method in &mut stack_methods {
448        let new_ops = optimize_stack_ops(&method.ops);
449        method.source_locs = vec![None; new_ops.len()];
450        method.ops = new_ops;
451    }
452
453    // Pass 6: Emit (catch panics)
454    let emit_result_outer = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
455        emit(&stack_methods)
456    }));
457
458    match emit_result_outer {
459        Ok(Ok(emit_result)) => {
460            let anf_ref = result.anf.as_ref().unwrap();
461            let artifact = assemble_artifact(
462                anf_ref,
463                &emit_result.script_hex,
464                &emit_result.script_asm,
465                emit_result.constructor_slots,
466                emit_result.code_separator_index,
467                emit_result.code_separator_indices,
468                true,
469                emit_result.source_map,
470            );
471            result.script_hex = Some(emit_result.script_hex);
472            result.script_asm = Some(emit_result.script_asm);
473            result.artifact = Some(artifact);
474        }
475        Ok(Err(e)) => {
476            result.diagnostics.push(Diagnostic::error(
477                format!("emit: {}", e),
478                None,
479            ));
480        }
481        Err(panic_val) => {
482            let msg = if let Some(s) = panic_val.downcast_ref::<&str>() {
483                format!("emit panic: {}", s)
484            } else if let Some(s) = panic_val.downcast_ref::<String>() {
485                format!("emit panic: {}", s)
486            } else {
487                "emit panic: unknown error".to_string()
488            };
489            result.diagnostics.push(Diagnostic::error(msg, None));
490        }
491    }
492
493    result.success = !result.has_errors();
494    result
495}
496
497/// Compile from a source file on disk, collecting ALL diagnostics.
498pub fn compile_from_source_with_result(
499    path: &Path,
500    opts: &CompileOptions,
501) -> CompileResult {
502    use frontend::diagnostic::Diagnostic;
503
504    let source = match std::fs::read_to_string(path) {
505        Ok(s) => s,
506        Err(e) => {
507            let mut result = CompileResult::new();
508            result.diagnostics.push(Diagnostic::error(
509                format!("reading source file: {}", e),
510                None,
511            ));
512            return result;
513        }
514    };
515    let file_name = path
516        .file_name()
517        .map(|n| n.to_string_lossy().to_string())
518        .unwrap_or_else(|| "contract.ts".to_string());
519    compile_from_source_str_with_result(&source, Some(&file_name), opts)
520}