Skip to main content

bock_core/
string_builder.rs

1//! StringBuilder type for efficient string concatenation.
2//!
3//! Provides a mutable buffer that avoids repeated allocation from string `+`
4//! chaining. Methods: `new`, `append`, `len`, `build`, `clear`.
5
6use std::sync::{Arc, Mutex};
7
8use bock_interp::{BockString, BuiltinRegistry, RuntimeError, TypeTag, Value};
9
10/// Register all StringBuilder methods and the `StringBuilder.new()` global.
11pub fn register(registry: &mut BuiltinRegistry) {
12    // Global constructor
13    registry.register_global("StringBuilder", sb_new);
14
15    // Instance methods
16    registry.register(TypeTag::StringBuilder, "append", sb_append);
17    registry.register(TypeTag::StringBuilder, "len", sb_len);
18    registry.register(TypeTag::StringBuilder, "build", sb_build);
19    registry.register(TypeTag::StringBuilder, "clear", sb_clear);
20}
21
22fn expect_sb(args: &[Value], method: &str) -> Result<Arc<Mutex<String>>, RuntimeError> {
23    match args.first() {
24        Some(Value::StringBuilder(rc)) => Ok(Arc::clone(rc)),
25        Some(other) => Err(RuntimeError::TypeError(format!(
26            "StringBuilder.{method} called on {other}, expected StringBuilder"
27        ))),
28        None => Err(RuntimeError::ArityMismatch {
29            expected: 1,
30            got: 0,
31        }),
32    }
33}
34
35/// `StringBuilder()` → new empty builder.
36/// `StringBuilder("initial")` → builder pre-loaded with content.
37fn sb_new(args: &[Value]) -> Result<Value, RuntimeError> {
38    let initial = match args.first() {
39        Some(Value::String(s)) => s.as_str().to_string(),
40        Some(other) => {
41            return Err(RuntimeError::TypeError(format!(
42                "StringBuilder expects optional String argument, got {other}"
43            )))
44        }
45        None => String::new(),
46    };
47    Ok(Value::StringBuilder(Arc::new(Mutex::new(initial))))
48}
49
50/// `sb.append(value)` → appends the string representation, returns the builder for chaining.
51fn sb_append(args: &[Value]) -> Result<Value, RuntimeError> {
52    let rc = expect_sb(args, "append")?;
53    let val = args.get(1).ok_or(RuntimeError::ArityMismatch {
54        expected: 2,
55        got: 1,
56    })?;
57    let s = match val {
58        Value::String(s) => s.as_str().to_string(),
59        other => other.to_string(),
60    };
61    rc.lock().unwrap().push_str(&s);
62    Ok(Value::StringBuilder(rc))
63}
64
65/// `sb.len()` → character count of the accumulated content.
66fn sb_len(args: &[Value]) -> Result<Value, RuntimeError> {
67    let rc = expect_sb(args, "len")?;
68    let count = rc.lock().unwrap().chars().count() as i64;
69    Ok(Value::Int(count))
70}
71
72/// `sb.build()` → finalize into a `String` value.
73fn sb_build(args: &[Value]) -> Result<Value, RuntimeError> {
74    let rc = expect_sb(args, "build")?;
75    let content = rc.lock().unwrap().clone();
76    Ok(Value::String(BockString::new(content)))
77}
78
79/// `sb.clear()` → empty the buffer, returns the builder.
80fn sb_clear(args: &[Value]) -> Result<Value, RuntimeError> {
81    let rc = expect_sb(args, "clear")?;
82    rc.lock().unwrap().clear();
83    Ok(Value::StringBuilder(rc))
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    fn reg() -> BuiltinRegistry {
91        let mut r = BuiltinRegistry::new();
92        register(&mut r);
93        r
94    }
95
96    fn s(v: &str) -> Value {
97        Value::String(BockString::new(v))
98    }
99
100    #[test]
101    fn new_empty() {
102        let r = reg();
103        let sb = r.call_global("StringBuilder", &[]).unwrap().unwrap();
104        let result = r
105            .call(TypeTag::StringBuilder, "build", &[sb])
106            .unwrap()
107            .unwrap();
108        assert_eq!(result, s(""));
109    }
110
111    #[test]
112    fn new_with_initial() {
113        let r = reg();
114        let sb = r
115            .call_global("StringBuilder", &[s("hello")])
116            .unwrap()
117            .unwrap();
118        let result = r
119            .call(TypeTag::StringBuilder, "build", &[sb])
120            .unwrap()
121            .unwrap();
122        assert_eq!(result, s("hello"));
123    }
124
125    #[test]
126    fn append_and_build() {
127        let r = reg();
128        let sb = r.call_global("StringBuilder", &[]).unwrap().unwrap();
129        let sb = r
130            .call(TypeTag::StringBuilder, "append", &[sb, s("hello")])
131            .unwrap()
132            .unwrap();
133        let sb = r
134            .call(TypeTag::StringBuilder, "append", &[sb, s(" world")])
135            .unwrap()
136            .unwrap();
137        let result = r
138            .call(TypeTag::StringBuilder, "build", &[sb])
139            .unwrap()
140            .unwrap();
141        assert_eq!(result, s("hello world"));
142    }
143
144    #[test]
145    fn append_non_string() {
146        let r = reg();
147        let sb = r.call_global("StringBuilder", &[]).unwrap().unwrap();
148        let sb = r
149            .call(TypeTag::StringBuilder, "append", &[sb, Value::Int(42)])
150            .unwrap()
151            .unwrap();
152        let result = r
153            .call(TypeTag::StringBuilder, "build", &[sb])
154            .unwrap()
155            .unwrap();
156        assert_eq!(result, s("42"));
157    }
158
159    #[test]
160    fn len_counts_chars() {
161        let r = reg();
162        let sb = r
163            .call_global("StringBuilder", &[s("héllo")])
164            .unwrap()
165            .unwrap();
166        let result = r
167            .call(TypeTag::StringBuilder, "len", &[sb])
168            .unwrap()
169            .unwrap();
170        assert_eq!(result, Value::Int(5));
171    }
172
173    #[test]
174    fn clear_empties_buffer() {
175        let r = reg();
176        let sb = r
177            .call_global("StringBuilder", &[s("content")])
178            .unwrap()
179            .unwrap();
180        let sb = r
181            .call(TypeTag::StringBuilder, "clear", &[sb])
182            .unwrap()
183            .unwrap();
184        let result = r
185            .call(TypeTag::StringBuilder, "build", &[sb])
186            .unwrap()
187            .unwrap();
188        assert_eq!(result, s(""));
189    }
190}