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
9// Allow some clippy lints that are stylistic and not critical
10#![allow(clippy::module_inception)]
11#![allow(clippy::result_large_err)]
12#![allow(clippy::arc_with_non_send_sync)]
13#![allow(clippy::only_used_in_recursion)]
14#![allow(clippy::type_complexity)]
15#![allow(clippy::ptr_arg)]
16#![allow(clippy::new_without_default)]
17#![allow(clippy::collapsible_if)]
18#![allow(clippy::collapsible_else_if)]
19#![allow(clippy::collapsible_match)]
20#![allow(clippy::derivable_impls)]
21#![allow(clippy::unnecessary_cast)]
22#![allow(clippy::needless_borrow)]
23#![allow(clippy::wildcard_in_or_patterns)]
24#![allow(clippy::needless_borrows_for_generic_args)]
25#![allow(clippy::unnecessary_lazy_evaluations)]
26#![allow(clippy::len_zero)]
27#![allow(clippy::redundant_pattern_matching)]
28#![allow(clippy::trim_split_whitespace)]
29#![allow(clippy::to_string_in_format_args)]
30#![allow(clippy::while_let_on_iterator)]
31#![allow(clippy::manual_ok_err)]
32#![allow(clippy::unwrap_or_default)]
33#![allow(clippy::unnecessary_filter_map)]
34#![allow(clippy::manual_clamp)]
35#![allow(clippy::redundant_closure)]
36#![allow(clippy::unused_enumerate_index)]
37#![allow(clippy::too_many_arguments)]
38#![allow(clippy::let_underscore_future)]
39#![allow(clippy::double_ended_iterator_last)]
40#![allow(clippy::needless_late_init)]
41#![allow(clippy::manual_strip)]
42#![allow(clippy::never_loop)]
43
44pub mod ast;
45pub mod coverage;
46pub mod error;
47pub mod interpreter;
48pub mod lexer;
49pub mod live;
50pub mod migration;
51pub mod module;
52pub mod parser;
53pub mod scaffold;
54pub mod serve;
55pub mod solidb_http;
56pub mod span;
57pub mod template;
58pub mod types;
59
60use ast::expr::Argument;
61use error::SolilangError;
62use interpreter::Value;
63
64/// Run a Solilang program from source code.
65pub fn run(source: &str) -> Result<(), SolilangError> {
66    run_with_options(source, true)
67}
68
69/// Run a Solilang program with optional type checking.
70pub fn run_with_type_check(source: &str, type_check: bool) -> Result<(), SolilangError> {
71    run_with_options(source, type_check)
72}
73
74/// Run a Solilang program with full control over execution options.
75pub fn run_with_options(
76    source: &str,
77    type_check: bool,
78) -> Result<(), SolilangError> {
79    run_with_path(source, None, type_check)
80}
81
82/// Run a Solilang program from a file path with module resolution.
83pub fn run_file(
84    path: &std::path::Path,
85    type_check: bool,
86) -> Result<(), SolilangError> {
87    let source = std::fs::read_to_string(path).map_err(|e| error::RuntimeError::General {
88        message: format!("Failed to read file '{}': {}", path.display(), e),
89        span: span::Span::new(0, 0, 1, 1),
90    })?;
91
92    run_with_path(&source, Some(path), type_check)
93}
94
95/// Run a Solilang program with optional source path for module resolution.
96pub fn run_with_path(
97    source: &str,
98    source_path: Option<&std::path::Path>,
99    type_check: bool,
100) -> Result<(), SolilangError> {
101    // Lexing
102    let tokens = lexer::Scanner::new(source).scan_tokens()?;
103
104    // Parsing
105    let mut program = parser::Parser::new(tokens).parse()?;
106
107    // Module resolution (if we have imports and a source path)
108    if let Some(path) = source_path.filter(|_| has_imports(&program)) {
109        let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
110        let mut resolver = module::ModuleResolver::new(base_dir);
111        program = resolver
112            .resolve(program, path)
113            .map_err(|e| error::RuntimeError::General {
114                message: format!("Module resolution error: {}", e),
115                span: span::Span::new(0, 0, 1, 1),
116            })?;
117    }
118
119    // Type checking (optional)
120    if type_check {
121        let mut checker = types::TypeChecker::new();
122        if let Err(errors) = checker.check(&program) {
123            return Err(errors.into_iter().next().unwrap().into());
124        }
125    }
126
127    // Execute with tree-walking interpreter
128    let mut interpreter = interpreter::Interpreter::new();
129    interpreter.interpret(&program)?;
130
131    Ok(())
132}
133
134/// Run a Solilang program with optional coverage tracking.
135#[cfg(feature = "coverage")]
136pub fn run_with_path_and_coverage(
137    source: &str,
138    source_path: Option<&std::path::Path>,
139    type_check: bool,
140    coverage_tracker: Option<&std::rc::Rc<std::cell::RefCell<coverage::CoverageTracker>>>,
141    source_file_path: Option<&std::path::Path>,
142) -> Result<i64, SolilangError> {
143    // Clear any previous test suites
144    interpreter::builtins::test_dsl::clear_test_suites();
145
146    // Lexing
147    let tokens = lexer::Scanner::new(source).scan_tokens()?;
148
149    // Parsing
150    let mut program = parser::Parser::new(tokens).parse()?;
151
152    // Module resolution (if we have imports and a source path)
153    if let Some(path) = source_path.filter(|_| has_imports(&program)) {
154        let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
155        let mut resolver = module::ModuleResolver::new(base_dir);
156        program = resolver
157            .resolve(program, path)
158            .map_err(|e| error::RuntimeError::General {
159                message: format!("Module resolution error: {}", e),
160                span: span::Span::new(0, 0, 1, 1),
161            })?;
162    }
163
164    // Type checking (optional)
165    if type_check {
166        let mut checker = types::TypeChecker::new();
167        if let Err(errors) = checker.check(&program) {
168            return Err(errors.into_iter().next().unwrap().into());
169        }
170    }
171
172    // Extract test definitions from AST
173    let test_suites = extract_test_definitions(&program);
174
175    // Execute with tree-walking interpreter
176    let mut interpreter = interpreter::Interpreter::new();
177    if let (Some(tracker), Some(path)) = (coverage_tracker, source_file_path) {
178        interpreter.set_coverage_tracker(tracker.clone());
179        interpreter.set_source_path(path.to_path_buf());
180    }
181    interpreter.interpret(&program)?;
182
183    // Execute collected tests
184    let (failed_count, failed_tests) = execute_test_suites(&mut interpreter, &test_suites)?;
185
186    // Get assertion count from thread-local storage
187    let assertion_count = interpreter::builtins::assertions::get_and_reset_assertion_count();
188
189    // Return error if any tests failed
190    if failed_count > 0 {
191        let error_msg = if failed_tests.len() == 1 {
192            format!("Test failed: {}", failed_tests[0])
193        } else {
194            format!("{} tests failed:\n  - {}", failed_count, failed_tests.join("\n  - "))
195        };
196        return Err(SolilangError::Runtime(error::RuntimeError::General {
197            message: error_msg,
198            span: span::Span::new(0, 0, 1, 1),
199        }));
200    }
201
202    Ok(assertion_count)
203}
204
205/// Run a Solilang program (coverage disabled at compile time).
206#[cfg(not(feature = "coverage"))]
207pub fn run_with_path_and_coverage(
208    source: &str,
209    source_path: Option<&std::path::Path>,
210    type_check: bool,
211    _coverage_tracker: Option<&std::rc::Rc<std::cell::RefCell<coverage::CoverageTracker>>>,
212    source_file_path: Option<&std::path::Path>,
213) -> Result<i64, SolilangError> {
214    // Clear any previous test suites
215    interpreter::builtins::test_dsl::clear_test_suites();
216
217    // Lexing
218    let tokens = lexer::Scanner::new(source).scan_tokens()?;
219
220    // Parsing
221    let mut program = parser::Parser::new(tokens).parse()?;
222
223    // Module resolution (if we have imports and a source path)
224    if let Some(path) = source_path.filter(|_| has_imports(&program)) {
225        let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
226        let mut resolver = module::ModuleResolver::new(base_dir);
227        program = resolver
228            .resolve(program, path)
229            .map_err(|e| error::RuntimeError::General {
230                message: format!("Module resolution error: {}", e),
231                span: span::Span::new(0, 0, 1, 1),
232            })?;
233    }
234
235    // Type checking (optional)
236    if type_check {
237        let mut checker = types::TypeChecker::new();
238        if let Err(errors) = checker.check(&program) {
239            return Err(errors.into_iter().next().unwrap().into());
240        }
241    }
242
243    // Extract test definitions from AST
244    let test_suites = extract_test_definitions(&program);
245
246    // Execute with tree-walking interpreter
247    let mut interpreter = interpreter::Interpreter::new();
248    if let Some(path) = source_file_path {
249        interpreter.set_source_path(path.to_path_buf());
250    }
251    interpreter.interpret(&program)?;
252
253    // Execute collected tests
254    let (failed_count, failed_tests) = execute_test_suites(&mut interpreter, &test_suites)?;
255
256    // Get assertion count from thread-local storage
257    let assertion_count = interpreter::builtins::assertions::get_and_reset_assertion_count();
258
259    // Return error if any tests failed
260    if failed_count > 0 {
261        let error_msg = if failed_tests.len() == 1 {
262            format!("Test failed: {}", failed_tests[0])
263        } else {
264            format!("{} tests failed:\n  - {}", failed_count, failed_tests.join("\n  - "))
265        };
266        return Err(SolilangError::Runtime(error::RuntimeError::General {
267            message: error_msg,
268            span: span::Span::new(0, 0, 1, 1),
269        }));
270    }
271
272    Ok(assertion_count)
273}
274
275fn extract_test_definitions(program: &ast::Program) -> Vec<interpreter::builtins::test_dsl::TestSuite> {
276    let mut suites = Vec::new();
277    for stmt in &program.statements {
278        if let ast::StmtKind::Expression(expr) = &stmt.kind {
279            if let ast::ExprKind::Call { callee, arguments } = &expr.kind {
280                // Check if this is a describe call
281                if let ast::ExprKind::Variable(name) = &callee.kind {
282                    if name == "describe" || name == "context" {
283                        if let Some(suite) = extract_suite_from_call(name, arguments, stmt.span) {
284                            suites.push(suite);
285                        }
286                    }
287                }
288            }
289        }
290    }
291    suites
292}
293
294fn extract_suite_from_call(
295    _name: &str,
296    arguments: &[Argument],
297    _span: span::Span,
298) -> Option<interpreter::builtins::test_dsl::TestSuite> {
299    if arguments.len() < 2 {
300        return None;
301    }
302
303    // First argument should be the suite name
304    let first_arg = match &arguments[0] {
305        Argument::Positional(expr) => expr,
306        Argument::Named(_) => return None,
307    };
308    let suite_name = match &first_arg.kind {
309        ast::ExprKind::StringLiteral(s) => s.clone(),
310        _ => return None,
311    };
312
313    // Second argument should be a lambda (the suite body)
314    let second_arg = match &arguments[1] {
315        Argument::Positional(expr) => expr,
316        Argument::Named(_) => return None,
317    };
318    let suite_body = match &second_arg.kind {
319        ast::ExprKind::Lambda { body, .. } => body.clone(),
320        _ => return None,
321    };
322
323    let mut suite = interpreter::builtins::test_dsl::TestSuite {
324        name: suite_name,
325        tests: Vec::new(),
326        before_each: None,
327        after_each: None,
328        before_all: None,
329        after_all: None,
330        nested_suites: Vec::new(),
331    };
332
333    // Extract tests and nested suites from the lambda body
334    extract_tests_from_block(&suite_body, &mut suite);
335
336    Some(suite)
337}
338
339fn extract_tests_from_block(
340    statements: &[ast::Stmt],
341    suite: &mut interpreter::builtins::test_dsl::TestSuite,
342) {
343    for stmt in statements {
344        if let ast::StmtKind::Expression(expr) = &stmt.kind {
345            if let ast::ExprKind::Call { callee, arguments } = &expr.kind {
346                if let ast::ExprKind::Variable(name) = &callee.kind {
347                        if name == "test" || name == "it" || name == "specify" {
348                            if let Some(test) = extract_test_from_call(arguments, stmt.span) {
349                                suite.tests.push(test);
350                            }
351                        } else if name == "describe" || name == "context" {
352                            if let Some(nested) = extract_suite_from_call(name, arguments, stmt.span) {
353                                suite.nested_suites.push(nested);
354                            }
355                        } else if name == "before_each" {
356                            if let Some(Argument::Positional(callback)) = arguments.first() {
357                                suite.before_each = Some(ast_expr_to_value(callback));
358                            }
359                        } else if name == "after_each" {
360                            if let Some(Argument::Positional(callback)) = arguments.first() {
361                                suite.after_each = Some(ast_expr_to_value(callback));
362                            }
363                        } else if name == "before_all" {
364                            if let Some(Argument::Positional(callback)) = arguments.first() {
365                                suite.before_all = Some(ast_expr_to_value(callback));
366                            }
367                        } else if name == "after_all" {
368                            if let Some(Argument::Positional(callback)) = arguments.first() {
369                                suite.after_all = Some(ast_expr_to_value(callback));
370                            }
371                        }
372                }
373            }
374        }
375    }
376}
377
378fn extract_test_from_call(
379    arguments: &[Argument],
380    span: span::Span,
381) -> Option<interpreter::builtins::test_dsl::TestDefinition> {
382    if arguments.len() < 2 {
383        return None;
384    }
385
386    let first_arg = match &arguments[0] {
387        Argument::Positional(expr) => expr,
388        Argument::Named(_) => return None,
389    };
390    let test_name = match &first_arg.kind {
391        ast::ExprKind::StringLiteral(s) => s.clone(),
392        _ => return None,
393    };
394
395    let second_arg = match &arguments[1] {
396        Argument::Positional(expr) => expr,
397        Argument::Named(_) => return None,
398    };
399    let test_body = match &second_arg.kind {
400        ast::ExprKind::Lambda { params, return_type, body } => {
401            create_function_value(params.clone(), return_type.clone(), body.clone(), span)
402        }
403        _ => return None,
404    };
405
406    Some(interpreter::builtins::test_dsl::TestDefinition {
407        name: test_name,
408        body: test_body,
409    })
410}
411
412fn create_function_value(
413    params: Vec<ast::stmt::Parameter>,
414    return_type: Option<ast::types::TypeAnnotation>,
415    body: Vec<ast::Stmt>,
416    span: span::Span,
417) -> Value {
418    use interpreter::value::Function;
419    use std::rc::Rc;
420    use std::cell::RefCell;
421
422    // Create an environment with builtins registered
423    let mut env = interpreter::environment::Environment::new();
424    interpreter::builtins::register_builtins(&mut env);
425
426    let decl = ast::FunctionDecl {
427        name: "test_fn".to_string(),
428        params,
429        return_type,
430        body,
431        span,
432    };
433    let closure = Rc::new(RefCell::new(env));
434    Value::Function(Rc::new(Function::from_decl(&decl, closure, None)))
435}
436
437fn ast_expr_to_value(expr: &ast::Expr) -> Value {
438    match &expr.kind {
439        ast::ExprKind::Lambda { params, return_type, body } => {
440            create_function_value(params.clone(), return_type.clone(), body.clone(), expr.span)
441        }
442        _ => Value::Null,
443    }
444}
445
446fn execute_test_suites(
447    interpreter: &mut interpreter::Interpreter,
448    suites: &[interpreter::builtins::test_dsl::TestSuite],
449) -> Result<(i64, Vec<String>), error::RuntimeError> {
450    let mut failed_count = 0i64;
451    let mut failed_tests = Vec::new();
452
453    for suite in suites {
454        // Run before_all if defined
455        if let Some(before_all) = &suite.before_all {
456            let _ = interpreter.call_value(before_all.clone(), Vec::new(), span::Span::new(0, 0, 1, 1));
457        }
458
459        for test in &suite.tests {
460            // Run before_each if defined
461            if let Some(before_each) = &suite.before_each {
462                let _ = interpreter.call_value(before_each.clone(), Vec::new(), span::Span::new(0, 0, 1, 1));
463            }
464
465            // Execute the test body and track failures
466            let result = interpreter.call_value(
467                test.body.clone(),
468                Vec::new(),
469                span::Span::new(0, 0, 1, 1),
470            );
471
472            if let Err(e) = result {
473                failed_count += 1;
474                failed_tests.push(format!("{}: {}", test.name, e));
475            }
476
477            // Run after_each if defined
478            if let Some(after_each) = &suite.after_each {
479                let _ = interpreter.call_value(after_each.clone(), Vec::new(), span::Span::new(0, 0, 1, 1));
480            }
481        }
482
483        // Run nested suites
484        let (nested_failed, mut nested_errors) = execute_test_suites(interpreter, &suite.nested_suites)?;
485        failed_count += nested_failed;
486        failed_tests.append(&mut nested_errors);
487
488        // Run after_all if defined
489        if let Some(after_all) = &suite.after_all {
490            let _ = interpreter.call_value(after_all.clone(), Vec::new(), span::Span::new(0, 0, 1, 1));
491        }
492    }
493    Ok((failed_count, failed_tests))
494}
495
496/// Check if a program has any import statements.
497pub(crate) fn has_imports(program: &ast::Program) -> bool {
498    program
499        .statements
500        .iter()
501        .any(|stmt| matches!(stmt.kind, ast::StmtKind::Import(_)))
502}
503
504/// Parse source code into an AST without executing.
505pub fn parse(source: &str) -> Result<ast::Program, SolilangError> {
506    let tokens = lexer::Scanner::new(source).scan_tokens()?;
507    let program = parser::Parser::new(tokens).parse()?;
508    Ok(program)
509}
510
511/// Type check a program without executing.
512pub fn type_check(source: &str) -> Result<(), Vec<error::TypeError>> {
513    let tokens = lexer::Scanner::new(source)
514        .scan_tokens()
515        .map_err(|_| Vec::new())?;
516    let program = parser::Parser::new(tokens)
517        .parse()
518        .map_err(|_| Vec::new())?;
519
520    let mut checker = types::TypeChecker::new();
521    checker.check(&program)
522}