Skip to main content

fiddler_script/
lib.rs

1//! # FiddlerScript
2//!
3//! A minimal C-style scripting language with a Rust-based interpreter.
4//!
5//! ## Features
6//!
7//! - Variables with `let` declarations (integers and strings)
8//! - Control flow with `if-else` statements and `for` loops
9//! - User-defined functions with `fn` syntax
10//! - Built-in functions (starting with `print()`)
11//! - Single-line comments with `//`
12//!
13//! ## Example
14//!
15//! ```
16//! use fiddler_script::Interpreter;
17//!
18//! let source = r#"
19//!     let x = 10;
20//!     let y = 20;
21//!     print(x + y);
22//! "#;
23//!
24//! let mut interpreter = Interpreter::new();
25//! interpreter.run(source).expect("Failed to run script");
26//! ```
27//!
28//! ## Custom Built-in Functions
29//!
30//! You can extend the interpreter with custom built-in functions:
31//!
32//! ```
33//! use fiddler_script::{Interpreter, Value, RuntimeError};
34//! use std::collections::HashMap;
35//!
36//! let mut builtins: HashMap<String, fn(Vec<Value>) -> Result<Value, RuntimeError>> = HashMap::new();
37//! builtins.insert("double".to_string(), |args| {
38//!     if let Some(Value::Integer(n)) = args.first() {
39//!         Ok(Value::Integer(n * 2))
40//!     } else {
41//!         Err(RuntimeError::invalid_argument("Expected integer"))
42//!     }
43//! });
44//!
45//! let mut interpreter = Interpreter::with_builtins(builtins);
46//! ```
47
48pub mod ast;
49pub mod builtins;
50pub mod error;
51pub mod interpreter;
52pub mod lexer;
53pub mod parser;
54
55use indexmap::IndexMap;
56
57// Re-export main types for convenience
58pub use ast::{Expression, Program, Statement};
59pub use builtins::BuiltinFn;
60pub use error::{FiddlerError, LexError, ParseError, RuntimeError};
61pub use interpreter::Interpreter;
62pub use lexer::{Lexer, Token};
63pub use parser::Parser;
64
65/// Parse FiddlerScript source without executing it.
66///
67/// Runs the lexer and parser but does not create an interpreter or execute
68/// any code. Use this to validate script syntax at configuration time.
69///
70/// Returns the parsed [`Program`] AST on success, or a [`FiddlerError`]
71/// describing the first lex or parse error encountered.
72///
73/// # Example
74///
75/// ```
76/// use fiddler_script::parse;
77///
78/// // Valid script parses successfully
79/// assert!(parse("let x = 10;").is_ok());
80///
81/// // Syntax errors are caught without execution
82/// assert!(parse("let x = ;").is_err());
83/// ```
84pub fn parse(source: &str) -> Result<Program, FiddlerError> {
85    let mut lexer = Lexer::new(source);
86    let tokens = lexer.tokenize()?;
87    let mut parser = Parser::new(tokens);
88    let program = parser.parse()?;
89    Ok(program)
90}
91
92/// Check whether FiddlerScript source is syntactically valid.
93///
94/// Returns `Ok(())` if the source parses successfully, or the first
95/// [`FiddlerError`] encountered. This is a convenience wrapper around
96/// [`parse`] that discards the AST.
97pub fn check(source: &str) -> Result<(), FiddlerError> {
98    parse(source).map(|_| ())
99}
100
101/// A runtime value in FiddlerScript.
102#[derive(Debug, Clone)]
103pub enum Value {
104    /// Integer value
105    Integer(i64),
106    /// Float value (64-bit floating point)
107    Float(f64),
108    /// String value
109    String(String),
110    /// Boolean value
111    Boolean(bool),
112    /// Bytes value (raw binary data)
113    Bytes(Vec<u8>),
114    /// Array value (list of values)
115    Array(Vec<Value>),
116    /// Dictionary value (key-value pairs with insertion order preserved)
117    Dictionary(IndexMap<String, Value>),
118    /// Represents no value (e.g., from a function with no return)
119    Null,
120}
121
122// Custom PartialEq to handle cross-type numeric comparisons and NaN
123impl PartialEq for Value {
124    fn eq(&self, other: &Self) -> bool {
125        match (self, other) {
126            (Value::Integer(a), Value::Integer(b)) => a == b,
127            (Value::Float(a), Value::Float(b)) => a == b, // NaN != NaN per IEEE 754
128            (Value::Integer(a), Value::Float(b)) => (*a as f64) == *b,
129            (Value::Float(a), Value::Integer(b)) => *a == (*b as f64),
130            (Value::String(a), Value::String(b)) => a == b,
131            (Value::Boolean(a), Value::Boolean(b)) => a == b,
132            (Value::Bytes(a), Value::Bytes(b)) => a == b,
133            (Value::Array(a), Value::Array(b)) => a == b,
134            (Value::Dictionary(a), Value::Dictionary(b)) => a == b,
135            (Value::Null, Value::Null) => true,
136            _ => false,
137        }
138    }
139}
140
141impl std::fmt::Display for Value {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Value::Integer(n) => write!(f, "{}", n),
145            Value::Float(fl) => {
146                if fl.is_nan() {
147                    write!(f, "NaN")
148                } else if fl.is_infinite() {
149                    if fl.is_sign_positive() {
150                        write!(f, "Infinity")
151                    } else {
152                        write!(f, "-Infinity")
153                    }
154                } else if fl.fract() == 0.0 && fl.abs() < 1e15 {
155                    // Integer-valued floats: show as 1.0, 2.0, etc.
156                    write!(f, "{:.1}", fl)
157                } else {
158                    // General floats: format with precision, strip trailing zeros
159                    let s = format!("{:.10}", fl);
160                    let s = s.trim_end_matches('0').trim_end_matches('.');
161                    write!(f, "{}", s)
162                }
163            }
164            Value::String(s) => write!(f, "{}", s),
165            Value::Boolean(b) => write!(f, "{}", b),
166            Value::Bytes(bytes) => write!(f, "<bytes: {} bytes>", bytes.len()),
167            Value::Array(arr) => {
168                write!(f, "[")?;
169                let mut first = true;
170                for v in arr {
171                    if !first {
172                        write!(f, ", ")?;
173                    }
174                    write!(f, "{}", v)?;
175                    first = false;
176                }
177                write!(f, "]")
178            }
179            Value::Dictionary(dict) => {
180                write!(f, "{{")?;
181                let mut first = true;
182                for (k, v) in dict {
183                    if !first {
184                        write!(f, ", ")?;
185                    }
186                    write!(f, "{}: {}", k, v)?;
187                    first = false;
188                }
189                write!(f, "}}")
190            }
191            Value::Null => write!(f, "null"),
192        }
193    }
194}
195
196impl Value {
197    /// Convert the value to bytes representation.
198    pub fn to_bytes(&self) -> Vec<u8> {
199        match self {
200            Value::Integer(n) => n.to_string().into_bytes(),
201            Value::Float(f) => f.to_string().into_bytes(),
202            Value::String(s) => s.as_bytes().to_vec(),
203            Value::Boolean(b) => b.to_string().into_bytes(),
204            Value::Bytes(bytes) => bytes.clone(),
205            Value::Array(_) | Value::Dictionary(_) => self.to_string().into_bytes(),
206            Value::Null => Vec::new(),
207        }
208    }
209
210    /// Check if value is a number (integer or float).
211    pub fn is_number(&self) -> bool {
212        matches!(self, Value::Integer(_) | Value::Float(_))
213    }
214
215    /// Try to get as f64, converting integers to floats.
216    pub fn as_f64(&self) -> Option<f64> {
217        match self {
218            Value::Integer(n) => Some(*n as f64),
219            Value::Float(f) => Some(*f),
220            _ => None,
221        }
222    }
223
224    /// Try to get as i64, truncating floats.
225    pub fn as_i64(&self) -> Option<i64> {
226        match self {
227            Value::Integer(n) => Some(*n),
228            Value::Float(f) => Some(*f as i64),
229            _ => None,
230        }
231    }
232
233    /// Try to create a Value from bytes, interpreting as UTF-8 string.
234    pub fn from_bytes(bytes: Vec<u8>) -> Self {
235        Value::Bytes(bytes)
236    }
237
238    /// Convert to string, handling both String and Bytes variants.
239    ///
240    /// Returns an error if the value is not a String or Bytes type.
241    /// For Bytes, invalid UTF-8 sequences are replaced with the Unicode
242    /// replacement character.
243    pub fn as_string_lossy(&self) -> Result<String, RuntimeError> {
244        match self {
245            Value::String(s) => Ok(s.clone()),
246            Value::Bytes(b) => Ok(String::from_utf8_lossy(b).into_owned()),
247            _ => Err(RuntimeError::invalid_argument(
248                "Expected string or bytes value",
249            )),
250        }
251    }
252
253    /// Check if the value is an array.
254    pub fn is_array(&self) -> bool {
255        matches!(self, Value::Array(_))
256    }
257
258    /// Check if the value is a dictionary.
259    pub fn is_dictionary(&self) -> bool {
260        matches!(self, Value::Dictionary(_))
261    }
262
263    /// Get as array if this value is an array.
264    pub fn as_array(&self) -> Option<&Vec<Value>> {
265        match self {
266            Value::Array(arr) => Some(arr),
267            _ => None,
268        }
269    }
270
271    /// Get as dictionary if this value is a dictionary.
272    pub fn as_dictionary(&self) -> Option<&IndexMap<String, Value>> {
273        match self {
274            Value::Dictionary(dict) => Some(dict),
275            _ => None,
276        }
277    }
278}
279
280#[cfg(test)]
281mod lint_tests {
282    use super::*;
283
284    #[test]
285    fn parse_valid_source() {
286        let program = parse("let x = 10; let y = x + 1;").unwrap();
287        assert!(!program.statements.is_empty());
288    }
289
290    #[test]
291    fn parse_returns_lex_error_for_bad_char() {
292        let err = parse("let x = @;").unwrap_err();
293        assert!(matches!(err, FiddlerError::Lex(_)));
294    }
295
296    #[test]
297    fn parse_returns_lex_error_for_unterminated_string() {
298        let err = parse(r#"let x = "hello;"#).unwrap_err();
299        assert!(matches!(err, FiddlerError::Lex(_)));
300    }
301
302    #[test]
303    fn parse_returns_parse_error_for_missing_semicolon() {
304        let err = parse("let x = 10\nlet y = 20;").unwrap_err();
305        assert!(matches!(err, FiddlerError::Parse(_)));
306    }
307
308    #[test]
309    fn parse_does_not_execute() {
310        // Script references undefined variable — this is a runtime error only.
311        // parse should succeed because the syntax is valid.
312        let result = parse("let x = undefined_var + 1;");
313        assert!(result.is_ok());
314    }
315
316    #[test]
317    fn check_valid_source() {
318        assert!(check("let x = 10;").is_ok());
319    }
320
321    #[test]
322    fn check_invalid_source() {
323        assert!(check("let x = ;").is_err());
324    }
325}