Skip to main content

solilang/
lib.rs

1//! Solilang: A statically-typed, class-based OOP language with pipeline operators.
2//!
3//! This is the library root that exports all modules.
4//!
5//! # Execution
6//!
7//! Solilang uses a tree-walking interpreter for executing programs.
8
9pub mod ast;
10pub mod coverage;
11pub mod error;
12pub mod interpreter;
13pub mod lexer;
14pub mod lint;
15pub mod live;
16pub mod migration;
17pub mod module;
18pub mod parser;
19pub mod regex_cache;
20pub mod repl_common;
21pub mod repl_highlight;
22pub mod repl_simple;
23pub mod repl_tui;
24pub mod scaffold;
25pub mod serve;
26pub mod solidb_http;
27pub mod span;
28pub mod template;
29pub mod types;
30pub mod vm;
31
32use ast::expr::Argument;
33use error::SolilangError;
34use interpreter::Value;
35
36/// Run a Solilang program from source code.
37pub fn run(source: &str) -> Result<(), SolilangError> {
38    run_with_options(source, true)
39}
40
41/// Run a Solilang program with optional type checking.
42pub fn run_with_type_check(source: &str, type_check: bool) -> Result<(), SolilangError> {
43    run_with_options(source, type_check)
44}
45
46/// Run a Solilang program with full control over execution options.
47pub fn run_with_options(source: &str, type_check: bool) -> Result<(), SolilangError> {
48    run_with_path(source, None, type_check)
49}
50
51/// Run a Solilang program from a file path with module resolution.
52pub fn run_file(path: &std::path::Path, type_check: bool) -> Result<(), SolilangError> {
53    let source = std::fs::read_to_string(path).map_err(|e| error::RuntimeError::General {
54        message: format!("Failed to read file '{}': {}", path.display(), e),
55        span: span::Span::new(0, 0, 1, 1),
56    })?;
57
58    run_with_path(&source, Some(path), type_check)
59}
60
61/// Run a Solilang program with optional source path for module resolution.
62pub fn run_with_path(
63    source: &str,
64    source_path: Option<&std::path::Path>,
65    type_check: bool,
66) -> Result<(), SolilangError> {
67    // Lexing
68    let tokens = lexer::Scanner::new(source).scan_tokens()?;
69
70    // Parsing
71    let mut program = parser::Parser::new(tokens).parse()?;
72
73    // Module resolution (if we have imports and a source path)
74    if let Some(path) = source_path.filter(|_| has_imports(&program)) {
75        let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
76        let mut resolver = module::ModuleResolver::new(base_dir);
77        program = resolver
78            .resolve(program, path)
79            .map_err(|e| error::RuntimeError::General {
80                message: format!("Module resolution error: {}", e),
81                span: span::Span::new(0, 0, 1, 1),
82            })?;
83    }
84
85    // Type checking (optional)
86    if type_check {
87        let mut checker = types::TypeChecker::new();
88        if let Err(errors) = checker.check(&program) {
89            return Err(errors.into_iter().next().unwrap().into());
90        }
91    }
92
93    // Execute with tree-walking interpreter
94    let mut interpreter = interpreter::Interpreter::new();
95    interpreter.interpret(&program)?;
96
97    Ok(())
98}
99
100/// Run a Solilang program through the bytecode VM (faster execution).
101pub fn run_file_vm(path: &std::path::Path, type_check: bool) -> Result<(), SolilangError> {
102    let source = std::fs::read_to_string(path).map_err(|e| error::RuntimeError::General {
103        message: format!("Failed to read file '{}': {}", path.display(), e),
104        span: span::Span::new(0, 0, 1, 1),
105    })?;
106
107    run_vm(&source, Some(path), type_check)
108}
109
110/// Run a Solilang program through the bytecode VM.
111pub fn run_vm(
112    source: &str,
113    source_path: Option<&std::path::Path>,
114    type_check: bool,
115) -> Result<(), SolilangError> {
116    // Lexing
117    let tokens = lexer::Scanner::new(source).scan_tokens()?;
118
119    // Parsing
120    let mut program = parser::Parser::new(tokens).parse()?;
121
122    // Module resolution
123    if let Some(path) = source_path.filter(|_| has_imports(&program)) {
124        let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
125        let mut resolver = module::ModuleResolver::new(base_dir);
126        program = resolver
127            .resolve(program, path)
128            .map_err(|e| error::RuntimeError::General {
129                message: format!("Module resolution error: {}", e),
130                span: span::Span::new(0, 0, 1, 1),
131            })?;
132    }
133
134    // Type checking
135    if type_check {
136        let mut checker = types::TypeChecker::new();
137        if let Err(errors) = checker.check(&program) {
138            return Err(errors.into_iter().next().unwrap().into());
139        }
140    }
141
142    // Compile to bytecode
143    let module = vm::Compiler::compile(&program).map_err(|e| error::RuntimeError::General {
144        message: format!("Compile error: {}", e),
145        span: span::Span::new(0, 0, 1, 1),
146    })?;
147
148    // Execute in VM
149    let mut vm_instance = vm::Vm::new();
150
151    // Register builtins
152    use interpreter::value::{NativeFunction, Value as V};
153    vm_instance.globals.insert(
154        "print".to_string(),
155        V::NativeFunction(NativeFunction::new("print", None, |args| {
156            let output: Vec<String> = args.iter().map(|a| format!("{}", a)).collect();
157            println!("{}", output.join(" "));
158            Ok(V::Null)
159        })),
160    );
161    vm_instance.globals.insert(
162        "puts".to_string(),
163        V::NativeFunction(NativeFunction::new("puts", None, |args| {
164            let output: Vec<String> = args.iter().map(|a| format!("{}", a)).collect();
165            println!("{}", output.join(" "));
166            Ok(V::Null)
167        })),
168    );
169    vm_instance.globals.insert(
170        "len".to_string(),
171        V::NativeFunction(NativeFunction::new("len", Some(1), |args| match &args[0] {
172            V::String(s) => Ok(V::Int(s.len() as i64)),
173            V::Array(arr) => Ok(V::Int(arr.borrow().len() as i64)),
174            V::Hash(hash) => Ok(V::Int(hash.borrow().len() as i64)),
175            _ => Ok(V::Int(0)),
176        })),
177    );
178    vm_instance.globals.insert(
179        "str".to_string(),
180        V::NativeFunction(NativeFunction::new("str", Some(1), |args| {
181            Ok(V::String(format!("{}", args[0])))
182        })),
183    );
184    vm_instance.globals.insert(
185        "type_of".to_string(),
186        V::NativeFunction(NativeFunction::new("type_of", Some(1), |args| {
187            Ok(V::String(args[0].type_name().to_string()))
188        })),
189    );
190    vm_instance.globals.insert(
191        "clock".to_string(),
192        V::NativeFunction(NativeFunction::new("clock", Some(0), |_args| {
193            use std::time::{SystemTime, UNIX_EPOCH};
194            let now = SystemTime::now()
195                .duration_since(UNIX_EPOCH)
196                .unwrap_or_default();
197            Ok(V::Float(now.as_secs_f64()))
198        })),
199    );
200
201    // Execute the compiled module
202    vm_instance.execute(&module.main)?;
203
204    Ok(())
205}
206
207/// Run a Solilang program with optional coverage tracking.
208#[cfg(feature = "coverage")]
209pub fn run_with_path_and_coverage(
210    source: &str,
211    source_path: Option<&std::path::Path>,
212    type_check: bool,
213    coverage_tracker: Option<&std::rc::Rc<std::cell::RefCell<coverage::CoverageTracker>>>,
214    source_file_path: Option<&std::path::Path>,
215) -> Result<i64, SolilangError> {
216    // Clear any previous test suites
217    interpreter::builtins::test_dsl::clear_test_suites();
218
219    // Lexing
220    let tokens = lexer::Scanner::new(source).scan_tokens()?;
221
222    // Parsing
223    let mut program = parser::Parser::new(tokens).parse()?;
224
225    // Module resolution (if we have imports and a source path)
226    if let Some(path) = source_path.filter(|_| has_imports(&program)) {
227        let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
228        let mut resolver = module::ModuleResolver::new(base_dir);
229        program = resolver
230            .resolve(program, path)
231            .map_err(|e| error::RuntimeError::General {
232                message: format!("Module resolution error: {}", e),
233                span: span::Span::new(0, 0, 1, 1),
234            })?;
235    }
236
237    // Type checking (optional)
238    if type_check {
239        let mut checker = types::TypeChecker::new();
240        if let Err(errors) = checker.check(&program) {
241            return Err(errors.into_iter().next().unwrap().into());
242        }
243    }
244
245    // Extract test definitions from AST
246    let test_suites = extract_test_definitions(&program);
247
248    // Execute with tree-walking interpreter
249    let mut interpreter = interpreter::Interpreter::new();
250    if let (Some(tracker), Some(path)) = (coverage_tracker, source_file_path) {
251        interpreter.set_coverage_tracker(tracker.clone());
252        interpreter.set_source_path(path.to_path_buf());
253    }
254    interpreter.interpret(&program)?;
255
256    // Execute collected tests
257    let (failed_count, failed_tests) = execute_test_suites(&mut interpreter, &test_suites)?;
258
259    // Get assertion count from thread-local storage
260    let assertion_count = interpreter::builtins::assertions::get_and_reset_assertion_count();
261
262    // Return error if any tests failed
263    if failed_count > 0 {
264        let error_msg = if failed_tests.len() == 1 {
265            format!("Test failed: {}", failed_tests[0])
266        } else {
267            format!(
268                "{} tests failed:\n  - {}",
269                failed_count,
270                failed_tests.join("\n  - ")
271            )
272        };
273        return Err(SolilangError::Runtime(error::RuntimeError::General {
274            message: error_msg,
275            span: span::Span::new(0, 0, 1, 1),
276        }));
277    }
278
279    Ok(assertion_count)
280}
281
282/// Run a Solilang program (coverage disabled at compile time).
283#[cfg(not(feature = "coverage"))]
284pub fn run_with_path_and_coverage(
285    source: &str,
286    source_path: Option<&std::path::Path>,
287    type_check: bool,
288    _coverage_tracker: Option<&std::rc::Rc<std::cell::RefCell<coverage::CoverageTracker>>>,
289    source_file_path: Option<&std::path::Path>,
290) -> Result<i64, SolilangError> {
291    // Clear any previous test suites
292    interpreter::builtins::test_dsl::clear_test_suites();
293
294    // Lexing
295    let tokens = lexer::Scanner::new(source).scan_tokens()?;
296
297    // Parsing
298    let mut program = parser::Parser::new(tokens).parse()?;
299
300    // Module resolution (if we have imports and a source path)
301    if let Some(path) = source_path.filter(|_| has_imports(&program)) {
302        let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
303        let mut resolver = module::ModuleResolver::new(base_dir);
304        program = resolver
305            .resolve(program, path)
306            .map_err(|e| error::RuntimeError::General {
307                message: format!("Module resolution error: {}", e),
308                span: span::Span::new(0, 0, 1, 1),
309            })?;
310    }
311
312    // Type checking (optional)
313    if type_check {
314        let mut checker = types::TypeChecker::new();
315        if let Err(errors) = checker.check(&program) {
316            return Err(errors.into_iter().next().unwrap().into());
317        }
318    }
319
320    // Extract test definitions from AST
321    let test_suites = extract_test_definitions(&program);
322
323    // Execute with tree-walking interpreter
324    let mut interpreter = interpreter::Interpreter::new();
325    if let Some(path) = source_file_path {
326        interpreter.set_source_path(path.to_path_buf());
327    }
328    interpreter.interpret(&program)?;
329
330    // Execute collected tests
331    let (failed_count, failed_tests) = execute_test_suites(&mut interpreter, &test_suites)?;
332
333    // Get assertion count from thread-local storage
334    let assertion_count = interpreter::builtins::assertions::get_and_reset_assertion_count();
335
336    // Return error if any tests failed
337    if failed_count > 0 {
338        let error_msg = if failed_tests.len() == 1 {
339            format!("Test failed: {}", failed_tests[0])
340        } else {
341            format!(
342                "{} tests failed:\n  - {}",
343                failed_count,
344                failed_tests.join("\n  - ")
345            )
346        };
347        return Err(SolilangError::Runtime(error::RuntimeError::General {
348            message: error_msg,
349            span: span::Span::new(0, 0, 1, 1),
350        }));
351    }
352
353    Ok(assertion_count)
354}
355
356fn extract_test_definitions(
357    program: &ast::Program,
358) -> Vec<interpreter::builtins::test_dsl::TestSuite> {
359    let mut suites = Vec::new();
360    for stmt in &program.statements {
361        if let ast::StmtKind::Expression(expr) = &stmt.kind {
362            if let ast::ExprKind::Call { callee, arguments } = &expr.kind {
363                // Check if this is a describe call
364                if let ast::ExprKind::Variable(name) = &callee.kind {
365                    if name == "describe" || name == "context" {
366                        if let Some(suite) = extract_suite_from_call(name, arguments, stmt.span) {
367                            suites.push(suite);
368                        }
369                    }
370                }
371            }
372        }
373    }
374    suites
375}
376
377fn extract_suite_from_call(
378    _name: &str,
379    arguments: &[Argument],
380    _span: span::Span,
381) -> Option<interpreter::builtins::test_dsl::TestSuite> {
382    if arguments.len() < 2 {
383        return None;
384    }
385
386    // First argument should be the suite name
387    let first_arg = match &arguments[0] {
388        Argument::Positional(expr) => expr,
389        Argument::Named(_) => return None,
390        Argument::Block(_) => return None,
391    };
392    let suite_name = match &first_arg.kind {
393        ast::ExprKind::StringLiteral(s) => s.clone(),
394        _ => return None,
395    };
396
397    // Second argument should be a lambda (the suite body)
398    let second_arg = match &arguments[1] {
399        Argument::Positional(expr) => expr,
400        Argument::Named(_) => return None,
401        Argument::Block(_) => return None,
402    };
403    let suite_body = match &second_arg.kind {
404        ast::ExprKind::Lambda { body, .. } => body.clone(),
405        _ => return None,
406    };
407
408    let mut suite = interpreter::builtins::test_dsl::TestSuite {
409        name: suite_name,
410        tests: Vec::new(),
411        before_each: None,
412        after_each: None,
413        before_all: None,
414        after_all: None,
415        nested_suites: Vec::new(),
416    };
417
418    // Extract tests and nested suites from the lambda body
419    extract_tests_from_block(&suite_body, &mut suite);
420
421    Some(suite)
422}
423
424fn extract_tests_from_block(
425    statements: &[ast::Stmt],
426    suite: &mut interpreter::builtins::test_dsl::TestSuite,
427) {
428    for stmt in statements {
429        if let ast::StmtKind::Expression(expr) = &stmt.kind {
430            if let ast::ExprKind::Call { callee, arguments } = &expr.kind {
431                if let ast::ExprKind::Variable(name) = &callee.kind {
432                    if name == "test" || name == "it" || name == "specify" {
433                        if let Some(test) = extract_test_from_call(arguments, stmt.span) {
434                            suite.tests.push(test);
435                        }
436                    } else if name == "describe" || name == "context" {
437                        if let Some(nested) = extract_suite_from_call(name, arguments, stmt.span) {
438                            suite.nested_suites.push(nested);
439                        }
440                    } else if name == "before_each" {
441                        if let Some(Argument::Positional(callback)) = arguments.first() {
442                            suite.before_each = Some(ast_expr_to_value(callback));
443                        }
444                    } else if name == "after_each" {
445                        if let Some(Argument::Positional(callback)) = arguments.first() {
446                            suite.after_each = Some(ast_expr_to_value(callback));
447                        }
448                    } else if name == "before_all" {
449                        if let Some(Argument::Positional(callback)) = arguments.first() {
450                            suite.before_all = Some(ast_expr_to_value(callback));
451                        }
452                    } else if name == "after_all" {
453                        if let Some(Argument::Positional(callback)) = arguments.first() {
454                            suite.after_all = Some(ast_expr_to_value(callback));
455                        }
456                    }
457                }
458            }
459        }
460    }
461}
462
463fn extract_test_from_call(
464    arguments: &[Argument],
465    span: span::Span,
466) -> Option<interpreter::builtins::test_dsl::TestDefinition> {
467    if arguments.len() < 2 {
468        return None;
469    }
470
471    let first_arg = match &arguments[0] {
472        Argument::Positional(expr) => expr,
473        Argument::Named(_) => return None,
474        Argument::Block(_) => return None,
475    };
476    let test_name = match &first_arg.kind {
477        ast::ExprKind::StringLiteral(s) => s.clone(),
478        _ => return None,
479    };
480
481    let second_arg = match &arguments[1] {
482        Argument::Positional(expr) => expr,
483        Argument::Named(_) => return None,
484        Argument::Block(_) => return None,
485    };
486    let test_body = match &second_arg.kind {
487        ast::ExprKind::Lambda {
488            params,
489            return_type,
490            body,
491        } => create_function_value(params.clone(), return_type.clone(), body.clone(), span),
492        _ => return None,
493    };
494
495    Some(interpreter::builtins::test_dsl::TestDefinition {
496        name: test_name,
497        body: test_body,
498    })
499}
500
501fn create_function_value(
502    params: Vec<ast::stmt::Parameter>,
503    return_type: Option<ast::types::TypeAnnotation>,
504    body: Vec<ast::Stmt>,
505    span: span::Span,
506) -> Value {
507    use interpreter::value::Function;
508    use std::cell::RefCell;
509    use std::rc::Rc;
510
511    // Create an environment with builtins registered
512    let mut env = interpreter::environment::Environment::new();
513    interpreter::builtins::register_builtins(&mut env, true);
514
515    let decl = ast::FunctionDecl {
516        name: "test_fn".to_string(),
517        params,
518        return_type,
519        body,
520        span,
521    };
522    let closure = Rc::new(RefCell::new(env));
523    Value::Function(Rc::new(Function::from_decl(&decl, closure, None)))
524}
525
526fn ast_expr_to_value(expr: &ast::Expr) -> Value {
527    match &expr.kind {
528        ast::ExprKind::Lambda {
529            params,
530            return_type,
531            body,
532        } => create_function_value(params.clone(), return_type.clone(), body.clone(), expr.span),
533        _ => Value::Null,
534    }
535}
536
537fn execute_test_suites(
538    interpreter: &mut interpreter::Interpreter,
539    suites: &[interpreter::builtins::test_dsl::TestSuite],
540) -> Result<(i64, Vec<String>), error::RuntimeError> {
541    let mut failed_count = 0i64;
542    let mut failed_tests = Vec::new();
543
544    for suite in suites {
545        // Run before_all if defined
546        if let Some(before_all) = &suite.before_all {
547            let rebound = rebind_closure(before_all, &interpreter.environment);
548            let _ = interpreter.call_value(rebound, Vec::new(), span::Span::new(0, 0, 1, 1));
549        }
550
551        for test in &suite.tests {
552            // Run before_each if defined
553            if let Some(before_each) = &suite.before_each {
554                let rebound = rebind_closure(before_each, &interpreter.environment);
555                let _ = interpreter.call_value(rebound, Vec::new(), span::Span::new(0, 0, 1, 1));
556            }
557
558            // Rebind test body closure to interpreter's environment so
559            // top-level `def` functions (e.g. register_test_user) are accessible.
560            let test_body = rebind_closure(&test.body, &interpreter.environment);
561
562            // Execute the test body and track failures
563            let result = interpreter.call_value(test_body, Vec::new(), span::Span::new(0, 0, 1, 1));
564
565            if let Err(e) = result {
566                failed_count += 1;
567                failed_tests.push(format!("{}: {}", test.name, e));
568            }
569
570            // Run after_each if defined
571            if let Some(after_each) = &suite.after_each {
572                let rebound = rebind_closure(after_each, &interpreter.environment);
573                let _ = interpreter.call_value(rebound, Vec::new(), span::Span::new(0, 0, 1, 1));
574            }
575        }
576
577        // Run nested suites
578        let (nested_failed, mut nested_errors) =
579            execute_test_suites(interpreter, &suite.nested_suites)?;
580        failed_count += nested_failed;
581        failed_tests.append(&mut nested_errors);
582
583        // Run after_all if defined
584        if let Some(after_all) = &suite.after_all {
585            let rebound = rebind_closure(after_all, &interpreter.environment);
586            let _ = interpreter.call_value(rebound, Vec::new(), span::Span::new(0, 0, 1, 1));
587        }
588    }
589    Ok((failed_count, failed_tests))
590}
591
592/// Rebind a test function's closure to use the interpreter's environment,
593/// so that top-level definitions (def, let) are accessible inside tests.
594fn rebind_closure(
595    value: &interpreter::value::Value,
596    env: &std::rc::Rc<std::cell::RefCell<interpreter::environment::Environment>>,
597) -> interpreter::value::Value {
598    use interpreter::value::{Function, Value};
599    match value {
600        Value::Function(func) => {
601            let mut new_func = Function {
602                name: func.name.clone(),
603                params: func.params.clone(),
604                body: func.body.clone(),
605                closure: env.clone(),
606                is_method: func.is_method,
607                span: func.span,
608                source_path: func.source_path.clone(),
609                defining_superclass: func.defining_superclass.clone(),
610                return_type: func.return_type.clone(),
611            };
612            new_func.closure = env.clone();
613            Value::Function(std::rc::Rc::new(new_func))
614        }
615        other => other.clone(),
616    }
617}
618
619/// Check if a program has any import statements.
620pub(crate) fn has_imports(program: &ast::Program) -> bool {
621    program
622        .statements
623        .iter()
624        .any(|stmt| matches!(stmt.kind, ast::StmtKind::Import(_)))
625}
626
627/// Parse source code into an AST without executing.
628pub fn parse(source: &str) -> Result<ast::Program, SolilangError> {
629    let tokens = lexer::Scanner::new(source).scan_tokens()?;
630    let program = parser::Parser::new(tokens).parse()?;
631    Ok(program)
632}
633
634/// Lint source code and return diagnostics.
635pub fn lint(source: &str) -> Result<Vec<lint::LintDiagnostic>, SolilangError> {
636    let tokens = lexer::Scanner::new(source).scan_tokens()?;
637    let program = parser::Parser::new(tokens).parse()?;
638    Ok(lint::Linter::new(source).lint(&program))
639}
640
641/// Type check a program without executing.
642pub fn type_check(source: &str) -> Result<(), Vec<error::TypeError>> {
643    let tokens = lexer::Scanner::new(source)
644        .scan_tokens()
645        .map_err(|_| Vec::new())?;
646    let program = parser::Parser::new(tokens)
647        .parse()
648        .map_err(|_| Vec::new())?;
649
650    let mut checker = types::TypeChecker::new();
651    checker.check(&program)
652}