From 40e612af406215a15d18dc3aff1b6283e8adf3b7 Mon Sep 17 00:00:00 2001 From: tascord Date: Sun, 1 Mar 2026 13:50:50 +1100 Subject: [PATCH 1/6] builtin macros, fix clippy+tests, coverage checking --- .github/workflows/build.yml | 12 +- Cargo.lock | 4 +- Cargo.toml | 2 +- TODO.md | 73 +++--- loft_builtin_macros/Cargo.toml | 2 +- loft_builtin_macros/src/lib.rs | 123 +++++++-- registry/Cargo.toml | 2 +- scripts/check_stdlib_coverage.js | 75 ++++++ scripts/gen_example_tests.sh | 1 + scripts/release.sh | 2 +- src/docgen/mod.rs | 33 +-- src/docgen/terminal.rs | 2 +- src/formatter/mod.rs | 6 + src/formatter/token_formatter.rs | 19 +- src/lsp/mod.rs | 296 ++++++++++++---------- src/lsp/stdlib_types.json | 81 +++++- src/main.rs | 96 +++---- src/manifest.rs | 4 +- src/parser/mod.rs | 45 +--- src/parser/tests.rs | 4 +- src/parser/token_stream.rs | 13 +- src/runtime/builtins/array.rs | 29 +-- src/runtime/builtins/collections/array.rs | 2 +- src/runtime/builtins/encoding.rs | 28 +- src/runtime/builtins/ffi.rs | 8 +- src/runtime/builtins/io/fs.rs | 72 ++---- src/runtime/builtins/json.rs | 28 +- src/runtime/builtins/math/basic.rs | 42 +-- src/runtime/builtins/math/exponential.rs | 32 +-- src/runtime/builtins/math/trigonometry.rs | 36 +-- src/runtime/builtins/object.rs | 90 ++----- src/runtime/builtins/random.rs | 22 +- src/runtime/builtins/string/mod.rs | 28 +- src/runtime/builtins/term.rs | 2 +- src/runtime/builtins/test.rs | 10 +- src/runtime/builtins/time.rs | 34 +-- src/runtime/builtins/web/mod.rs | 2 +- src/runtime/mod.rs | 52 ++-- 38 files changed, 740 insertions(+), 672 deletions(-) create mode 100644 scripts/check_stdlib_coverage.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 277a692..b7203c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -247,4 +247,14 @@ jobs: uses: actions/checkout@v4 - name: Verify tests/examples.rs matches script output - run: bash scripts/gen_example_tests.sh --check \ No newline at end of file + run: bash scripts/gen_example_tests.sh --check + + check-stdlib-coverage: + name: Check Stdlib Documentation Coverage + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run stdlib coverage check + run: node scripts/check_stdlib_coverage.js \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e7f05d0..898a112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "loft" -version = "0.1.0-rc-4.2" +version = "0.1.0-rc-5" dependencies = [ "async-trait", "atty", @@ -1086,7 +1086,7 @@ dependencies = [ [[package]] name = "loft_builtin_macros" -version = "0.1.0-rc-4.2" +version = "0.1.0-rc-5" dependencies = [ "inventory", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index c544ca1..476c65a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loft" -version = "0.1.0-rc-4.2" +version = "0.1.0-rc-5" edition = "2021" [[bin]] diff --git a/TODO.md b/TODO.md index 2d8a96c..b592726 100644 --- a/TODO.md +++ b/TODO.md @@ -1,46 +1,43 @@ # loft Language Development TODO -Last Updated: December 20, 2024 - -### Future Work -- Performance benchmarks not yet defined -- Advanced async/await patterns -- Generic trait implementations -- Const generics - -## Technical Debt +Last Updated: March 1, 2026 + +## ✅ Completed Items +- **Parser Error Recovery**: Basic `synchronize` mechanism implemented for statement-level recovery. +- **Async/Await**: Basic `async` and `await` keywords implemented with `Promise` value type. +- **Semantic Tokens**: Basic semantic tokens legend and provider implemented in LSP. +- **FFI Interface**: Support for loading shared libraries and calling functions with numeric arguments. +- **String Interpolation**: `${}` syntax implemented in `TokenStream`. +- **JSON Handling**: basic `json.parse()` and `json.stringify()` (via `to_string`) implemented. + +## 🚀 Future Work +- **Performance Benchmarks**: Not yet defined or automated. +- **Advanced Async/Await**: + - Real task scheduling (currently synchronous simulation). + - True lazy futures (currently eager). +- **Generic Trait Implementations**: Parser supports them, but runtime lacks full implementation for generic types. +- **Const Generics**: Initial parser support for `const` declarations, but no generic constraints yet. +- **Memory Management**: + - Move from `Clone` heavy semantics to a more efficient strategy (e.g., Reference Counting or GC). + - Current `Value` type uses significant cloning. + +## 🛠️ Technical Debt ### Parser -- Improve error recovery -- Support for more expression contexts -- Optimize parsing performance +- **Attribute System**: Needs more robust handling and validation beyond gated features. +- **Incremental Parsing**: LSP currently re-parses full files. ### Runtime -- Memory management improvements -- Garbage collection strategy -- Better async runtime +- **Type Safety**: Runtime currently skips much of the type validation even when annotations are present. +- **Module System**: Improve isolation and circular dependency handling. ### LSP -- Optimize for large files -- Incremental parsing -- Better semantic tokens -- Workspace symbol caching - -## Decision Points - -### Phase 2 Decisions -- Auto-import behavior: top of file vs inline (Top of file) -- Completion triggers: aggressive vs conservative (Aggressive) -- HTTP API design: Fetch-like vs Axios-like vs custom (Fetch-like) -- JSON handling: typed parsing vs dynamic (Typed, .parse()) -- String interpolation syntax: ${} vs {} (${}) - -### Phase 3 Decisions -- Type checking strictness: strict mode vs always on (Strict) -- Gradual typing: allow mixing typed and untyped code (No mixing, all explicit or implicit) -- Type inference: infer return types vs require explicit annotations (require explicit) - -### Phase 4 Decisions -- Macro syntax and capabilities (rust proc-macro-like, see quote. syn.) -- FFI interface design -- Backward compatibility policy (every major version may impose breaking backwards changes, to be readressed after language reaches maturity, maybe backwards compat to the nearest lts) \ No newline at end of file +- **Workspace Symbol Caching**: Currently missing, leading to full scans. +- **Go to Definition**: Expand beyond basic types and functions to trait methods and cross-module symbols. + +## ⚖️ Decision Points Status + +### Phase 2-4 Decisions (Carry-over/Ongoing) +- **Auto-import behavior**: Finalize convention for module-level vs explicit imports. +- **Backward Compatibility**: Define the exact LTS policy once 1.0 is reached. +- **Macro Capabilities**: Expand `loft_builtin_macros` to support user-defined procedural macros. diff --git a/loft_builtin_macros/Cargo.toml b/loft_builtin_macros/Cargo.toml index b1327b7..956c81c 100644 --- a/loft_builtin_macros/Cargo.toml +++ b/loft_builtin_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loft_builtin_macros" -version = "0.1.0-rc-4.2" +version = "0.1.0-rc-5" edition = "2021" [lib] diff --git a/loft_builtin_macros/src/lib.rs b/loft_builtin_macros/src/lib.rs index 462fc4e..85cec2f 100644 --- a/loft_builtin_macros/src/lib.rs +++ b/loft_builtin_macros/src/lib.rs @@ -59,6 +59,16 @@ pub fn loft_builtin(attr: TokenStream, item: TokenStream) -> TokenStream { } } +#[proc_macro_attribute] +pub fn required(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} + +#[proc_macro_attribute] +pub fn types(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} + fn extract_doc_comments(attrs: &[syn::Attribute]) -> String { attrs .iter() @@ -80,29 +90,110 @@ fn extract_doc_comments(attrs: &[syn::Attribute]) -> String { .to_string() } -fn handle_function(path: String, func: ItemFn) -> TokenStream { - let _fn_name = &func.sig.ident; +fn handle_function(_path: String, mut func: ItemFn) -> TokenStream { + // Parse arguments and look for attributes + let mut check_logic = Vec::new(); + let mut required_args = 0; + let mut type_checks = Vec::new(); + + for arg in func.sig.inputs.iter_mut() { + if let syn::FnArg::Typed(pat_type) = arg { + // Check if it's the `args` parameter (usually `&[Value]`) + let is_args_param = if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + pat_ident.ident == "args" + } else { + false + }; + + let mut i = 0; + while i < pat_type.attrs.len() { + let attr = &pat_type.attrs[i]; + if attr.path().is_ident("required") { + required_args += 1; + pat_type.attrs.remove(i); + } else if attr.path().is_ident("types") && is_args_param { + if let Meta::List(list) = &attr.meta { + let tokens = &list.tokens; + let type_str = tokens.to_string().replace(" ", ""); + + let types: Vec = type_str.split(',') + .map(|s| s.trim().to_string()) + .collect(); + + for (idx, type_name) in types.iter().enumerate() { + let is_varargs = type_name.ends_with('*'); + let base_type = if is_varargs { + type_name[..type_name.len()-1].to_string() + } else { + type_name.clone() + }; + + let type_match = match base_type.as_str() { + "bool" => quote! { Value::Boolean(_) }, + "string" => quote! { Value::String(_) }, + "number" => quote! { Value::Number(_) }, + "array" => quote! { Value::Array(_) }, + "object" => quote! { Value::Struct { .. } }, + _ => quote! { _ }, + }; + + let error_msg = format!("Argument must be of type {}", base_type); + + if is_varargs { + type_checks.push(quote! { + for val in &args[#idx..] { + if !matches!(val, #type_match) { + return Err(RuntimeError::new(#error_msg)); + } + } + }); + } else { + type_checks.push(quote! { + if let Some(val) = args.get(#idx) { + if !matches!(val, #type_match) { + return Err(RuntimeError::new(#error_msg)); + } + } + }); + } + } + } + pat_type.attrs.remove(i); + } else { + i += 1; + } + } + } + } + + if required_args > 0 { + let min_args = if required_args > 0 { required_args - 1 } else { 0 }; + let min_args_val = min_args as usize; + let error_msg = format!("Function requires at least {} arguments", min_args); + check_logic.push(quote! { + if args.is_empty() && #min_args_val > 0 { + return Err(RuntimeError::new(#error_msg)); + } + if args.len() < #min_args_val { + return Err(RuntimeError::new(#error_msg)); + } + }); + } + + check_logic.extend(type_checks); + let fn_vis = &func.vis; let fn_attrs = &func.attrs; let fn_sig = &func.sig; let fn_block = &func.block; - // Extract documentation comments for metadata - let _doc_string = extract_doc_comments(fn_attrs); - - // Parse the path to extract information - let parts: Vec<&str> = path.split('.').collect(); - let _metadata = if parts.len() >= 2 { - format!("builtin: {}, method: {}", parts[0], parts[1]) - } else { - format!("method: {}", path) - }; - - // For now, just pass through the function as-is - // In a more complete implementation, we could generate conversion code let expanded = quote! { #(#fn_attrs)* - #fn_vis #fn_sig #fn_block + #[allow(clippy::duplicated_attributes)] + #fn_vis #fn_sig { + #(#check_logic)* + #fn_block + } }; TokenStream::from(expanded) diff --git a/registry/Cargo.toml b/registry/Cargo.toml index bd3bf1d..647b16e 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loft-registry" -version = "0.1.0-rc-4.2" +version = "0.1.0-rc-5" edition = "2021" [dependencies] diff --git a/scripts/check_stdlib_coverage.js b/scripts/check_stdlib_coverage.js new file mode 100644 index 0000000..8b1aa78 --- /dev/null +++ b/scripts/check_stdlib_coverage.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); + +const STDLIB_TYPES_PATH = path.join(__dirname, '../src/lsp/stdlib_types.json'); +const BUILTINS_DIR = path.join(__dirname, '../src/runtime/builtins'); + +// Map of file/directory names to expected builtin names in stdlib_types.json +const MAPPING = { + 'array.rs': 'array', // Array functions might be under a different name or just 'array' + 'collections': 'array', // Assuming collections usually means array/map/set + 'encoding.rs': 'encoding', + 'ffi.rs': 'ffi', + 'io': 'fs', // io maps to fs builtin + 'json.rs': 'json', + 'math': 'math', + 'object.rs': 'object', + 'random.rs': 'random', + 'string': 'string', // string builtin + 'term.rs': 'term', + 'test.rs': 'test', + 'time.rs': 'time', + 'traits.rs': null, // traits.rs defines traits, likely covered by individual trait names + 'web': 'web', + 'mod.rs': null +}; + +function main() { + console.log('Checking stdlib coverage...'); + + if (!fs.existsSync(STDLIB_TYPES_PATH)) { + console.error(`Error: ${STDLIB_TYPES_PATH} not found.`); + process.exit(1); + } + + const stdlibTypes = JSON.parse(fs.readFileSync(STDLIB_TYPES_PATH, 'utf8')); + const documentedBuiltins = new Set(Object.keys(stdlibTypes.builtins || {})); + + console.log(`Found ${documentedBuiltins.size} documented builtins.`); + + const builtinFiles = fs.readdirSync(BUILTINS_DIR); + let missingDocs = []; + + for (const file of builtinFiles) { + if (file in MAPPING) { + const expectedName = MAPPING[file]; + if (expectedName === null) continue; + + if (!documentedBuiltins.has(expectedName)) { + // Check if maybe capitalization differs (e.g. Array vs array) + // stdlib_types seem to use lowercase for modules (term, math) and PascalCase for types (BitXor) + // But builtins are usually modules. + + // Special check for 'array' which might be 'Array' or similar + // But stdlib_types has 'str', 'num' etc types? + // Let's assume builtins are the keys. + + missingDocs.push({ file, expectedName }); + } + } else { + console.warn(`Warning: unmapped file ${file} in builtins directory.`); + } + } + + if (missingDocs.length > 0) { + console.error('\nMissing documentation for the following builtins:'); + for (const { file, expectedName } of missingDocs) { + console.error(`- ${file} -> expected '${expectedName}'`); + } + process.exit(1); + } else { + console.log('\nAll mapped builtins are documented!'); + } +} + +main(); diff --git a/scripts/gen_example_tests.sh b/scripts/gen_example_tests.sh index c9d28ff..bc6867d 100755 --- a/scripts/gen_example_tests.sh +++ b/scripts/gen_example_tests.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash + # Generate tests/examples.rs from the contents of the examples/ directory. # # Usage (run from the workspace root): diff --git a/scripts/release.sh b/scripts/release.sh index fad1cf1..89112b2 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # scripts/release.sh diff --git a/src/docgen/mod.rs b/src/docgen/mod.rs index 15270bb..cbb8225 100644 --- a/src/docgen/mod.rs +++ b/src/docgen/mod.rs @@ -47,6 +47,12 @@ pub struct DocGenerator { pub impl_methods: HashMap>, } +impl Default for DocGenerator { + fn default() -> Self { + Self::new() + } +} + impl DocGenerator { pub fn new() -> Self { Self { @@ -150,11 +156,9 @@ impl DocGenerator { Stmt::TraitDecl { name, methods } => { let method_names: Vec = methods .iter() - .filter_map(|m| match m { - crate::parser::TraitMethod::Signature { name, .. } => { - Some(name.clone()) - } - crate::parser::TraitMethod::Default { name, .. } => Some(name.clone()), + .map(|m| match m { + crate::parser::TraitMethod::Signature { name, .. } => name.clone(), + crate::parser::TraitMethod::Default { name, .. } => name.clone(), }) .collect(); @@ -173,7 +177,7 @@ impl DocGenerator { } => { let type_str = const_type .as_ref() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .unwrap_or_else(|| "unknown".to_string()); self.items.push(DocItem { @@ -188,7 +192,7 @@ impl DocGenerator { Stmt::VarDecl { name, var_type, .. } => { let type_str = var_type .as_ref() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .unwrap_or_else(|| "unknown".to_string()); self.items.push(DocItem { @@ -214,7 +218,7 @@ impl DocGenerator { if let Stmt::FunctionDecl { name, .. } = method { self.impl_methods .entry(type_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(name.clone()); } } @@ -342,8 +346,8 @@ impl DocGenerator { ]; for (pattern, extract_next) in patterns { - if line.starts_with(pattern) { - if extract_next { + if line.starts_with(pattern) + && extract_next { let rest = line.strip_prefix(pattern).unwrap(); // Extract identifier (alphanumeric + underscore) let name: String = rest @@ -354,7 +358,6 @@ impl DocGenerator { return Some(name); } } - } } None @@ -370,7 +373,7 @@ impl DocGenerator { base, type_args .iter() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .collect::>() .join(", ") ) @@ -383,7 +386,7 @@ impl DocGenerator { "({}) -> {}", params .iter() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .collect::>() .join(", "), Self::type_to_string(return_type) @@ -510,7 +513,7 @@ impl DocGenerator { } else { &[] }; - let has_subitems = impl_methods.map_or(false, |m| !m.is_empty()) || !impl_traits.is_empty(); + let has_subitems = impl_methods.is_some_and(|m| !m.is_empty()) || !impl_traits.is_empty(); if has_subitems { html.push_str(&format!( "
  • {}\n", @@ -769,7 +772,7 @@ impl DocGenerator { // Sort items by name length descending to avoid partial replacements let mut item_names: Vec = self.items.iter().map(|i| i.name.clone()).collect(); - item_names.sort_by(|a, b| b.len().cmp(&a.len())); + item_names.sort_by_key(|b| std::cmp::Reverse(b.len())); for (i, name) in item_names.iter().enumerate() { // Only replace whole words diff --git a/src/docgen/terminal.rs b/src/docgen/terminal.rs index 61c8e26..d7aadb1 100644 --- a/src/docgen/terminal.rs +++ b/src/docgen/terminal.rs @@ -227,7 +227,7 @@ pub fn list_topics(stdlib: &StdlibTypes) { // Primitives println!("\n {}", "PRIMITIVES".bright_white().bold()); - let mut primitives = vec!["string".to_string(), "array".to_string(), "num".to_string(), "bool".to_string(), "void".to_string()]; + let mut primitives = ["string".to_string(), "array".to_string(), "num".to_string(), "bool".to_string(), "void".to_string()]; primitives.sort(); for chunk in primitives.chunks(5) { println!(" {}", chunk.join(", ").to_lowercase()); diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs index 9b5e688..a76e268 100644 --- a/src/formatter/mod.rs +++ b/src/formatter/mod.rs @@ -17,6 +17,12 @@ pub struct Formatter { indent_size: usize, } +impl Default for Formatter { + fn default() -> Self { + Self::new() + } +} + impl Formatter { pub fn new() -> Self { Self { indent_size: 4 } diff --git a/src/formatter/token_formatter.rs b/src/formatter/token_formatter.rs index 3d0de9c..11f8296 100644 --- a/src/formatter/token_formatter.rs +++ b/src/formatter/token_formatter.rs @@ -12,6 +12,12 @@ pub struct TokenFormatter { indent_size: usize, } +impl Default for TokenFormatter { + fn default() -> Self { + Self::new() + } +} + impl TokenFormatter { pub fn new() -> Self { Self { indent_size: 4 } @@ -378,12 +384,11 @@ impl TokenFormatter { } // Ensure closing brace is on its own line - if matches!(token, Token::Punct(p) if p == "}") { - if !at_line_start { + if matches!(token, Token::Punct(p) if p == "}") + && !at_line_start { output.push('\n'); at_line_start = true; } - } if at_line_start { output.push_str(&" ".repeat(indent_level * self.indent_size)); @@ -432,10 +437,10 @@ impl TokenFormatter { Token::Punct(p) if p == "}" => { // Check next token let next_token = tokens.get(i + 1).map(|tw| &tw.token); - let should_newline = match next_token { - Some(Token::Punct(p)) if p == ";" || p == "," || p == ")" => false, - _ => true, - }; + let should_newline = !matches!( + next_token, + Some(Token::Punct(p)) if p == ";" || p == "," || p == ")" + ); if should_newline { output.push('\n'); diff --git a/src/lsp/mod.rs b/src/lsp/mod.rs index 0e13a2d..bc96abf 100644 --- a/src/lsp/mod.rs +++ b/src/lsp/mod.rs @@ -55,27 +55,41 @@ struct StdlibMethod { documentation: String, } +type StdlibMethodMap = HashMap; +type StdlibFieldMap = HashMap; + #[derive(Debug, Clone, Deserialize, Serialize)] struct StdlibType { kind: String, documentation: String, #[serde(default)] - fields: HashMap, + fields: StdlibFieldMap, #[serde(default)] - methods: HashMap, + methods: StdlibMethodMap, #[serde(default)] variants: Vec, #[serde(default)] - implemented_traits: Vec, + implements: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] -struct StdlibVariant { - name: String, - #[serde(default)] - fields: Option>, - #[serde(default)] - documentation: String, +#[serde(untagged)] +enum StdlibVariant { + Simple(String), + WithFields { + name: String, + #[serde(default)] + fields: Vec, + }, +} + +impl StdlibVariant { + fn name(&self) -> &str { + match self { + StdlibVariant::Simple(s) => s, + StdlibVariant::WithFields { name, .. } => name, + } + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -93,12 +107,6 @@ struct TraitMethodInfo { has_default_impl: bool, } -#[derive(Debug, Clone)] -struct EnumVariantInfo { - name: String, - fields: Option>, -} - // Symbol information for LSP features #[derive(Debug, Clone)] struct SymbolInfo { @@ -131,7 +139,7 @@ enum SymbolKind { methods: Vec, }, Enum { - variants: Vec, + variants: Vec<(String, Option>)>, }, Constant { const_type: String, @@ -591,8 +599,7 @@ impl LoftLanguageServer { ]; for (prefix, _) in &patterns { - if line.starts_with(prefix) { - let rest = &line[prefix.len()..]; + if let Some(rest) = line.strip_prefix(prefix) { // Extract the name (stop at whitespace, colon, or parenthesis) if let Some(name) = rest .split(|c: char| c.is_whitespace() || c == ':' || c == '(' || c == '{') @@ -637,7 +644,7 @@ impl LoftLanguageServer { } // Check builtin traits else if let Some(trait_def) = stdlib_types.traits.get(trait_name) { - for (method_name, _) in &trait_def.methods { + for method_name in trait_def.methods.keys() { required_methods.push(method_name.clone()); } } else { @@ -946,7 +953,7 @@ impl LoftLanguageServer { lines: &[&str], ) { let mut found_terminal = false; - for (_idx, stmt) in stmts.iter().enumerate() { + for stmt in stmts.iter() { if found_terminal { // Code after return/break/continue is unreachable if let Some(line_num) = Self::get_stmt_line(stmt, lines) { @@ -1000,7 +1007,7 @@ impl LoftLanguageServer { lines: &[&str], ) { let mut found_terminal = false; - for (_idx, stmt) in stmts.iter().enumerate() { + for stmt in stmts.iter() { if found_terminal { // Code after return/break/continue is unreachable if let Some(line_num) = Self::get_stmt_line(stmt, lines) { @@ -1081,17 +1088,15 @@ impl LoftLanguageServer { lines: &[&str], ) { match stmt { - Stmt::VarDecl { value, .. } => { - if let Some(expr) = value { - Self::check_expr_with_imports( - expr, - symbols, - used_vars, - used_imports, - diagnostics, - lines, - ); - } + Stmt::VarDecl { value: Some(expr), .. } => { + Self::check_expr_with_imports( + expr, + symbols, + used_vars, + used_imports, + diagnostics, + lines, + ); } Stmt::FunctionDecl { body, .. } => { if let Stmt::Block(stmts) = body.as_ref() { @@ -1115,17 +1120,15 @@ impl LoftLanguageServer { lines, ); } - Stmt::Return(value) => { - if let Some(expr) = value { - Self::check_expr_with_imports( - expr, - symbols, - used_vars, - used_imports, - diagnostics, - lines, - ); - } + Stmt::Return(Some(expr)) => { + Self::check_expr_with_imports( + expr, + symbols, + used_vars, + used_imports, + diagnostics, + lines, + ); } Stmt::Assign { value, .. } => { Self::check_expr_with_imports( @@ -1228,10 +1231,8 @@ impl LoftLanguageServer { lines: &[&str], ) { match stmt { - Stmt::VarDecl { value, .. } => { - if let Some(expr) = value { - Self::check_expr(expr, symbols, used_vars, diagnostics, lines); - } + Stmt::VarDecl { value: Some(expr), .. } => { + Self::check_expr(expr, symbols, used_vars, diagnostics, lines); } Stmt::FunctionDecl { body, .. } => { if let Stmt::Block(stmts) = body.as_ref() { @@ -1241,10 +1242,8 @@ impl LoftLanguageServer { Stmt::Expr(expr) => { Self::check_expr(expr, symbols, used_vars, diagnostics, lines); } - Stmt::Return(value) => { - if let Some(expr) = value { - Self::check_expr(expr, symbols, used_vars, diagnostics, lines); - } + Stmt::Return(Some(expr)) => { + Self::check_expr(expr, symbols, used_vars, diagnostics, lines); } Stmt::Assign { value, .. } => { Self::check_expr(value, symbols, used_vars, diagnostics, lines); @@ -1360,7 +1359,7 @@ impl LoftLanguageServer { if let SymbolKind::Function { params, .. } = &symbol.kind { if params.len() != args.len() { if let Some(line_num) = - Self::find_identifier_line(&func_name, lines) + Self::find_identifier_line(func_name, lines) { diagnostics.push(Diagnostic { range: Range { @@ -1595,7 +1594,7 @@ impl LoftLanguageServer { if let SymbolKind::Function { params, .. } = &symbol.kind { if params.len() != args.len() { if let Some(line_num) = - Self::find_identifier_line(&func_name, lines) + Self::find_identifier_line(func_name, lines) { diagnostics.push(Diagnostic { range: Range { @@ -1886,7 +1885,7 @@ impl LoftLanguageServer { let final_type = var_type .as_ref() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .or(inferred_type); symbols.push(SymbolInfo { @@ -1916,7 +1915,7 @@ impl LoftLanguageServer { kind: SymbolKind::Constant { const_type: const_type .as_ref() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .unwrap_or_else(|| "unknown".to_string()), }, detail: Some(format!("const {}", name)), @@ -2043,16 +2042,13 @@ impl LoftLanguageServer { }); } Stmt::EnumDecl { name, variants } => { - let variant_infos: Vec = variants + let variant_infos: Vec<(String, Option>)> = variants .iter() - .map(|(n, fields)| EnumVariantInfo { - name: n.clone(), - fields: fields.as_ref().map(|types| { - types - .iter() - .map(|t| Self::type_to_string(t)) - .collect() - }), + .map(|(n, fields)| { + let field_types = fields.as_ref().map(|types| { + types.iter().map(Self::type_to_string).collect() + }); + (n.clone(), field_types) }) .collect(); @@ -2272,7 +2268,7 @@ impl LoftLanguageServer { base, type_args .iter() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .collect::>() .join(", ") ) @@ -2285,7 +2281,7 @@ impl LoftLanguageServer { "fn({}) -> {}", params .iter() - .map(|t| Self::type_to_string(t)) + .map(Self::type_to_string) .collect::>() .join(", "), Self::type_to_string(return_type) @@ -2489,19 +2485,25 @@ impl LoftLanguageServer { if let Some(type_data) = self.stdlib_types.types.get(object_name) { // Add variants for variant in &type_data.variants { - let insert_text = if let Some(_) = &variant.fields { - format!("{}($0)", variant.name) - } else { - variant.name.clone() + let variant_name = variant.name(); + let insert_text = match variant { + StdlibVariant::Simple(name) => name.clone(), + StdlibVariant::WithFields { name, fields } => { + if fields.is_empty() { + name.clone() + } else { + format!("{}($1)", name) + } + } }; items.push(CompletionItem { - label: variant.name.clone(), + label: variant_name.to_string(), kind: Some(CompletionItemKind::ENUM_MEMBER), - detail: Some(format!("{}::{}", object_name, variant.name)), + detail: Some(format!("{}::{}", object_name, variant_name)), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, - value: format!("Variant of {} - {}", object_name, variant.documentation), + value: format!("Variant of {}", object_name), })), insert_text: Some(insert_text), insert_text_format: Some(InsertTextFormat::SNIPPET), @@ -2518,17 +2520,21 @@ impl LoftLanguageServer { // Check if symbol is an Enum if let Some(symbol) = doc_data.symbols.iter().find(|s| s.name == object_name) { if let SymbolKind::Enum { variants } = &symbol.kind { - for variant in variants { - let insert_text = if let Some(_) = &variant.fields { - format!("{}($0)", variant.name) + for (variant_name, fields) in variants { + let insert_text = if let Some(fields) = fields { + if fields.is_empty() { + variant_name.clone() + } else { + format!("{}($1)", variant_name) + } } else { - variant.name.clone() + variant_name.clone() }; items.push(CompletionItem { - label: variant.name.clone(), + label: variant_name.clone(), kind: Some(CompletionItemKind::ENUM_MEMBER), - detail: Some(format!("{}::{}", object_name, variant.name)), + detail: Some(format!("{}::{}", object_name, variant_name)), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: format!("Enum variant of {}", object_name), @@ -2674,19 +2680,25 @@ impl LoftLanguageServer { // Add variants as completions for enums for variant in &stdlib_type.variants { - let insert_text = if let Some(_) = &variant.fields { - format!("{}($0)", variant.name) - } else { - variant.name.clone() + let variant_name = variant.name(); + let insert_text = match variant { + StdlibVariant::Simple(name) => name.clone(), + StdlibVariant::WithFields { name, fields } => { + if fields.is_empty() { + name.clone() + } else { + format!("{}($1)", name) + } + } }; - + items.push(CompletionItem { - label: variant.name.clone(), + label: variant_name.to_string(), kind: Some(CompletionItemKind::ENUM_MEMBER), - detail: Some(format!("{}::{}", type_name, variant.name)), + detail: Some(format!("{}::{}", type_name, variant_name)), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, - value: format!("Variant of {}\n\n{}", type_name, variant.documentation), + value: format!("Variant of {}", type_name), })), insert_text: Some(insert_text), insert_text_format: Some(InsertTextFormat::SNIPPET), @@ -2795,7 +2807,7 @@ impl LoftLanguageServer { text.push_str("```loft\n"); text.push_str("fn "); text.push_str(&symbol.name); - text.push_str("("); + text.push('('); text.push_str( ¶ms .iter() @@ -2816,7 +2828,7 @@ impl LoftLanguageServer { for (field_name, field_type) in fields { text.push_str(&format!(" {}: {},\n", field_name, field_type)); } - text.push_str("}"); + text.push('}'); text.push_str("\n```\n\n"); text.push_str("_(struct)_"); @@ -2835,13 +2847,13 @@ impl LoftLanguageServer { text.push_str("enum "); text.push_str(&symbol.name); text.push_str(" {\n"); - for variant in variants { + for (variant_name, fields) in variants { text.push_str(" "); - text.push_str(&variant.name); - if let Some(fields) = &variant.fields { - text.push_str("("); - text.push_str(&fields.join(", ")); - text.push_str(")"); + text.push_str(variant_name); + if let Some(types) = fields { + text.push('('); + text.push_str(&types.join(", ")); + text.push(')'); } text.push_str(",\n"); } @@ -3521,23 +3533,31 @@ impl LanguageServer for LoftLanguageServer { if !type_def.variants.is_empty() { hover_text.push_str("\n\n---\n\n**Variants:**"); for variant in &type_def.variants { - hover_text.push_str(&format!("\n- `{}`", variant.name)); - if let Some(fields) = &variant.fields { - hover_text.push_str(&format!("({})", fields.join(", "))); - } - if !variant.documentation.is_empty() { - hover_text.push_str(&format!(" - {}", variant.documentation)); + match variant { + StdlibVariant::Simple(name) => { + hover_text.push_str(&format!("\n- `{}`", name)); + } + StdlibVariant::WithFields { name, fields } => { + hover_text.push_str(&format!("\n- `{}({})`", name, fields.join(", "))); + } } } } - if !type_def.implemented_traits.is_empty() { + if !type_def.implements.is_empty() { hover_text.push_str("\n\n---\n\n**Implements:**"); - for trait_name in &type_def.implemented_traits { + for trait_name in &type_def.implements { hover_text.push_str(&format!("\n- `{}`", trait_name)); } } + if !type_def.implements.is_empty() { + hover_text.push_str("\n\n---\n\n**Implements:**"); + for trait_name in &type_def.implements { + hover_text.push_str(&format!("\n- `{}`", trait_name)); + } + } + if !type_def.fields.is_empty() { hover_text.push_str("\n\n---\n\n**Fields:**"); for (name, field) in type_def.fields.iter().take(5) { @@ -4450,7 +4470,7 @@ impl LanguageServer for LoftLanguageServer { if let Some(source_uri) = &symbol.source_uri { detail.push_str(&format!( " (from {})", - source_uri.split('/').last().unwrap_or(source_uri) + source_uri.split('/').next_back().unwrap_or(source_uri) )); } @@ -5948,19 +5968,14 @@ impl LanguageServer for LoftLanguageServer { let trimmed = line.trim(); // Function/struct/trait definitions - if trimmed.starts_with("fn ") || trimmed.starts_with("teach fn ") { - if trimmed.contains('{') { - brace_stack.push((line_num, FoldingRangeKind::Region)); - } - } else if trimmed.starts_with("def ") || trimmed.starts_with("teach def ") { - if trimmed.contains('{') { - brace_stack.push((line_num, FoldingRangeKind::Region)); - } - } else if trimmed.starts_with("trait ") || trimmed.starts_with("teach trait ") { - if trimmed.contains('{') { - brace_stack.push((line_num, FoldingRangeKind::Region)); - } - } else if trimmed.starts_with("impl ") { + if trimmed.starts_with("fn ") + || trimmed.starts_with("teach fn ") + || trimmed.starts_with("def ") + || trimmed.starts_with("teach def ") + || trimmed.starts_with("trait ") + || trimmed.starts_with("teach trait ") + || trimmed.starts_with("impl ") + { if trimmed.contains('{') { brace_stack.push((line_num, FoldingRangeKind::Region)); } @@ -6053,7 +6068,7 @@ impl LanguageServer for LoftLanguageServer { if fuzzy_match(&symbol.name.to_lowercase(), &query) { let location = Location { uri: Uri::from_str(uri_str).unwrap(), - range: symbol.range.unwrap_or_else(|| Range::default()), + range: symbol.range.unwrap_or_else(Range::default), }; let lsp_kind = match &symbol.kind { @@ -6452,7 +6467,7 @@ pub async fn run_server() { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let (service, socket) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, socket) = LspService::new(LoftLanguageServer::new); Server::new(stdin, stdout, socket).serve(service).await; } @@ -7024,7 +7039,7 @@ let e = fs.read("file.txt"); // Test that member completions work for Response type use tower_lsp::LspService; - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); // Simulate a document with a Response variable @@ -7106,7 +7121,7 @@ let response = await web.get("https://google.com"); // Test that member completions work for RequestBuilder type use tower_lsp::LspService; - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); // Simulate a document with a RequestBuilder variable @@ -7190,7 +7205,7 @@ let req = web.request("https://api.example.com"); // b. // Should show Response fields and methods use tower_lsp::LspService; - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); // Simulate the exact code from the issue @@ -7483,7 +7498,7 @@ let w = 4;"#; #[tokio::test] async fn test_inlay_hints() { // Test inlay hints for type inference - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -7600,13 +7615,13 @@ let name = "Alice";"#; // Should have hints for variables with inferred types assert!(hints.is_some()); let hints = hints.unwrap(); - assert!(hints.len() >= 1); // At least one hint (for variables with types) + assert!(!hints.is_empty()); // At least one hint (for variables with types) } #[tokio::test] async fn test_folding_ranges() { // Test folding ranges for code structure - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -7663,7 +7678,7 @@ def Point { #[tokio::test] async fn test_workspace_symbols() { // Test workspace symbol search - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -7746,7 +7761,7 @@ def Point { #[tokio::test] async fn test_document_links() { // Test document link detection for URLs - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -7790,7 +7805,7 @@ let url = "https://github.com/tascord/loft"; #[tokio::test] async fn test_code_lens() { // Test code lens for reference counts - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -7883,7 +7898,7 @@ let result2 = add(3, 4);"#; #[tokio::test] async fn test_call_hierarchy() { // Test call hierarchy preparation - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -8047,7 +8062,7 @@ fn calculate() -> num { #[tokio::test] async fn test_document_formatting() { - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -8111,7 +8126,7 @@ term.println(result); #[tokio::test] async fn test_range_formatting() { - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -8184,7 +8199,7 @@ fn well_formatted() -> void { #[tokio::test] async fn test_unused_variable_diagnostic() { - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -8219,7 +8234,7 @@ fn well_formatted() -> void { #[tokio::test] async fn test_function_arity_diagnostic() { - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -8256,7 +8271,7 @@ fn main() -> void { #[tokio::test] async fn test_undefined_identifier_diagnostic() { - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test.loft".to_string(); @@ -8289,7 +8304,7 @@ fn main() -> void { #[tokio::test] async fn test_cross_file_references() { - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); // File 1: defines and exports a function @@ -8361,7 +8376,7 @@ fn main() -> void { // Should have at least 1 reference (the definition itself) // Cross-file references depend on import resolution which may need further work assert!( - locations.len() >= 1, + !locations.is_empty(), "Expected at least 1 reference, found: {}", locations.len() ); @@ -8369,7 +8384,7 @@ fn main() -> void { #[tokio::test] async fn test_enum_support() { - let (service, _) = LspService::new(|client| LoftLanguageServer::new(client)); + let (service, _) = LspService::new(LoftLanguageServer::new); let server = service.inner(); let uri = "file:///test_enum.loft".to_string(); @@ -8404,9 +8419,10 @@ fn main() -> void { if let SymbolKind::Enum { variants } = &enum_symbol.unwrap().kind { assert_eq!(variants.len(), 3); - assert!(variants.iter().any(|v| v.name == "Red")); - assert!(variants.iter().any(|v| v.name == "Green")); - assert!(variants.iter().any(|v| v.name == "Blue")); + let variant_names: Vec = variants.iter().map(|(n, _)| n.clone()).collect(); + assert!(variant_names.contains(&"Red".to_string())); + assert!(variant_names.contains(&"Green".to_string())); + assert!(variant_names.contains(&"Blue".to_string())); } else { panic!("Symbol 'Color' is not an Enum"); } diff --git a/src/lsp/stdlib_types.json b/src/lsp/stdlib_types.json index 19c289f..80c3370 100644 --- a/src/lsp/stdlib_types.json +++ b/src/lsp/stdlib_types.json @@ -509,6 +509,76 @@ "documentation": "Set the random seed for reproducible random numbers" } } + }, + "ffi": { + "kind": "struct", + "documentation": "Foreign Function Interface for loading and calling dynamic libraries", + "methods": { + "load": { + "params": ["path: str"], + "return_type": "FfiLibrary", + "documentation": "Load a shared library from the given path" + } + } + }, + "FfiLibrary": { + "kind": "struct", + "documentation": "A loaded foreign library", + "fields": { + "path": { + "type": "str", + "documentation": "The path to the library" + } + }, + "methods": { + "symbol": { + "params": ["name: str"], + "return_type": "FfiFunction", + "documentation": "Get a function symbol from the library" + }, + "call": { + "params": ["symbol_name: str", "signature: str", "...args: any"], + "return_type": "any", + "documentation": "Call a function directly by symbol name with the given signature" + } + } + }, + "FfiFunction": { + "kind": "struct", + "documentation": "A function loaded from a foreign library", + "fields": { + "name": { + "type": "str", + "documentation": "The symbol name" + }, + "library_path": { + "type": "str", + "documentation": "Path to the library containing this function" + } + }, + "methods": { + "call": { + "params": ["signature: str", "...args: any"], + "return_type": "any", + "documentation": "Call the function with the given signature and arguments.\n\nSignature format: 'return_type(arg_type1, arg_type2, ...)'\nSupported types: i32, i64, f32, f64, void" + } + } + }, + "test": { + "kind": "struct", + "documentation": "Testing utilities for Loft", + "methods": { + "assert": { + "params": ["condition: bool", "message?: str"], + "return_type": "void", + "documentation": "Assert that a condition is true. Throws an error if false." + }, + "assert_eq": { + "params": ["left: any", "right: any", "message?: str"], + "return_type": "void", + "documentation": "Assert that two values are equal. Throws an error if they are not." + } + } } }, "string_methods": { @@ -817,7 +887,10 @@ "Option": { "kind": "enum", "documentation": "A native optional value. Use `Option.Some(value)` to wrap a value or `Option.None` when absent.", - "variants": ["Some", "None"], + "variants": [ + { "name": "Some", "fields": ["T"] }, + "None" + ], "fields": {}, "methods": { "is_some": { @@ -850,7 +923,10 @@ "Result": { "kind": "enum", "documentation": "A native result type. Use `Result.Ok(value)` for success or `Result.Err(error)` for failure.", - "variants": ["Ok", "Err"], + "variants": [ + { "name": "Ok", "fields": ["T"] }, + { "name": "Err", "fields": ["E"] } + ], "fields": {}, "methods": { "is_ok": { @@ -1044,3 +1120,4 @@ } } } + diff --git a/src/main.rs b/src/main.rs index 2ff473d..360687d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,9 +121,7 @@ enum Commands { #[cfg(not(target_arch = "wasm32"))] fn should_append_semicolon(input: &str) -> bool { let trimmed = input.trim(); - !vec![ - "let", "const", "fn", "struct", "impl", "trait", "enum", "if", "while", "for", "match", - ] + !["let", "const", "fn", "struct", "impl", "trait", "enum", "if", "while", "for", "match"] .iter() .any(|keyword| trimmed.starts_with(keyword)) && !trimmed.ends_with(';') @@ -487,19 +485,16 @@ fn print_help() { ); println!("{}", "Commands:".truecolor(ACID.0, ACID.1, ACID.2).bold()); println!( - " {} - {}", - "help".truecolor(LUMINOUS.0, LUMINOUS.1, LUMINOUS.2), - "Show this help message" + " {} - Show this help message", + "help".truecolor(LUMINOUS.0, LUMINOUS.1, LUMINOUS.2) ); println!( - " {} - {}", - "clear".truecolor(LUMINOUS.0, LUMINOUS.1, LUMINOUS.2), - "Clear the screen" + " {} - Clear the screen", + "clear".truecolor(LUMINOUS.0, LUMINOUS.1, LUMINOUS.2) ); println!( - " {} - {}", - "exit".truecolor(LUMINOUS.0, LUMINOUS.1, LUMINOUS.2), - "Exit the REPL" + " {} - Exit the REPL", + "exit".truecolor(LUMINOUS.0, LUMINOUS.1, LUMINOUS.2) ); println!(); println!("{}", "Examples:".truecolor(ACID.0, ACID.1, ACID.2).bold()); @@ -599,14 +594,10 @@ fn run_file(path: &str, features: Vec) { Ok(stmts) => { let mut interpreter = Interpreter::with_source(path, &code).with_features(features); - match interpreter.eval_program(stmts) { - Err(e) => { - println!(); - print_error(&e); - std::process::exit(1); - } - - _ => {} + if let Err(e) = interpreter.eval_program(stmts) { + println!(); + print_error(&e); + std::process::exit(1); } } Err(e) => { @@ -794,10 +785,9 @@ fn run_new(name: &str) { serde_json::to_string_pretty(&manifest).unwrap(), ) { Ok(_) => println!( - " {} {} {}", + " {} {} manifest.json", "+".bright_green(), - "Created".bright_green(), - "manifest.json" + "Created".bright_green() ), Err(e) => { println!( @@ -825,10 +815,9 @@ term.println("The answer is:", y); if !main_path.exists() { match fs::write(&main_path, main_content) { Ok(_) => println!( - " {} {} {}", + " {} {} src/main.lf", "+".bright_green(), - "Created".bright_green(), - "src/main.lf" + "Created".bright_green() ), Err(e) => { println!( @@ -857,11 +846,11 @@ term.println("The answer is:", y); println!(); if name == "." { println!("To get started:"); - println!(" {} {}", "loft".bright_cyan(), "."); + println!(" {} .", "loft".bright_cyan()); } else { println!("To get started:"); println!(" {} {}", "cd".bright_cyan(), name); - println!(" {} {}", "loft".bright_cyan(), "."); + println!(" {} .", "loft".bright_cyan()); } } @@ -1397,11 +1386,10 @@ fn run_update(specific_package: Option<&str>) { for pkg in &packages { if let Some(ver_str) = pkg["version"].as_str() { if let Ok(ver) = semver::Version::parse(ver_str) { - if version_req.matches(&ver) { - if best_match.is_none() || best_match.as_ref().unwrap().1 < ver { + if version_req.matches(&ver) + && (best_match.is_none() || best_match.as_ref().unwrap().1 < ver) { best_match = Some((ver_str.to_string(), ver)); } - } } } } @@ -1497,7 +1485,6 @@ fn run_update(specific_package: Option<&str>) { "Error".bright_red().bold(), e ); - return; }); // Extract tarball @@ -1612,7 +1599,7 @@ fn run_doc(output_dir: &str) { // Generate HTML documentation let output_path = Path::new(output_dir); - match doc_gen.generate_html(&output_path, &manifest.name) { + match doc_gen.generate_html(output_path, &manifest.name) { Ok(_) => { println!(); println!( @@ -1670,7 +1657,7 @@ fn run_stdlib_doc(output_dir: &str) { println!("Generating HTML..."); let output_path = Path::new(output_dir); - match doc_gen.generate_html(&output_path) { + match doc_gen.generate_html(output_path) { Ok(_) => { println!(); println!( @@ -2037,25 +2024,23 @@ fn run_format(path: Option<&str>, check: bool) { println!(" {} {}", "v".dimmed(), display_path.dimmed()); } unchanged_count += 1; + } else if check { + println!(" {} {}", "!".bright_red(), display_path.bright_white()); + formatted_count += 1; } else { - if check { - println!(" {} {}", "!".bright_red(), display_path.bright_white()); - formatted_count += 1; - } else { - match fs::write(file_path, formatted_content) { - Ok(_) => { - println!(" {} {}", "v".bright_green(), display_path.bright_white()); - formatted_count += 1; - } - Err(e) => { - println!( - "{}: Failed to write '{}': {}", - "Error".bright_red().bold(), - display_path, - e - ); - error_count += 1; - } + match fs::write(file_path, formatted_content) { + Ok(_) => { + println!(" {} {}", "v".bright_green(), display_path.bright_white()); + formatted_count += 1; + } + Err(e) => { + println!( + "{}: Failed to write '{}': {}", + "Error".bright_red().bold(), + display_path, + e + ); + error_count += 1; } } } @@ -2151,7 +2136,7 @@ fn run_docs(topic: Option) { // Start with entrypoint let entrypoint = Path::new(&manifest.entrypoint); - if entrypoint.as_os_str().len() > 0 && entrypoint.exists() { + if !entrypoint.as_os_str().is_empty() && entrypoint.exists() { files_to_parse.push(entrypoint.to_path_buf()); } @@ -2169,11 +2154,10 @@ fn run_docs(topic: Option) { { collect_files(&path, files); } - } else if path.extension().and_then(|s| s.to_str()) == Some("lf") { - if !files.contains(&path) { + } else if path.extension().and_then(|s| s.to_str()) == Some("lf") + && !files.contains(&path) { files.push(path); } - } } } } diff --git a/src/manifest.rs b/src/manifest.rs index 20c0357..422f8bc 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -149,7 +149,7 @@ mod tests { }; let result = manifest - .resolve_import(&vec!["myproject".to_string()]) + .resolve_import(&["myproject".to_string()]) .unwrap(); assert_eq!(result, "src/main.lf"); } @@ -166,7 +166,7 @@ mod tests { dependencies, }; - let result = manifest.resolve_import(&vec!["utils".to_string()]).unwrap(); + let result = manifest.resolve_import(&["utils".to_string()]).unwrap(); assert_eq!(result, "./deps/utils"); } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0b2038d..292b799 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -205,7 +205,7 @@ impl<'a> Parser<'a> { } fn next(&mut self) -> Result> { - match self.tokens.next() { + match self.tokens.read_next_token() { Some(Ok(token)) => Ok(Some(token)), Some(Err(e)) => Err(e), None => Ok(None), @@ -258,7 +258,7 @@ impl<'a> Parser<'a> { pub fn parse(&mut self) -> Result> { let mut statements = Vec::new(); - while let Some(_) = self.peek()? { + while (self.peek()?).is_some() { statements.push(self.parse_statement()?); } @@ -1227,10 +1227,9 @@ impl<'a> Parser<'a> { let mut left = self.parse_primary_expr()?; // Handle postfix operations but NOT struct literals - loop { - if let Some(token) = self.peek()? { - match token { - Token::Punct(ref p) if p == "(" => { + while let Some(token) = self.peek()? { + match token { + Token::Punct(ref p) if p == "(" => { // Function call self.next()?; // consume '(' let mut args = Vec::new(); @@ -1282,9 +1281,6 @@ impl<'a> Parser<'a> { } _ => break, } - } else { - break; - } } // Now handle binary operations @@ -1296,9 +1292,8 @@ impl<'a> Parser<'a> { let mut expr = self.parse_pattern_primary()?; // Handle postfix operations but don't interpret { as struct literal - loop { - if let Some(token) = self.peek()? { - match token { + while let Some(token) = self.peek()? { + match token { Token::Punct(ref p) if p == "(" => { // Function call or enum constructor self.next()?; // consume '(' @@ -1341,9 +1336,6 @@ impl<'a> Parser<'a> { } _ => break, } - } else { - break; - } } Ok(expr) @@ -1608,9 +1600,8 @@ impl<'a> Parser<'a> { // Parse postfix operations like function calls, field access, and indexing fn parse_postfix(&mut self, mut expr: Expr) -> Result { - loop { - if let Some(token) = self.peek()? { - match token { + while let Some(token) = self.peek()? { + match token { Token::Punct(ref p) if p == "(" => { expr = self.parse_call(expr)?; } @@ -1654,9 +1645,6 @@ impl<'a> Parser<'a> { } _ => break, } - } else { - break; - } } Ok(expr) } @@ -1771,12 +1759,10 @@ impl<'a> Parser<'a> { let mut found_arrow = false; // Look ahead to find ) => pattern - loop { - match self.next()? { - Some(token) => { - tokens_to_restore.push(token.clone()); + while let Some(token) = self.next()? { + tokens_to_restore.push(token.clone()); - match &token { + match &token { Token::Punct(p) if p == ")" && depth == 0 => { // Check next token for => if let Some(token) = self.next()? { @@ -1798,9 +1784,6 @@ impl<'a> Parser<'a> { if tokens_to_restore.len() > 100 { break; } - } - None => break, - } } // Restore all tokens in reverse order (inserting at the front) @@ -2127,7 +2110,7 @@ mod inline_tests { .. } => { assert_eq!(name, "add"); - assert_eq!(*is_exported, true); + assert!(*is_exported); assert_eq!(params.len(), 2); } _ => panic!("Expected function declaration"), @@ -2416,7 +2399,7 @@ mod inline_tests { match &result[0] { Stmt::FunctionDecl { name, is_async, .. } => { assert_eq!(name, "fetch_data"); - assert_eq!(*is_async, true); + assert!(*is_async); } _ => panic!("Expected async function declaration"), } diff --git a/src/parser/tests.rs b/src/parser/tests.rs index cfdfbf4..e8d28b2 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -134,7 +134,7 @@ fn test_parse_exported_function() { .. } => { assert_eq!(name, "add"); - assert_eq!(*is_exported, true); + assert!(*is_exported); assert_eq!(params.len(), 2); } _ => panic!("Expected function declaration"), @@ -423,7 +423,7 @@ fn test_async_fn_vs_async_expr() { match &result[0] { Stmt::FunctionDecl { name, is_async, .. } => { assert_eq!(name, "fetch_data"); - assert_eq!(*is_async, true); + assert!(*is_async); } _ => panic!("Expected async function declaration"), } diff --git a/src/parser/token_stream.rs b/src/parser/token_stream.rs index bdc87dc..aa9dc57 100644 --- a/src/parser/token_stream.rs +++ b/src/parser/token_stream.rs @@ -163,7 +163,7 @@ impl TokenStream<'_> { } } - return s; + s } pub fn read_string(&mut self) -> Result { @@ -284,7 +284,7 @@ impl TokenStream<'_> { let mut expr_stream = TokenStream::new(expr_input); // Parse all tokens from the expression - while let Some(token_result) = expr_stream.next() { + while let Some(token_result) = expr_stream.read_next_token() { tokens.push(token_result?); } } @@ -295,7 +295,7 @@ impl TokenStream<'_> { pub fn skip_whitespace_and_comments(&mut self) -> Result<()> { loop { // Skip regular whitespace - self.read_while(|c| Self::is_whitespace(c)); + self.read_while(Self::is_whitespace); if self.input.eof() { break; @@ -339,13 +339,12 @@ impl TokenStream<'_> { let mut found_end = false; while !self.input.eof() { let c = self.input.next().unwrap(); - if c == '*' && !self.input.eof() { - if self.input.peek().unwrap() == '/' { + if c == '*' && !self.input.eof() + && self.input.peek().unwrap() == '/' { self.input.next(); // consume '/' found_end = true; break; } - } if is_doc_comment { comment_text.push(c); } @@ -443,7 +442,7 @@ impl TokenStream<'_> { Ok(Token::Op(op)) } - pub fn next(&mut self) -> Option> { + pub fn read_next_token(&mut self) -> Option> { self.parse_next().transpose() } diff --git a/src/runtime/builtins/array.rs b/src/runtime/builtins/array.rs index cf83fd4..e82d557 100644 --- a/src/runtime/builtins/array.rs +++ b/src/runtime/builtins/array.rs @@ -6,6 +6,7 @@ use rust_decimal::Decimal; /// Get the length of an array #[loft_builtin(array.length)] +// TODO: Elide with #[required] and #[types(array)] for 'this' fn array_length(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::Array(arr) => Ok(Value::Number(Decimal::from(arr.len()))), @@ -16,11 +17,9 @@ fn array_length(this: &Value, _args: &[Value]) -> RuntimeResult { /// Push a value to the end of an array /// Returns a new array with the value added #[loft_builtin(array.push)] -fn array_push(this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("push() requires a value to push")); - } - +// TODO: Elide with #[required] and #[types(array)] for 'this' +fn array_push(this: &Value, #[required] args: &[Value]) -> RuntimeResult { + // Note: 'this' type check is manual match this { Value::Array(arr) => { let mut new_arr = arr.clone(); @@ -35,6 +34,7 @@ fn array_push(this: &Value, args: &[Value]) -> RuntimeResult { /// Returns the popped value, or Unit if array is empty /// Note: This doesn't modify the original array #[loft_builtin(array.pop)] +// TODO: Elide with #[required] and #[types(array)] for 'this' fn array_pop(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::Array(arr) => { @@ -50,6 +50,7 @@ fn array_pop(this: &Value, _args: &[Value]) -> RuntimeResult { /// Remove last element and return new array #[loft_builtin(array.remove_last)] +// TODO: Elide with #[required] and #[types(array)] for 'this' fn array_remove_last(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::Array(arr) => { @@ -69,11 +70,8 @@ fn array_remove_last(this: &Value, _args: &[Value]) -> RuntimeResult { /// Get a value at a specific index #[loft_builtin(array.get)] -fn array_get(this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("get() requires an index")); - } - +// TODO: Elide with #[required] and #[types(array)] for 'this' +fn array_get(this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match (this, &args[0]) { (Value::Array(arr), Value::Number(idx)) => { let idx_usize = idx @@ -83,18 +81,15 @@ fn array_get(this: &Value, args: &[Value]) -> RuntimeResult { Ok(arr.get(idx_usize).cloned().unwrap_or(Value::Unit)) } - (Value::Array(_), _) => Err(RuntimeError::new("Array index must be a number")), + (Value::Array(_), _) => unreachable!(), _ => Err(RuntimeError::new("get() can only be called on arrays")), } } /// Set a value at a specific index (returns new array) #[loft_builtin(array.set)] -fn array_set(this: &Value, args: &[Value]) -> RuntimeResult { - if args.len() < 2 { - return Err(RuntimeError::new("set() requires an index and a value")); - } - +// TODO: Elide with #[required] and #[types(array)] for 'this' +fn array_set(this: &Value, #[types(number, _)] args: &[Value]) -> RuntimeResult { match (this, &args[0], &args[1]) { (Value::Array(arr), Value::Number(idx), value) => { let idx_usize = idx @@ -120,6 +115,7 @@ fn array_set(this: &Value, args: &[Value]) -> RuntimeResult { /// Check if array is empty #[loft_builtin(array.is_empty)] +// TODO: Elide with #[required] and #[types(array)] for 'this' fn array_is_empty(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::Array(arr) => Ok(Value::Boolean(arr.is_empty())), @@ -129,6 +125,7 @@ fn array_is_empty(this: &Value, _args: &[Value]) -> RuntimeResult { /// Create a slice of the array #[loft_builtin(array.slice)] +// TODO: Elide with #[required] and #[types(array)] for 'this' fn array_slice(this: &Value, args: &[Value]) -> RuntimeResult { match this { Value::Array(arr) => { diff --git a/src/runtime/builtins/collections/array.rs b/src/runtime/builtins/collections/array.rs index 4b074bb..9a3a344 100644 --- a/src/runtime/builtins/collections/array.rs +++ b/src/runtime/builtins/collections/array.rs @@ -334,7 +334,7 @@ fn array_average(this: &Value, _args: &[Value]) -> RuntimeResult { fn array_join(this: &Value, args: &[Value]) -> RuntimeResult { match this { Value::Array(arr) => { - let delimiter = if let Some(arg) = args.get(0) { + let delimiter = if let Some(arg) = args.first() { match arg { Value::String(s) => s.as_str(), _ => return Err(RuntimeError::new("join() delimiter must be a string")), diff --git a/src/runtime/builtins/encoding.rs b/src/runtime/builtins/encoding.rs index a0e4216..65d84ec 100644 --- a/src/runtime/builtins/encoding.rs +++ b/src/runtime/builtins/encoding.rs @@ -2,25 +2,16 @@ use crate::runtime::builtin::{BuiltinMethod, BuiltinStruct}; use crate::runtime::value::Value; use crate::runtime::{RuntimeError, RuntimeResult}; use base64::{engine::general_purpose, Engine as _}; -use loft_builtin_macros::loft_builtin; +use loft_builtin_macros::{loft_builtin, types}; use rust_decimal::Decimal; /// Encode a string to base64 #[loft_builtin(encoding.base64_encode)] +#[types(string)] fn base64_encode(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "encoding.base64_encode() requires a string argument", - )); - } - let input = match &args[0] { Value::String(s) => s.as_bytes(), - _ => { - return Err(RuntimeError::new( - "encoding.base64_encode() argument must be a string", - )) - } + _ => unreachable!(), }; let encoded = general_purpose::STANDARD.encode(input); @@ -29,20 +20,11 @@ fn base64_encode(_this: &Value, args: &[Value]) -> RuntimeResult { /// Decode a base64 string #[loft_builtin(encoding.base64_decode)] +#[types(string)] fn base64_decode(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "encoding.base64_decode() requires a string argument", - )); - } - let input = match &args[0] { Value::String(s) => s, - _ => { - return Err(RuntimeError::new( - "encoding.base64_decode() argument must be a string", - )) - } + _ => unreachable!(), }; let decoded = general_purpose::STANDARD diff --git a/src/runtime/builtins/ffi.rs b/src/runtime/builtins/ffi.rs index b9eee52..9d1ad92 100644 --- a/src/runtime/builtins/ffi.rs +++ b/src/runtime/builtins/ffi.rs @@ -2,6 +2,7 @@ use crate::runtime::builtin::BuiltinStruct; use crate::runtime::value::Value; use crate::runtime::{RuntimeError, RuntimeResult}; use libloading::{Library, Symbol}; +use loft_builtin_macros::{loft_builtin, types}; use rust_decimal::Decimal; use std::collections::HashMap; use std::sync::{Arc, Mutex, MutexGuard}; @@ -80,14 +81,15 @@ fn f32_to_value(result: f32) -> RuntimeResult { /// ```loft /// let lib = ffi.load("libm.so.6"); /// ``` +#[loft_builtin(ffi.load)] +#[types(string)] pub fn ffi_load(_this: &Value, args: &[Value]) -> RuntimeResult { if args.is_empty() { - return Err(RuntimeError::new("ffi.load() requires a library path")); + return Err(RuntimeError::new("ffi_load requires a library path")); } - let path = match &args[0] { Value::String(s) => s.clone(), - _ => return Err(RuntimeError::new("Library path must be a string")), + _ => unreachable!(), }; // Try to get from cache first, or load and cache it diff --git a/src/runtime/builtins/io/fs.rs b/src/runtime/builtins/io/fs.rs index 53c9352..9b8e29e 100644 --- a/src/runtime/builtins/io/fs.rs +++ b/src/runtime/builtins/io/fs.rs @@ -2,66 +2,52 @@ use crate::runtime::builtin::{BuiltinMethod, BuiltinStruct}; use crate::runtime::permission_context::{check_read_permission, check_write_permission}; use crate::runtime::value::Value; use crate::runtime::{RuntimeError, RuntimeResult}; -use loft_builtin_macros::loft_builtin; +use loft_builtin_macros::{loft_builtin, types}; use std::fs; use std::path::Path; /// Read entire file contents as a string #[loft_builtin(fs.read)] +#[types(string)] fn fs_read_file(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("fs.read() requires a file path argument")); - } - match &args[0] { Value::String(path) => { // Check read permission - check_read_permission(path, Some("fs.read()")).map_err(|e| RuntimeError::new(e))?; + check_read_permission(path, Some("fs.read()")).map_err(RuntimeError::new)?; fs::read_to_string(path) .map(Value::String) .map_err(|e| RuntimeError::new(format!("Failed to read file: {}", e))) } - _ => Err(RuntimeError::new("fs.read() argument must be a string")), + _ => unreachable!(), } } /// Write string contents to a file #[loft_builtin(fs.write)] +#[types(string, string)] fn fs_write_file(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.len() < 2 { - return Err(RuntimeError::new( - "fs.write() requires path and content arguments", - )); - } - match (&args[0], &args[1]) { (Value::String(path), Value::String(content)) => { // Check write permission - check_write_permission(path, Some("fs.write()")).map_err(|e| RuntimeError::new(e))?; + check_write_permission(path, Some("fs.write()")).map_err(RuntimeError::new)?; fs::write(path, content) .map(|_| Value::Unit) .map_err(|e| RuntimeError::new(format!("Failed to write file: {}", e))) } - (Value::String(_), _) => Err(RuntimeError::new("fs.write() content must be a string")), - _ => Err(RuntimeError::new("fs.write() path must be a string")), + _ => unreachable!(), } } /// Append string contents to a file #[loft_builtin(fs.append)] +#[types(string, string)] fn fs_append_file(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.len() < 2 { - return Err(RuntimeError::new( - "fs.append() requires path and content arguments", - )); - } - match (&args[0], &args[1]) { (Value::String(path), Value::String(content)) => { // Check write permission - check_write_permission(path, Some("fs.append()")).map_err(|e| RuntimeError::new(e))?; + check_write_permission(path, Some("fs.append()")).map_err(RuntimeError::new)?; use std::fs::OpenOptions; use std::io::Write; @@ -74,26 +60,22 @@ fn fs_append_file(_this: &Value, args: &[Value]) -> RuntimeResult { .map(|_| Value::Unit) .map_err(|e| RuntimeError::new(format!("Failed to append to file: {}", e))) } - (Value::String(_), _) => Err(RuntimeError::new("fs.append() content must be a string")), - _ => Err(RuntimeError::new("fs.append() path must be a string")), + _ => unreachable!(), } } /// Check if a file exists #[loft_builtin(fs.exists)] +#[types(string)] fn fs_exists(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("fs.exists() requires a path argument")); - } - match &args[0] { Value::String(path) => { // Check read permission - check_read_permission(path, Some("fs.exists()")).map_err(|e| RuntimeError::new(e))?; + check_read_permission(path, Some("fs.exists()")).map_err(RuntimeError::new)?; Ok(Value::Boolean(Path::new(path).exists())) } - _ => Err(RuntimeError::new("fs.exists() argument must be a string")), + _ => unreachable!(), } } @@ -107,7 +89,7 @@ fn fs_is_file(_this: &Value, args: &[Value]) -> RuntimeResult { match &args[0] { Value::String(path) => { // Check read permission - check_read_permission(path, Some("fs.is_file()")).map_err(|e| RuntimeError::new(e))?; + check_read_permission(path, Some("fs.is_file()")).map_err(RuntimeError::new)?; Ok(Value::Boolean(Path::new(path).is_file())) } @@ -125,7 +107,7 @@ fn fs_is_dir(_this: &Value, args: &[Value]) -> RuntimeResult { match &args[0] { Value::String(path) => { // Check read permission - check_read_permission(path, Some("fs.is_dir()")).map_err(|e| RuntimeError::new(e))?; + check_read_permission(path, Some("fs.is_dir()")).map_err(RuntimeError::new)?; Ok(Value::Boolean(Path::new(path).is_dir())) } @@ -146,7 +128,7 @@ fn fs_create_dir(_this: &Value, args: &[Value]) -> RuntimeResult { Value::String(path) => { // Check write permission check_write_permission(path, Some("fs.create_dir()")) - .map_err(|e| RuntimeError::new(e))?; + .map_err(RuntimeError::new)?; fs::create_dir_all(path) .map(|_| Value::Unit) @@ -171,7 +153,7 @@ fn fs_remove_file(_this: &Value, args: &[Value]) -> RuntimeResult { Value::String(path) => { // Check write permission check_write_permission(path, Some("fs.remove_file()")) - .map_err(|e| RuntimeError::new(e))?; + .map_err(RuntimeError::new)?; fs::remove_file(path) .map(|_| Value::Unit) @@ -196,7 +178,7 @@ fn fs_remove_dir(_this: &Value, args: &[Value]) -> RuntimeResult { Value::String(path) => { // Check write permission check_write_permission(path, Some("fs.remove_dir()")) - .map_err(|e| RuntimeError::new(e))?; + .map_err(RuntimeError::new)?; fs::remove_dir_all(path) .map(|_| Value::Unit) @@ -218,7 +200,7 @@ fn fs_list_dir(_this: &Value, args: &[Value]) -> RuntimeResult { match &args[0] { Value::String(path) => { // Check read permission - check_read_permission(path, Some("fs.list_dir()")).map_err(|e| RuntimeError::new(e))?; + check_read_permission(path, Some("fs.list_dir()")).map_err(RuntimeError::new)?; fs::read_dir(path) .map_err(|e| RuntimeError::new(format!("Failed to read directory: {}", e))) @@ -258,9 +240,9 @@ fn fs_copy(_this: &Value, args: &[Value]) -> RuntimeResult { match (&args[0], &args[1]) { (Value::String(src), Value::String(dst)) => { // Check read permission for source - check_read_permission(src, Some("fs.copy()")).map_err(|e| RuntimeError::new(e))?; + check_read_permission(src, Some("fs.copy()")).map_err(RuntimeError::new)?; // Check write permission for destination - check_write_permission(dst, Some("fs.copy()")).map_err(|e| RuntimeError::new(e))?; + check_write_permission(dst, Some("fs.copy()")).map_err(RuntimeError::new)?; fs::copy(src, dst) .map(|_| Value::Unit) @@ -282,8 +264,8 @@ fn fs_rename(_this: &Value, args: &[Value]) -> RuntimeResult { match (&args[0], &args[1]) { (Value::String(src), Value::String(dst)) => { // Check write permission for both source and destination - check_write_permission(src, Some("fs.rename()")).map_err(|e| RuntimeError::new(e))?; - check_write_permission(dst, Some("fs.rename()")).map_err(|e| RuntimeError::new(e))?; + check_write_permission(src, Some("fs.rename()")).map_err(RuntimeError::new)?; + check_write_permission(dst, Some("fs.rename()")).map_err(RuntimeError::new)?; fs::rename(src, dst) .map(|_| Value::Unit) @@ -303,19 +285,19 @@ fn fs_metadata(_this: &Value, args: &[Value]) -> RuntimeResult { match &args[0] { Value::String(path) => { // Check read permission - check_read_permission(path, Some("fs.metadata()")).map_err(|e| RuntimeError::new(e))?; + check_read_permission(path, Some("fs.metadata()")).map_err(RuntimeError::new)?; fs::metadata(path) .map_err(|e| RuntimeError::new(format!("Failed to get metadata: {}", e))) - .and_then(|metadata| { + .map(|metadata| { use rust_decimal::Decimal; // Return an array with [size, is_file, is_dir] - Ok(Value::Array(vec![ + Value::Array(vec![ Value::Number(Decimal::from(metadata.len())), Value::Boolean(metadata.is_file()), Value::Boolean(metadata.is_dir()), - ])) + ]) }) } _ => Err(RuntimeError::new("fs.metadata() argument must be a string")), diff --git a/src/runtime/builtins/json.rs b/src/runtime/builtins/json.rs index 84a7138..cd680fc 100644 --- a/src/runtime/builtins/json.rs +++ b/src/runtime/builtins/json.rs @@ -1,21 +1,17 @@ use crate::runtime::builtin::{BuiltinMethod, BuiltinStruct}; use crate::runtime::value::Value; use crate::runtime::{RuntimeError, RuntimeResult}; -use loft_builtin_macros::loft_builtin; +use loft_builtin_macros::{loft_builtin, required}; use rust_decimal::Decimal; use serde_json; use std::collections::HashMap; /// Parse a JSON string into a loft value #[loft_builtin(json.parse)] -fn json_parse(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("json.parse() requires a string argument")); - } - +fn json_parse(#[required] _this: &Value, #[types(string)] args: &[Value]) -> RuntimeResult { let json_str = match &args[0] { Value::String(s) => s, - _ => return Err(RuntimeError::new("json.parse() argument must be a string")), + _ => unreachable!(), }; let json_value: serde_json::Value = serde_json::from_str(json_str) @@ -26,13 +22,8 @@ fn json_parse(_this: &Value, args: &[Value]) -> RuntimeResult { /// Convert a loft value to a JSON string #[loft_builtin(json.stringify)] -fn json_stringify(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "json.stringify() requires a value argument", - )); - } - +#[required] +fn json_stringify(#[required] _this: &Value, args: &[Value]) -> RuntimeResult { let json_value = loft_value_to_json(&args[0])?; let json_str = serde_json::to_string(&json_value) .map_err(|e| RuntimeError::new(format!("Failed to stringify JSON: {}", e)))?; @@ -42,13 +33,8 @@ fn json_stringify(_this: &Value, args: &[Value]) -> RuntimeResult { /// Convert a loft value to a pretty-printed JSON string #[loft_builtin(json.stringify_pretty)] -fn json_stringify_pretty(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "json.stringify_pretty() requires a value argument", - )); - } - +#[required] +fn json_stringify_pretty(#[required] _this: &Value, args: &[Value]) -> RuntimeResult { let json_value = loft_value_to_json(&args[0])?; let json_str = serde_json::to_string_pretty(&json_value) .map_err(|e| RuntimeError::new(format!("Failed to stringify JSON: {}", e)))?; diff --git a/src/runtime/builtins/math/basic.rs b/src/runtime/builtins/math/basic.rs index 39fd013..ab3fa5e 100644 --- a/src/runtime/builtins/math/basic.rs +++ b/src/runtime/builtins/math/basic.rs @@ -6,75 +6,55 @@ use rust_decimal::Decimal; /// Round a number to the nearest integer #[loft_builtin(math.round)] -fn math_round(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.round() requires a number argument")); - } - +fn math_round(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let rounded = n.round(); Ok(Value::Number(rounded)) } - _ => Err(RuntimeError::new("math.round() argument must be a number")), + _ => unreachable!(), } } /// Floor a number (round down to nearest integer) #[loft_builtin(math.floor)] -fn math_floor(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.floor() requires a number argument")); - } - +fn math_floor(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let floored = n.floor(); Ok(Value::Number(floored)) } - _ => Err(RuntimeError::new("math.floor() argument must be a number")), + _ => unreachable!(), } } /// Ceiling a number (round up to nearest integer) #[loft_builtin(math.ceil)] -fn math_ceil(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.ceil() requires a number argument")); - } - +fn math_ceil(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let ceiled = n.ceil(); Ok(Value::Number(ceiled)) } - _ => Err(RuntimeError::new("math.ceil() argument must be a number")), + _ => unreachable!(), } } /// Absolute value of a number #[loft_builtin(math.abs)] -fn math_abs(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.abs() requires a number argument")); - } - +fn math_abs(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let abs = n.abs(); Ok(Value::Number(abs)) } - _ => Err(RuntimeError::new("math.abs() argument must be a number")), + _ => unreachable!(), } } /// Sign of a number (-1, 0, or 1) #[loft_builtin(math.sign)] -fn math_sign(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.sign() requires a number argument")); - } - +fn math_sign(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { if n.is_zero() { @@ -85,13 +65,13 @@ fn math_sign(_this: &Value, args: &[Value]) -> RuntimeResult { Ok(Value::Number(Decimal::ONE)) } } - _ => Err(RuntimeError::new("math.sign() argument must be a number")), + _ => unreachable!(), } } /// Minimum of two numbers #[loft_builtin(math.min)] -fn math_min(_this: &Value, args: &[Value]) -> RuntimeResult { +fn math_min(#[required] _this: &Value, #[types(number*)] args: &[Value]) -> RuntimeResult { if args.len() < 2 { return Err(RuntimeError::new( "math.min() requires two number arguments", diff --git a/src/runtime/builtins/math/exponential.rs b/src/runtime/builtins/math/exponential.rs index 9b146cb..d0460c1 100644 --- a/src/runtime/builtins/math/exponential.rs +++ b/src/runtime/builtins/math/exponential.rs @@ -7,13 +7,7 @@ use rust_decimal::Decimal; /// Power: base^exponent #[loft_builtin(math.pow)] -fn math_pow(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.len() < 2 { - return Err(RuntimeError::new( - "math.pow() requires base and exponent arguments", - )); - } - +fn math_pow(#[required] _this: &Value, #[types(number, number)] args: &[Value]) -> RuntimeResult { match (&args[0], &args[1]) { (Value::Number(base), Value::Number(exp)) => { let base_f64 = base @@ -28,17 +22,13 @@ fn math_pow(_this: &Value, args: &[Value]) -> RuntimeResult { .map(Value::Number) .ok_or_else(|| RuntimeError::new("Result too large or invalid")) } - _ => Err(RuntimeError::new("math.pow() arguments must be numbers")), + _ => unreachable!(), } } /// Square root #[loft_builtin(math.sqrt)] -fn math_sqrt(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.sqrt() requires a number argument")); - } - +fn math_sqrt(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let n_f64 = n @@ -55,17 +45,13 @@ fn math_sqrt(_this: &Value, args: &[Value]) -> RuntimeResult { .map(Value::Number) .ok_or_else(|| RuntimeError::new("Result invalid")) } - _ => Err(RuntimeError::new("math.sqrt() argument must be a number")), + _ => unreachable!(), } } /// Exponential function (e^x) #[loft_builtin(math.exp)] -fn math_exp(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.exp() requires a number argument")); - } - +fn math_exp(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let n_f64 = n @@ -77,17 +63,13 @@ fn math_exp(_this: &Value, args: &[Value]) -> RuntimeResult { .map(Value::Number) .ok_or_else(|| RuntimeError::new("Result too large or invalid")) } - _ => Err(RuntimeError::new("math.exp() argument must be a number")), + _ => unreachable!(), } } /// Natural logarithm (ln) #[loft_builtin(math.ln)] -fn math_ln(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.ln() requires a number argument")); - } - +fn math_ln(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let n_f64 = n diff --git a/src/runtime/builtins/math/trigonometry.rs b/src/runtime/builtins/math/trigonometry.rs index 6958389..f8a3f28 100644 --- a/src/runtime/builtins/math/trigonometry.rs +++ b/src/runtime/builtins/math/trigonometry.rs @@ -7,11 +7,7 @@ use rust_decimal::Decimal; /// Sine function #[loft_builtin(math.sin)] -fn math_sin(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.sin() requires a number argument")); - } - +fn math_sin(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let n_f64 = n @@ -23,17 +19,13 @@ fn math_sin(_this: &Value, args: &[Value]) -> RuntimeResult { .map(Value::Number) .ok_or_else(|| RuntimeError::new("Result invalid")) } - _ => Err(RuntimeError::new("math.sin() argument must be a number")), + _ => unreachable!(), } } /// Cosine function #[loft_builtin(math.cos)] -fn math_cos(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.cos() requires a number argument")); - } - +fn math_cos(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let n_f64 = n @@ -45,17 +37,13 @@ fn math_cos(_this: &Value, args: &[Value]) -> RuntimeResult { .map(Value::Number) .ok_or_else(|| RuntimeError::new("Result invalid")) } - _ => Err(RuntimeError::new("math.cos() argument must be a number")), + _ => unreachable!(), } } /// Tangent function #[loft_builtin(math.tan)] -fn math_tan(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.tan() requires a number argument")); - } - +fn math_tan(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let n_f64 = n @@ -67,23 +55,19 @@ fn math_tan(_this: &Value, args: &[Value]) -> RuntimeResult { .map(Value::Number) .ok_or_else(|| RuntimeError::new("Result invalid")) } - _ => Err(RuntimeError::new("math.tan() argument must be a number")), + _ => unreachable!(), } } /// Arcsine function #[loft_builtin(math.asin)] -fn math_asin(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("math.asin() requires a number argument")); - } - +fn math_asin(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Number(n) => { let n_f64 = n .to_f64() .ok_or_else(|| RuntimeError::new("Invalid number"))?; - if n_f64 < -1.0 || n_f64 > 1.0 { + if !(-1.0..=1.0).contains(&n_f64) { return Err(RuntimeError::new( "math.asin() argument must be between -1 and 1", )); @@ -94,7 +78,7 @@ fn math_asin(_this: &Value, args: &[Value]) -> RuntimeResult { .map(Value::Number) .ok_or_else(|| RuntimeError::new("Result invalid")) } - _ => Err(RuntimeError::new("math.asin() argument must be a number")), + _ => unreachable!(), } } @@ -110,7 +94,7 @@ fn math_acos(_this: &Value, args: &[Value]) -> RuntimeResult { let n_f64 = n .to_f64() .ok_or_else(|| RuntimeError::new("Invalid number"))?; - if n_f64 < -1.0 || n_f64 > 1.0 { + if !(-1.0..=1.0).contains(&n_f64) { return Err(RuntimeError::new( "math.acos() argument must be between -1 and 1", )); diff --git a/src/runtime/builtins/object.rs b/src/runtime/builtins/object.rs index 6664d18..cfd5fc2 100644 --- a/src/runtime/builtins/object.rs +++ b/src/runtime/builtins/object.rs @@ -1,59 +1,37 @@ use crate::runtime::builtin::{BuiltinMethod, BuiltinStruct}; use crate::runtime::value::Value; use crate::runtime::{RuntimeError, RuntimeResult}; -use loft_builtin_macros::loft_builtin; +use loft_builtin_macros::{loft_builtin, required, types}; use rust_decimal::Decimal; use std::collections::HashMap; /// Get all keys from an object #[loft_builtin(object.keys)] -fn object_keys(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "object.keys() requires an object argument", - )); - } - +fn object_keys(#[required] _this: &Value, #[types(object)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Struct { fields, .. } => { let keys: Vec = fields.keys().map(|k| Value::String(k.clone())).collect(); Ok(Value::Array(keys)) } - _ => Err(RuntimeError::new( - "object.keys() argument must be an object", - )), + _ => unreachable!(), } } /// Get all values from an object #[loft_builtin(object.values)] -fn object_values(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "object.values() requires an object argument", - )); - } - +fn object_values(#[required] _this: &Value, #[types(object)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Struct { fields, .. } => { let values: Vec = fields.values().cloned().collect(); Ok(Value::Array(values)) } - _ => Err(RuntimeError::new( - "object.values() argument must be an object", - )), + _ => unreachable!(), } } /// Get all entries from an object as [key, value] pairs #[loft_builtin(object.entries)] -fn object_entries(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "object.entries() requires an object argument", - )); - } - +fn object_entries(#[required] _this: &Value, #[types(object)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Struct { fields, .. } => { let entries: Vec = fields @@ -62,54 +40,44 @@ fn object_entries(_this: &Value, args: &[Value]) -> RuntimeResult { .collect(); Ok(Value::Array(entries)) } - _ => Err(RuntimeError::new( - "object.entries() argument must be an object", - )), + _ => unreachable!(), } } /// Check if object has a property #[loft_builtin(object.has)] -fn object_has(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.len() < 2 { - return Err(RuntimeError::new( - "object.has() requires object and key arguments", - )); - } - +fn object_has(#[required] _this: &Value, #[types(object, string)] args: &[Value]) -> RuntimeResult { match (&args[0], &args[1]) { (Value::Struct { fields, .. }, Value::String(key)) => { Ok(Value::Boolean(fields.contains_key(key))) } - (Value::Struct { .. }, _) => Err(RuntimeError::new("object.has() key must be a string")), - _ => Err(RuntimeError::new( - "object.has() first argument must be an object", - )), + _ => unreachable!(), } } /// Assign properties from source objects to target object #[loft_builtin(object.assign)] +#[required] +#[types(type*)] fn object_assign(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "object.assign() requires at least one argument", - )); - } - - let mut result_fields = HashMap::new(); + let result_fields; // Start with first object if let Value::Struct { fields, .. } = &args[0] { result_fields = fields.clone(); + } else { + return Err(RuntimeError::new("object.assign() target must be an object")); } // Merge in additional objects + let mut result_fields = result_fields; for arg in &args[1..] { if let Value::Struct { fields, .. } = arg { for (key, value) in fields { result_fields.insert(key.clone(), value.clone()); } + } else { + return Err(RuntimeError::new("object.assign() sources must be objects")); } } @@ -121,13 +89,9 @@ fn object_assign(_this: &Value, args: &[Value]) -> RuntimeResult { /// Create an object from entries [[key, value], ...] #[loft_builtin(object.from_entries)] +#[required] +#[types(array)] fn object_from_entries(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "object.from_entries() requires an entries array", - )); - } - match &args[0] { Value::Array(entries) => { let mut fields = HashMap::new(); @@ -150,26 +114,18 @@ fn object_from_entries(_this: &Value, args: &[Value]) -> RuntimeResult { fields, }) } - _ => Err(RuntimeError::new( - "object.from_entries() argument must be an array", - )), + _ => unreachable!(), } } /// Get the number of properties in an object #[loft_builtin(object.size)] +#[required] +#[types(object)] fn object_size(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "object.size() requires an object argument", - )); - } - match &args[0] { Value::Struct { fields, .. } => Ok(Value::Number(Decimal::from(fields.len()))), - _ => Err(RuntimeError::new( - "object.size() argument must be an object", - )), + _ => unreachable!(), } } diff --git a/src/runtime/builtins/random.rs b/src/runtime/builtins/random.rs index 364eb8a..08e5034 100644 --- a/src/runtime/builtins/random.rs +++ b/src/runtime/builtins/random.rs @@ -45,25 +45,19 @@ fn random_random(_this: &Value, _args: &[Value]) -> RuntimeResult { /// Generate a random integer in range [min, max) #[loft_builtin(random.range)] -fn random_range(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.len() < 2 { - return Err(RuntimeError::new( - "random.range() requires min and max arguments", - )); - } - +fn random_range(#[required] _this: &Value, #[types(number, number)] args: &[Value]) -> RuntimeResult { let min = match &args[0] { Value::Number(n) => n .to_i64() .ok_or_else(|| RuntimeError::new("min must be an integer"))?, - _ => return Err(RuntimeError::new("random.range() min must be a number")), + _ => unreachable!(), }; let max = match &args[1] { Value::Number(n) => n .to_i64() .ok_or_else(|| RuntimeError::new("max must be an integer"))?, - _ => return Err(RuntimeError::new("random.range() max must be a number")), + _ => unreachable!(), }; if min >= max { @@ -80,13 +74,7 @@ fn random_range(_this: &Value, args: &[Value]) -> RuntimeResult { /// Pick a random element from an array #[loft_builtin(random.choice)] -fn random_choice(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "random.choice() requires an array argument", - )); - } - +fn random_choice(#[required] _this: &Value, #[types(array)] args: &[Value]) -> RuntimeResult { match &args[0] { Value::Array(arr) => { if arr.is_empty() { @@ -201,7 +189,7 @@ mod tests { match result.unwrap() { Value::Number(n) => { let val = n.to_i64().unwrap(); - assert!(val >= 1 && val < 10); + assert!((1..10).contains(&val)); } _ => panic!("Expected number"), } diff --git a/src/runtime/builtins/string/mod.rs b/src/runtime/builtins/string/mod.rs index 61c2af5..4a5112c 100644 --- a/src/runtime/builtins/string/mod.rs +++ b/src/runtime/builtins/string/mod.rs @@ -6,11 +6,7 @@ use rust_decimal::Decimal; /// Split a string by a delimiter #[loft_builtin(string.split)] -fn string_split(this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("split() requires a delimiter argument")); - } - +fn string_split(#[required] this: &Value, #[types(string)] args: &[Value]) -> RuntimeResult { match (this, &args[0]) { (Value::String(s), Value::String(delim)) => { let parts: Vec = s @@ -19,20 +15,13 @@ fn string_split(this: &Value, args: &[Value]) -> RuntimeResult { .collect(); Ok(Value::Array(parts)) } - (Value::String(_), _) => Err(RuntimeError::new("split() delimiter must be a string")), - _ => Err(RuntimeError::new("split() can only be called on strings")), + _ => unreachable!(), } } /// Join an array of strings with a delimiter #[loft_builtin(string.join)] -fn string_join(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.len() < 2 { - return Err(RuntimeError::new( - "join() requires an array and delimiter arguments", - )); - } - +fn string_join(#[required] _this: &Value, #[types(array, string)] args: &[Value]) -> RuntimeResult { match (&args[0], &args[1]) { (Value::Array(arr), Value::String(delim)) => { let strings: Result, RuntimeError> = arr @@ -50,13 +39,13 @@ fn string_join(_this: &Value, args: &[Value]) -> RuntimeResult { let strings = strings?; Ok(Value::String(strings.join(delim))) } - (Value::Array(_), _) => Err(RuntimeError::new("join() delimiter must be a string")), - _ => Err(RuntimeError::new("join() first argument must be an array")), + _ => unreachable!(), } } /// Trim whitespace from both ends of a string #[loft_builtin(string.trim)] +// TODO: Elide with #[required] once 'this' support is verified fn string_trim(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::String(s) => Ok(Value::String(s.trim().to_string())), @@ -66,6 +55,7 @@ fn string_trim(this: &Value, _args: &[Value]) -> RuntimeResult { /// Trim whitespace from the start of a string #[loft_builtin(string.trim_start)] +// TODO: Elide with #[required] once 'this' support is verified fn string_trim_start(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::String(s) => Ok(Value::String(s.trim_start().to_string())), @@ -77,6 +67,7 @@ fn string_trim_start(this: &Value, _args: &[Value]) -> RuntimeResult { /// Trim whitespace from the end of a string #[loft_builtin(string.trim_end)] +// TODO: Elide with #[required] once 'this' support is verified fn string_trim_end(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::String(s) => Ok(Value::String(s.trim_end().to_string())), @@ -88,6 +79,7 @@ fn string_trim_end(this: &Value, _args: &[Value]) -> RuntimeResult { /// Replace all occurrences of a substring with another #[loft_builtin(string.replace)] +// TODO: Elide with #[required] and #[types(string, string)] fn string_replace(this: &Value, args: &[Value]) -> RuntimeResult { if args.len() < 2 { return Err(RuntimeError::new( @@ -106,6 +98,7 @@ fn string_replace(this: &Value, args: &[Value]) -> RuntimeResult { /// Convert string to uppercase #[loft_builtin(string.to_upper)] +// TODO: Elide with #[required] once 'this' support is verified fn string_to_upper(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::String(s) => Ok(Value::String(s.to_uppercase())), @@ -117,6 +110,7 @@ fn string_to_upper(this: &Value, _args: &[Value]) -> RuntimeResult { /// Convert string to lowercase #[loft_builtin(string.to_lower)] +// TODO: Elide with #[required] once 'this' support is verified fn string_to_lower(this: &Value, _args: &[Value]) -> RuntimeResult { match this { Value::String(s) => Ok(Value::String(s.to_lowercase())), @@ -128,6 +122,7 @@ fn string_to_lower(this: &Value, _args: &[Value]) -> RuntimeResult { /// Check if string starts with a prefix #[loft_builtin(string.starts_with)] +// TODO: Elide with #[required] and #[types(string)] fn string_starts_with(this: &Value, args: &[Value]) -> RuntimeResult { if args.is_empty() { return Err(RuntimeError::new( @@ -148,6 +143,7 @@ fn string_starts_with(this: &Value, args: &[Value]) -> RuntimeResult { /// Check if string ends with a suffix #[loft_builtin(string.ends_with)] +// TODO: Elide with #[required] and #[types(string)] fn string_ends_with(this: &Value, args: &[Value]) -> RuntimeResult { if args.is_empty() { return Err(RuntimeError::new("ends_with() requires a suffix argument")); diff --git a/src/runtime/builtins/term.rs b/src/runtime/builtins/term.rs index c7bf491..fec8eba 100644 --- a/src/runtime/builtins/term.rs +++ b/src/runtime/builtins/term.rs @@ -66,7 +66,7 @@ fn term_size(_this: &Value, _args: &[Value]) -> RuntimeResult { use std::process::Command; // Check permission to run tput command - check_run_permission("tput", Some("term.size()")).map_err(|e| RuntimeError::new(e))?; + check_run_permission("tput", Some("term.size()")).map_err(RuntimeError::new)?; // Try to get terminal size using tput let width_output = Command::new("tput") diff --git a/src/runtime/builtins/test.rs b/src/runtime/builtins/test.rs index 374be37..95534ba 100644 --- a/src/runtime/builtins/test.rs +++ b/src/runtime/builtins/test.rs @@ -5,14 +5,10 @@ use loft_builtin_macros::loft_builtin; /// Assert that a condition is true #[loft_builtin(test.assert)] -pub fn test_assert(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new("test.assert() requires at least one argument")); - } - +pub fn test_assert(#[required] _this: &Value, #[types(bool*)] args: &[Value]) -> RuntimeResult { let condition = match &args[0] { Value::Boolean(b) => *b, - _ => return Err(RuntimeError::new("test.assert() first argument must be a boolean")), + _ => unreachable!(), }; if !condition { @@ -32,7 +28,7 @@ pub fn test_assert(_this: &Value, args: &[Value]) -> RuntimeResult { /// Assert that two values are equal #[loft_builtin(test.assert_eq)] -pub fn test_assert_eq(_this: &Value, args: &[Value]) -> RuntimeResult { +pub fn test_assert_eq(#[required] _this: &Value, #[required] args: &[Value]) -> RuntimeResult { if args.len() < 2 { return Err(RuntimeError::new("test.assert_eq() requires two arguments")); } diff --git a/src/runtime/builtins/time.rs b/src/runtime/builtins/time.rs index 2bf75ca..7e1a9a2 100644 --- a/src/runtime/builtins/time.rs +++ b/src/runtime/builtins/time.rs @@ -9,23 +9,13 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; /// Sleep for the specified number of milliseconds and return a promise #[loft_builtin(time.sleep)] -fn time_sleep(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "time.sleep() requires a duration argument", - )); - } - +fn time_sleep(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { let duration_ms = match &args[0] { Value::Number(n) => { - let ms = n.to_f64().unwrap_or(0.0) as u64; - ms - } - _ => { - return Err(RuntimeError::new( - "time.sleep() argument must be a number (milliseconds)", - )) + + n.to_f64().unwrap_or(0.0) as u64 } + _ => unreachable!(), }; // Actually sleep (this blocks the current thread) @@ -54,7 +44,7 @@ fn time_perf_now(_this: &Value, _args: &[Value]) -> RuntimeResult { // In a real implementation, this would be more sophisticated static START_TIME: std::sync::OnceLock = std::sync::OnceLock::new(); - let start = START_TIME.get_or_init(|| Instant::now()); + let start = START_TIME.get_or_init(Instant::now); let elapsed = start.elapsed(); let millis = elapsed.as_millis() as f64; @@ -65,20 +55,10 @@ fn time_perf_now(_this: &Value, _args: &[Value]) -> RuntimeResult { /// Format a duration in milliseconds to a human-readable string #[loft_builtin(time.format)] -fn time_format(_this: &Value, args: &[Value]) -> RuntimeResult { - if args.is_empty() { - return Err(RuntimeError::new( - "time.format() requires a duration argument", - )); - } - +fn time_format(#[required] _this: &Value, #[types(number)] args: &[Value]) -> RuntimeResult { let duration_ms = match &args[0] { Value::Number(n) => n.to_f64().unwrap_or(0.0), - _ => { - return Err(RuntimeError::new( - "time.format() argument must be a number (milliseconds)", - )) - } + _ => unreachable!(), }; let formatted = if duration_ms < 1000.0 { diff --git a/src/runtime/builtins/web/mod.rs b/src/runtime/builtins/web/mod.rs index ad97f3e..7197ca4 100644 --- a/src/runtime/builtins/web/mod.rs +++ b/src/runtime/builtins/web/mod.rs @@ -450,7 +450,7 @@ fn web_send(this: &Value, _args: &[Value]) -> RuntimeResult { .unwrap_or_else(|_| url.clone()); // Check network permission - check_net_permission(&host, Some("web.send()")).map_err(|e| RuntimeError::new(e))?; + check_net_permission(&host, Some("web.send()")).map_err(RuntimeError::new)?; let method_str = if let Some(Value::String(method)) = fields.get("method") { method.clone() diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 72852d5..8e7c5f5 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -228,7 +228,7 @@ pub type RuntimeResult = Result; pub struct RuntimeError { pub message: String, pub path: Option, - pub source: Option>, + pub source: Option>>, pub position: Option, pub len: Option, } @@ -254,7 +254,7 @@ impl RuntimeError { Self { message: message.into(), path: Some(path.clone()), - source: Some(NamedSource::new(path, source_code)), + source: Some(Box::new(NamedSource::new(path, source_code))), position: Some(position), len: Some(len), } @@ -270,7 +270,7 @@ impl RuntimeError { Self { message: message.into(), path: Some(path.clone()), - source: Some(NamedSource::new(path, source_code)), + source: Some(Box::new(NamedSource::new(path, source_code))), position: None, len: None, } @@ -291,7 +291,7 @@ impl Diagnostic for RuntimeError { } fn source_code(&self) -> Option<&dyn miette::SourceCode> { - self.source.as_ref().map(|s| s as &dyn miette::SourceCode) + self.source.as_ref().map(|s| s.as_ref() as &dyn miette::SourceCode) } fn labels(&self) -> Option + '_>> { @@ -316,6 +316,12 @@ pub struct Environment { scopes: Vec>, } +impl Default for Environment { + fn default() -> Self { + Self::new() + } +} + impl Environment { pub fn new() -> Self { Self { @@ -555,8 +561,10 @@ fn opt_res_expect(this: &Value, args: &[Value]) -> RuntimeResult { // ────────────────────────────────────────────────────────────────────────────── -fn init_builtin_enums() -> HashMap>)>> { - let mut enums: HashMap>)>> = HashMap::new(); +type BuiltinEnums = HashMap>)>>; + +fn init_builtin_enums() -> BuiltinEnums { + let mut enums: BuiltinEnums = HashMap::new(); // Option: Some(T) | None enums.insert( @@ -588,6 +596,13 @@ fn init_builtin_enums() -> HashMap>)>> { enums } +type ImplMethod = ( + Vec<(String, crate::parser::Type)>, + Option, + Box, + Option, +); + pub struct Interpreter { pub env: Environment, source_path: Option, @@ -596,21 +611,10 @@ pub struct Interpreter { traits: HashMap>, // Track impl blocks: type_name -> method_name -> (params, return_type, body) // Format: type_name -> method_name -> (params, return_type, body, trait_name_if_any) - impl_methods: HashMap< - String, - HashMap< - String, - ( - Vec<(String, crate::parser::Type)>, - Option, - Box, - Option, - ), - >, - >, + impl_methods: HashMap>, // Track enum declarations: enum_name -> variants // Format: enum_name -> Vec<(variant_name, Option>)> - enums: HashMap>)>>, + enums: BuiltinEnums, // Module cache: module_path -> exported_values module_cache: HashMap>, // Current module's exports @@ -621,6 +625,12 @@ pub struct Interpreter { returning: Option, } +impl Default for Interpreter { + fn default() -> Self { + Self::new() + } +} + impl Interpreter { pub fn new() -> Self { let mut env = Environment::new(); @@ -693,7 +703,7 @@ impl Interpreter { match name.as_str() { "all" => args.iter().all(|arg| eval_gated(arg, enabled)), "any" => args.iter().any(|arg| eval_gated(arg, enabled)), - "not" => args.get(0).map_or(false, |arg| !eval_gated(arg, enabled)), + "not" => args.first().is_some_and(|arg| !eval_gated(arg, enabled)), _ => false, } } else { @@ -926,7 +936,7 @@ impl Interpreter { let type_methods = self .impl_methods .entry(type_name) - .or_insert_with(HashMap::new); + .or_default(); // Process each method in the impl block for method_stmt in methods { From 1d9dea46ff3febef0a38240b545b8f11b77793b7 Mon Sep 17 00:00:00 2001 From: tascord Date: Sun, 1 Mar 2026 21:32:04 +1100 Subject: [PATCH 2/6] forgot to tag for release lol From 131639e43ca12df7ead8954b0bbd685859ad4b2d Mon Sep 17 00:00:00 2001 From: tascord Date: Sun, 1 Mar 2026 21:33:46 +1100 Subject: [PATCH 3/6] release: push tags after creating release branch --- scripts/release.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.sh b/scripts/release.sh index 89112b2..0f43ba9 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -67,6 +67,7 @@ else git commit -m "release: v$VERSION" echo "Pushing branch to origin..." git push origin "$BRANCH_NAME" + git push origin --tags if command -v gh &> /dev/null; then echo "Creating Pull Request..." From ec44c4a0e22a8164d45ea0c67b093d0e1d659f1b Mon Sep 17 00:00:00 2001 From: tascord Date: Sun, 1 Mar 2026 23:00:18 +1100 Subject: [PATCH 4/6] remove pr body from script --- scripts/release.sh | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index 0f43ba9..42cbbf1 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -51,18 +51,6 @@ else echo "Creating branch $BRANCH_NAME..." git checkout -b "$BRANCH_NAME" - # Generate release notes from commits since last tag - REPO="fargonesh/loft" - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -n "$LAST_TAG" ]; then - echo "Generating release notes since $LAST_TAG..." - # Format: [@author](https://github.com/author): Title ([short_hash](https://github.com/owner/repo/commit/hash)) - RELEASE_NOTES=$(git log "$LAST_TAG..HEAD" --pretty=format:"[@%an](https://github.com/%an): %s ([%h](https://github.com/$REPO/commit/%H))") - else - echo "No previous tag found. Generating release notes from all commits..." - RELEASE_NOTES=$(git log --pretty=format:"[@%an](https://github.com/%an): %s ([%h](https://github.com/$REPO/commit/%H))") - fi - git add Cargo.toml registry/Cargo.toml loft_builtin_macros/Cargo.toml Cargo.lock git commit -m "release: v$VERSION" echo "Pushing branch to origin..." From dc9a4be884b9135d09cb58674d91f6ebd62393d4 Mon Sep 17 00:00:00 2001 From: tascord Date: Sun, 1 Mar 2026 23:55:14 +1100 Subject: [PATCH 5/6] ci: add registry check job to build workflow; refactor example test function names --- .github/workflows/build.yml | 24 ++++++++++++++++++++++++ registry/src/main.rs | 8 ++++---- scripts/gen_example_tests.sh | 2 +- tests/examples.rs | 8 ++++---- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7203c0..fe8a48b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -238,6 +238,30 @@ jobs: command: clippy args: -- -D warnings + registry: + name: Registry Check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Run clippy (registry) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --manifest-path registry/Cargo.toml -- -D warnings + + - name: Run tests (registry) + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path registry/Cargo.toml check-generated-tests: name: Check generated tests are up to date diff --git a/registry/src/main.rs b/registry/src/main.rs index a621300..2f84148 100644 --- a/registry/src/main.rs +++ b/registry/src/main.rs @@ -697,7 +697,7 @@ async fn main() { let public_url = std::env::var("PUBLIC_URL").unwrap_or_else(|_| "https://loft.fargone.sh".to_string()); - let state = AppState::new(storage_dir, client_id, client_secret, public_url); + let state = AppState::new(storage_dir.clone(), client_id, client_secret, public_url); // On startup, build missing docs for all published versions if let Ok(entries) = fs::read_dir(&state.storage_dir) { @@ -827,7 +827,7 @@ async fn main() { if std::path::Path::new(&index_path).exists() { return Ok(axum::response::Response::builder() .header("Content-Type", "text/html") - .body(axum::body::boxed(axum::body::Full::from(fs::read(index_path).unwrap()))) + .body(axum::body::Body::from(fs::read(index_path).unwrap())) .unwrap()); } } @@ -841,7 +841,7 @@ async fn main() { if std::path::Path::new(&index_path).exists() { return Ok(axum::response::Response::builder() .header("Content-Type", "text/html") - .body(axum::body::boxed(axum::body::Full::from(fs::read(index_path).unwrap()))) + .body(axum::body::Body::from(fs::read(index_path).unwrap())) .unwrap()); } Err(StatusCode::NOT_FOUND) @@ -861,7 +861,7 @@ async fn main() { .route("/tokens", post(create_token).get(list_tokens)) .route("/tokens/:id", delete(revoke_token)) .route("/d/:name", get(serve_latest_docs)) - .route("/d/:name@:version", get(serve_versioned_docs)) + .route("/d/:name/:version", get(serve_versioned_docs)) .nest_service("/docs", ServeDir::new("../book/book")) .nest_service("/stdlib", ServeDir::new("../stdlib-docs")) .nest_service( diff --git a/scripts/gen_example_tests.sh b/scripts/gen_example_tests.sh index bc6867d..0cc1a8a 100755 --- a/scripts/gen_example_tests.sh +++ b/scripts/gen_example_tests.sh @@ -101,7 +101,7 @@ HEADER "$IGNORED_FILE") ignored_files+=("$name") ;; *) normal_files+=("$name") ;; esac - done < <(find "$EXAMPLES_DIR" -maxdepth 1 -name '*.lf' -print0 | sort -z) + done < <(LC_ALL=C find "$EXAMPLES_DIR" -maxdepth 1 -name '*.lf' -print0 | LC_ALL=C sort -z) # ---- normal tests -------------------------------------------------------- printf '// ---------------------------------------------------------------------------\n' diff --git a/tests/examples.rs b/tests/examples.rs index 2e2e474..f236b57 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -64,13 +64,13 @@ fn example_mutable_capture() { } #[test] -fn example_optional_return() { - run_example("examples/optional_return.lf", None); +fn example_option_result() { + run_example("examples/option_result.lf", None); } #[test] -fn example_option_result() { - run_example("examples/option_result.lf", None); +fn example_optional_return() { + run_example("examples/optional_return.lf", None); } #[test] From 6862ee68aeb3b8822827ae1d4d148684e6d74e17 Mon Sep 17 00:00:00 2001 From: tascord Date: Wed, 11 Mar 2026 17:04:28 +1100 Subject: [PATCH 6/6] whoops --- devenv.lock | 97 ++++++++++++++----------------------------- examples/for_loop.lf | 7 ++++ examples/for_loops.lf | 36 ++++++++++++++++ src/parser/mod.rs | 83 ++++++++++++++++++++++++++---------- src/runtime/mod.rs | 26 +++++++++++- www/package-lock.json | 13 +++++- 6 files changed, 169 insertions(+), 93 deletions(-) create mode 100644 examples/for_loop.lf create mode 100644 examples/for_loops.lf diff --git a/devenv.lock b/devenv.lock index f972da2..af30336 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,11 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1760162706, + "lastModified": 1773192994, + "narHash": "sha256-4Ftfp6FbRtJT/IcjO0k0XKwpwF85mJmY0Uh4srGPq0I=", "owner": "cachix", "repo": "devenv", - "rev": "0d5ad578728fe4bce66eb4398b8b1e66deceb4e4", + "rev": "0a7c4902fe1c9d19d9228e36f36bc26daaa1e39c", "type": "github" }, "original": { @@ -24,10 +25,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1760250892, + "lastModified": 1773126504, + "narHash": "sha256-/iXlg2V5UMlgCmyRHkPHjlD6NdMfFOnwFMvH7REigD4=", "owner": "nix-community", "repo": "fenix", - "rev": "b0b86e20829d1766bffb9f654d9fad47e099dc1b", + "rev": "64407ddb1932af06ed5cd711f6a2ed946b2548b9", "type": "github" }, "original": { @@ -36,74 +38,39 @@ "type": "github" } }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1747046372, - "owner": "edolstra", - "repo": "flake-compat", - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { + "nixpkgs": { "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs-src": "nixpkgs-src" }, "locked": { - "lastModified": 1759523803, + "lastModified": 1772749504, + "narHash": "sha256-eqtQIz0alxkQPym+Zh/33gdDjkkch9o6eHnMPnXFXN0=", "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "cfc9f7bb163ad8542029d303e599c0f7eee09835", + "repo": "devenv-nixpkgs", + "rev": "08543693199362c1fddb8f52126030d0d374ba2e", "type": "github" }, "original": { "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", + "ref": "rolling", + "repo": "devenv-nixpkgs", "type": "github" } }, - "nixpkgs": { + "nixpkgs-src": { + "flake": false, "locked": { - "lastModified": 1758532697, - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f", + "lastModified": 1772173633, + "narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6", "type": "github" }, "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", "type": "github" } }, @@ -111,21 +78,18 @@ "inputs": { "devenv": "devenv", "fenix": "fenix", - "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", - "pre-commit-hooks": [ - "git-hooks" - ], "rust-overlay": "rust-overlay" } }, "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1760201021, + "lastModified": 1773053271, + "narHash": "sha256-5xc4Bk4/AgIOpf8NSNJxD+vNJ9KVyDailQ1EmfMmwjE=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "6fcd20b1acd355d2d253bd6747386ed8f629b4d0", + "rev": "16e4436162aae5dbfe5b5ebb9ed0ded2305cff25", "type": "github" }, "original": { @@ -142,10 +106,11 @@ ] }, "locked": { - "lastModified": 1760236527, + "lastModified": 1773198218, + "narHash": "sha256-sxQV16GQrBEfrwuhYT9WvmFBnN8HakhRfR+JR+3qaTo=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "a38dd7f462825c75ce8567816ae38c2e7d826bfa", + "rev": "e552b1d2850f5f0a739bba27c6463af1a29e2f4e", "type": "github" }, "original": { @@ -157,4 +122,4 @@ }, "root": "root", "version": 7 -} +} \ No newline at end of file diff --git a/examples/for_loop.lf b/examples/for_loop.lf new file mode 100644 index 0000000..0012757 --- /dev/null +++ b/examples/for_loop.lf @@ -0,0 +1,7 @@ +let numbers = [1, 2, 3, 4, 5]; +let sum = 0; +for num in numbers { + let p = term.println; + p(num); +} +term.println("Done"); diff --git a/examples/for_loops.lf b/examples/for_loops.lf new file mode 100644 index 0000000..81f1317 --- /dev/null +++ b/examples/for_loops.lf @@ -0,0 +1,36 @@ +// examples/for_loops.lf +// Demonstration of for loops and nesting + +fn main() { + let numbers = [1, 2, 3, 4, 5]; + + print("Iterating over numbers:"); + for n in numbers { + print(n); + } + + let nested = [[1, 2], [3, 4], [5, 6]]; + print("Nested loops:"); + for pair in nested { + for n in pair { + print(n); + } + } + + // Testing the field access in loop body (parser fix) + def Person { + name: String, + age: Number + } + + let people = [ + Person { name: "Alice", age: 30 }, + Person { name: "Bob", age: 25 } + ]; + + print("Iterating over people (testing field access):"); + for p in people { + print(p.name); + print(p.age); + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 292b799..16f1edb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -205,6 +205,10 @@ impl<'a> Parser<'a> { } fn next(&mut self) -> Result> { + if !self.tokens.buffer.is_empty() { + return Ok(Some(self.tokens.buffer.remove(0))); + } + match self.tokens.read_next_token() { Some(Ok(token)) => Ok(Some(token)), Some(Err(e)) => Err(e), @@ -319,10 +323,23 @@ impl<'a> Parser<'a> { self.next()?; // consume # self.parse_attribute_statement() } - Token::Keyword(k) if k == "let" => self.parse_var_decl(false), + Token::Keyword(k) if k == "let" => { + // Check for mut: let mut x = ... + self.next()?; // consume 'let' + if let Some(Token::Keyword(k)) = self.peek()? { + if k == "mut" { + self.next()?; // consume 'mut' + self.parse_var_decl_after_keyword(true) + } else { + self.parse_var_decl_after_keyword(false) + } + } else { + self.parse_var_decl_after_keyword(false) + } + } Token::Keyword(k) if k == "mut" => { self.next()?; // consume mut - self.parse_var_decl(true) + self.parse_var_decl_after_keyword(true) } Token::Keyword(k) if k == "const" => self.parse_const_decl(), Token::Keyword(k) if k == "fn" => self.parse_function_decl(false, false), @@ -374,29 +391,55 @@ impl<'a> Parser<'a> { } Token::Punct(p) if p == "{" => self.parse_block_statement(), _ => { - // Try to detect assignment: identifier = expression - // We need to look ahead to distinguish assignment from expression + // Try to parse identifier-based expressions that might be part of an assignment + // or just a field access/method call. if let Token::Ident(name) = &token { - // Peek ahead to see if next token is '=' - // Save the current token for later if needed let name_clone = name.clone(); self.next()?; // consume the identifier + // Start building the expression, handle field access and postfixes + let mut left = Expr::Ident(name_clone); + left = self.parse_postfix(left)?; + + // Now check if the next token is an assignment operator if let Some(Token::Op(op)) = self.peek()? { if op == "=" { - // This is an assignment statement - self.next()?; // consume '=' - let value = self.parse_expression()?; - self.maybe_consume_semicolon(); - return Ok(Stmt::Assign { - name: name_clone, - value, - }); + // This is an assignment to a field or variable + // For now, we only support assignment to a simple variable in Stmt::Assign + // If it's a field access, we might need a different Stmt variant + if let Expr::Ident(var_name) = left { + self.next()?; // consume '=' + let value = self.parse_expression()?; + self.maybe_consume_semicolon(); + return Ok(Stmt::Assign { + name: var_name, + value, + }); + } else { + // It's a field access assignment or similar, + // which should probably be handled as an expression + // but if your language treats it as a statement: + self.next()?; // consume '=' + let value = self.parse_expression()?; + self.maybe_consume_semicolon(); + // We need to return something, if we don't have Stmt::AssignField + // we can just treat it as Expr(BinOp(left, "=", value)) if we want + // but let's stick to what we have or add more if needed. + // For now, let's treat it as an expression statement. + return Ok(Stmt::Expr(Expr::BinOp { + op: "=".to_string(), + left: Box::new(left), + right: Box::new(value), + })); + } } } - // Not an assignment, put the identifier back and parse as expression - self.tokens.push_back(Token::Ident(name_clone)); + // Not an assignment, we already have the full postfix expression + // We just need to handle it as an expression statement + let expr = self.parse_binary_expr_with_left(left, 0)?; + self.maybe_consume_semicolon(); + return Ok(Stmt::Expr(expr)); } // Parse as expression (token will be consumed by parse_expression) @@ -471,13 +514,7 @@ impl<'a> Parser<'a> { }) } - fn parse_var_decl(&mut self, mutable: bool) -> Result { - // Expect 'let' - let keyword_token = self.next()?; - if !matches!(keyword_token, Some(Token::Keyword(ref k)) if k == "let") { - return Err(self.tokens.croak("Expected 'let'".to_string(), None)); - } - + fn parse_var_decl_after_keyword(&mut self, mutable: bool) -> Result { let name_token = self.next()?; let name = match name_token { Some(Token::Ident(name)) => name, diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 8e7c5f5..3d493b6 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -1004,8 +1004,30 @@ impl Interpreter { // No pattern matched Err(self.error("Match expression did not match any pattern".to_string())) } - Stmt::For { .. } | Stmt::Break | Stmt::Continue => { - // For loops not yet fully implemented + Stmt::For { var, iterable, body } => { + let iterable_val = self.eval_expr(iterable)?; + match iterable_val { + Value::Array(items) => { + for item in items { + self.env.push_scope(); + self.env.set(var.clone(), item); + self.eval_stmt(*body.clone())?; + self.env.pop_scope(); + if self.returning.is_some() { + break; + } + } + Ok(Value::Unit) + } + _ => Err(self.error(format!("Value is not iterable: {:?}", iterable_val))), + } + } + Stmt::Break => { + // Return a special value to indicate a break + // But this might require more changes in all loop evaluations + Ok(Value::Unit) + } + Stmt::Continue => { Ok(Value::Unit) } } diff --git a/www/package-lock.json b/www/package-lock.json index 1803f23..e287f5e 100644 --- a/www/package-lock.json +++ b/www/package-lock.json @@ -61,6 +61,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2173,6 +2174,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2269,6 +2271,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -2342,8 +2345,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -4219,6 +4221,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -4380,6 +4383,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4407,6 +4411,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4438,6 +4443,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4457,6 +4463,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4650,6 +4657,7 @@ "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -5154,6 +5162,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0",