frut_lib/
lib.rs

1//! # Frut Library
2//!
3//! A no_std library for parsing, analyzing, and executing the Frut programming language.
4//! This library provides the core functionality for lexing, parsing, semantic analysis,
5//! and interpretation/compilation of Frut code.
6//!
7//! ## Features
8//!
9//! - No-std compatible (uses only core and alloc)
10//! - Modular architecture for extensibility
11//! - Support for both interpretation and compilation workflows in the future
12
13#![cfg_attr(not(feature = "std"), no_std)]
14
15#![no_std]
16#![no_main]
17
18#[macro_use]
19extern crate alloc;
20
21// Re-export main modules for easy access
22pub mod ast;
23pub mod errors;
24pub mod lexer;
25pub mod parser;
26pub mod semantic;
27pub mod types;
28pub mod value;
29pub mod primitives;
30
31// Core types and functions
32pub use ast::*;
33pub use errors::*;
34pub use lexer::*;
35pub use parser::*;
36pub use semantic::*;
37pub use types::*;
38pub use value::*;
39pub use primitives::*;
40use crate::alloc::*;
41
42#[cfg(not(feature = "std"))]
43use linked_list_allocator::LockedHeap;
44
45use alloc::string::{String, ToString};
46use alloc::vec::Vec;
47use core::hash::BuildHasherDefault;
48
49#[cfg(not(feature = "std"))]
50use core::panic::PanicInfo;
51
52#[cfg(not(feature = "std"))]
53#[panic_handler]
54fn panic(_info: &PanicInfo) -> ! {
55    loop {}
56}
57
58#[allow(deprecated)]
59pub type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<core::hash::SipHasher>>;
60
61/// Represents a source file provided to the core (can be in-memory or from disk)
62#[derive(Debug, Clone)]
63pub struct File {
64    pub path: String,
65    pub code: String,
66    pub ast: Option<Vec<Statement>>, // NOTE: filled after parsing
67}
68
69/// A parsed project consisting of multiple files
70#[derive(Debug, Clone)]
71pub struct Project {
72    pub files: Vec<File>,
73}
74
75/// Result of parsing multiple files
76#[derive(Debug, Clone)]
77pub struct ParseProjectResult {
78    pub project: Project,
79    pub errors: ErrorCollection,
80}
81
82impl ParseProjectResult {
83    pub fn is_success(&self) -> bool { self.errors.is_empty() }
84}
85
86/// Parse multiple files and return a project with per-file ASTs and aggregated errors
87pub fn parse_files(mut files: Vec<File>) -> ParseProjectResult {
88    let mut all_errors = ErrorCollection::new();
89
90    for file in files.iter_mut() {
91        let res = parse_code(&file.code, file.path.clone());
92        if res.is_success() {
93            file.ast = Some(res.statements);
94        } else {
95            for e in res.errors.errors.iter() {
96                all_errors.add_error(e.clone());
97            }
98            file.ast = None;
99        }
100    }
101
102    ParseProjectResult { project: Project { files }, errors: all_errors }
103}
104
105/// Run semantic analysis across all files in the project.
106pub fn analyze_project(project: &mut Project) -> core::result::Result<(), ErrorCollection> {
107    fn extract_code_snippet(code: &str, line: usize) -> String {
108        if line == 0 { return String::new(); }
109        let lines: Vec<&str> = code.lines().collect();
110        if line > 0 && line <= lines.len() { lines[line - 1].to_string() } else { String::new() }
111    }
112
113    let mut all_errors = ErrorCollection::new();
114
115    use alloc::collections::BTreeMap;
116    let mut module_funcs: BTreeMap<String, Vec<(String, Vec<Type>, Type)>> = BTreeMap::new();
117    let mut module_vars: BTreeMap<String, Vec<(String, Type)>> = BTreeMap::new();
118
119    for file in project.files.iter() {
120        if let Some(ref ast) = file.ast {
121            let mut sigs: Vec<(String, Vec<Type>, Type)> = Vec::new();
122            let mut vars: Vec<(String, Type)> = Vec::new();
123            for stmt in ast.iter() {
124                if let StatementKind::FunctionDeclaration { name, params, return_type, .. } = &stmt.kind {
125                    let param_types = params.iter().map(|p| Type::from(p.param_type.as_str())).collect::<Vec<_>>();
126                    let ret = Type::from(return_type.as_str());
127                    sigs.push((name.clone(), param_types, ret));
128                } else if let StatementKind::VariableDeclaration { name, var_type, .. } = &stmt.kind {
129                    vars.push((name.clone(), Type::from(var_type.as_str())));
130                }
131            }
132            module_funcs.insert(file.path.clone(), sigs);
133            module_vars.insert(file.path.clone(), vars);
134        }
135    }
136
137    fn resolve_module_file<'a>(project: &'a Project, path_segments: &[String]) -> Option<&'a File> {
138        let joined = path_segments.join("/");
139        let needle = joined.as_str();
140        for f in project.files.iter() {
141            if f.path.ends_with(&format!("/{}.ft", joined)) || f.path.ends_with(&format!("{}{}.ft", if joined.is_empty() { "" } else { "/" }, joined)) {
142                return Some(f);
143            }
144            if let Some(file_name) = f.path.rsplit('/').next() {
145                let stem = file_name.strip_suffix(".ft").unwrap_or(file_name);
146                if stem == needle || stem == path_segments.last().map(|s| s.as_str()).unwrap_or(needle) {
147                    return Some(f);
148                }
149            }
150        }
151        None
152    }
153
154    for file in project.files.iter() {
155        if let Some(ref ast) = file.ast {
156            let mut analyzer = SemanticAnalyzer::new(file.path.clone(), file.code.clone());
157
158            for stmt in ast.iter() {
159                if let StatementKind::Import { path, kind } = &stmt.kind {
160                    let target = resolve_module_file(project, path);
161                    if target.is_none() {
162                        let pos = &stmt.pos;
163                        all_errors.add_error(ErrorReport::with_file(
164                            ErrorType::SyntaxError,
165                            format!("Module not found: {}", path.join(".")),
166                            file.path.clone(),
167                            pos.line,
168                            pos.column,
169                            pos.offset,
170                            pos.length,
171                            extract_code_snippet(&file.code, pos.line),
172                        ));
173                        continue;
174                    }
175                    let target = target.unwrap();
176                    let funcs_opt = module_funcs.get(&target.path);
177                    let vars_opt = module_vars.get(&target.path);
178                    match kind {
179                        ImportKind::Wildcard => {
180                            if let Some(funcs) = funcs_opt {
181                                for (fname, params, ret) in funcs.iter() {
182                                    let _ = analyzer.predeclare_function(fname.clone(), params.clone(), ret.clone());
183                                }
184                            }
185                            if let Some(vars) = vars_opt {
186                                for (vname, vtype) in vars.iter() {
187                                    let _ = analyzer.predeclare_initialized_variable(vname.clone(), vtype.clone());
188                                }
189                            }
190                        }
191                        ImportKind::Single(name) => {
192                            let mut found = false;
193                            if let Some(funcs) = funcs_opt {
194                                if let Some((_, params, ret)) = funcs.iter().find(|(n, _, _)| n == name) {
195                                    let _ = analyzer.predeclare_function(name.clone(), params.clone(), ret.clone());
196                                    found = true;
197                                }
198                            }
199                            if !found {
200                                if let Some(vars) = vars_opt {
201                                    if let Some((_, vtype)) = vars.iter().find(|(n, _)| n == name) {
202                                        let _ = analyzer.predeclare_initialized_variable(name.clone(), vtype.clone());
203                                        found = true;
204                                    }
205                                }
206                            }
207                            if !found {
208                                let pos = &stmt.pos;
209                                all_errors.add_error(ErrorReport::with_file(
210                                    ErrorType::UndefinedVariable(name.clone()),
211                                    format!("Symbol '{}' not found in module {}", name, path.join(".")),
212                                    file.path.clone(),
213                                    pos.line,
214                                    pos.column,
215                                    pos.offset,
216                                    pos.length,
217                                    extract_code_snippet(&file.code, pos.line),
218                                ));
219                            }
220                        }
221                        ImportKind::Group(names) => {
222                            for name in names.iter() {
223                                let mut found = false;
224                                if let Some(funcs) = funcs_opt {
225                                    if let Some((_, params, ret)) = funcs.iter().find(|(n, _, _)| n == name) {
226                                        let _ = analyzer.predeclare_function(name.clone(), params.clone(), ret.clone());
227                                        found = true;
228                                    }
229                                }
230                                if !found {
231                                    if let Some(vars) = vars_opt {
232                                        if let Some((_, vtype)) = vars.iter().find(|(n, _)| n == name) {
233                                            let _ = analyzer.predeclare_initialized_variable(name.clone(), vtype.clone());
234                                            found = true;
235                                        }
236                                    }
237                                }
238                                if !found {
239                                    let pos = &stmt.pos;
240                                    all_errors.add_error(ErrorReport::with_file(
241                                        ErrorType::UndefinedVariable(name.clone()),
242                                        format!("Symbol '{}' not found in module {}", name, path.join(".")),
243                                        file.path.clone(),
244                                        pos.line,
245                                        pos.column,
246                                        pos.offset,
247                                        pos.length,
248                                        extract_code_snippet(&file.code, pos.line),
249                                    ));
250                                }
251                            }
252                        }
253                    }
254                }
255            }
256
257            if let Err(errors) = analyzer.analyze(ast) {
258                for e in errors.errors.iter() {
259                    all_errors.add_error(e.clone());
260                }
261            }
262        }
263    }
264
265    if all_errors.is_empty() { Ok(()) } else { Err(all_errors) }
266}
267
268/// Simple parsing code
269pub fn parse_code(input: &str, filename: String) -> ParseResult {
270    #[cfg(not(feature = "std"))]
271    unsafe { ALLOCATOR.lock().init(HEAP_START.as_mut_ptr(), HEAP_SIZE); }
272    
273    let mut lexer = Lexer::new(input, filename.clone());
274    let tokens = lexer.tokenize();
275
276    let mut parser = Parser::new(tokens, input.to_string(), filename);
277    parser.parse()
278}
279
280/// Version information
281pub const VERSION: &str = env!("CARGO_PKG_VERSION");
282
283/// TODO: um i guess check no-std lol
284#[cfg(not(feature = "std"))]
285const HEAP_SIZE: usize = 16 * 1024;
286
287#[cfg(not(feature = "std"))]
288static mut HEAP_START: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
289
290#[cfg(not(feature = "std"))]
291#[global_allocator]
292static ALLOCATOR: LockedHeap = LockedHeap::empty();