Skip to main content

atmos/os_lib/aura/builtins/
template.rs

1// 分割: aura/builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ6)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5pub(crate) fn parse_format_spec(spec: &str) -> FormatSpec {
6    let mut f = FormatSpec {
7        alternate: false,
8        zero_pad: false,
9        width: 0,
10        comma: false,
11        precision: None,
12        fmt_type: None,
13    };
14
15    let mut chars = spec.chars().peekable();
16
17    if chars.peek() == Some(&'#') {
18        f.alternate = true;
19        chars.next();
20    }
21
22    if chars.peek() == Some(&'0') {
23        f.zero_pad = true;
24        chars.next();
25    }
26
27    let mut width_str = String::new();
28    while let Some(&c) = chars.peek() {
29        if c.is_ascii_digit() {
30            width_str.push(c);
31            chars.next();
32        } else {
33            break;
34        }
35    }
36    if !width_str.is_empty() {
37        f.width = width_str.parse().unwrap_or(0);
38    }
39
40    if chars.peek() == Some(&',') {
41        f.comma = true;
42        chars.next();
43    }
44
45    if chars.peek() == Some(&'.') {
46        chars.next();
47        let mut prec_str = String::new();
48        while let Some(&c) = chars.peek() {
49            if c.is_ascii_digit() {
50                prec_str.push(c);
51                chars.next();
52            } else {
53                break;
54            }
55        }
56        if !prec_str.is_empty() {
57            f.precision = Some(prec_str.parse().unwrap_or(0));
58        }
59    }
60
61    if let Some(&c) = chars.peek() {
62        f.fmt_type = Some(c);
63        chars.next();
64    }
65
66    f
67}
68
69pub(crate) fn bigint_to_bin(mut val: BigInt) -> String {
70    if val.is_zero() {
71        return String::from("0");
72    }
73    let mut bin = String::new();
74    let two = BigInt::from_i64(2);
75    let sign = val.sign;
76    val.sign = true;
77    while !val.is_zero() {
78        if let Some((q, r)) = val.div_rem(&two) {
79            let rem = r.to_i64().unwrap_or(0);
80            bin.push(if rem == 0 { '0' } else { '1' });
81            val = q;
82        } else {
83            break;
84        }
85    }
86    let mut s = String::new();
87    if !sign {
88        s.push('-');
89    }
90    s.extend(bin.chars().rev());
91    s
92}
93
94pub(crate) fn bigint_to_hex(mut val: BigInt, uppercase: bool) -> String {
95    if val.is_zero() {
96        return String::from("0");
97    }
98    let mut hex = String::new();
99    let sixteen = BigInt::from_i64(16);
100    let sign = val.sign;
101    val.sign = true;
102    let hex_chars = if uppercase {
103        "0123456789ABCDEF"
104    } else {
105        "0123456789abcdef"
106    };
107    while !val.is_zero() {
108        if let Some((q, r)) = val.div_rem(&sixteen) {
109            let rem = r.to_i64().unwrap_or(0) as usize;
110            hex.push(hex_chars.chars().nth(rem).unwrap_or('0'));
111            val = q;
112        } else {
113            break;
114        }
115    }
116    let mut s = String::new();
117    if !sign {
118        s.push('-');
119    }
120    s.extend(hex.chars().rev());
121    s
122}
123
124pub(crate) fn format_comma(s: &str) -> String {
125    let mut res = String::new();
126    let mut count = 0;
127    for c in s.chars().rev() {
128        if c.is_ascii_digit() {
129            if count > 0 && count % 3 == 0 {
130                res.push(',');
131            }
132            count += 1;
133        }
134        res.push(c);
135    }
136    res.chars().rev().collect()
137}
138
139pub(crate) fn apply_padding(val_str: String, spec: &FormatSpec, prefix: &str, is_negative: bool) -> String {
140    let mut clean_str = val_str;
141    let mut needed_len = spec.width;
142    if is_negative {
143        needed_len = needed_len.saturating_sub(1);
144    }
145    needed_len = needed_len.saturating_sub(prefix.len());
146
147    if clean_str.len() < needed_len && spec.zero_pad {
148        let pad_len = needed_len - clean_str.len();
149        let mut padded = String::new();
150        for _ in 0..pad_len {
151            padded.push('0');
152        }
153        padded.push_str(&clean_str);
154        clean_str = padded;
155    }
156
157    let mut final_str = String::new();
158    if is_negative {
159        final_str.push('-');
160    }
161    final_str.push_str(prefix);
162    final_str.push_str(&clean_str);
163    final_str
164}
165
166pub(crate) fn format_value(val: &Value, spec_str: &str) -> Result<String, String> {
167    if spec_str.is_empty() {
168        return Ok(match val {
169            Value::Str(s) => s.clone(),
170            _ => val.to_string(),
171        });
172    }
173
174    let spec = parse_format_spec(spec_str);
175
176    match val {
177        Value::Num(n) => {
178            let mut prefix = "";
179            let mut val_str;
180            let is_negative = !n.sign && !n.is_zero();
181            let mut abs_n = n.clone();
182            abs_n.sign = true;
183
184            match spec.fmt_type {
185                Some('b') => {
186                    val_str = bigint_to_bin(abs_n);
187                    if spec.alternate {
188                        prefix = "0b";
189                    }
190                }
191                Some('x') => {
192                    val_str = bigint_to_hex(abs_n, false);
193                    if spec.alternate {
194                        prefix = "0x";
195                    }
196                }
197                Some('X') => {
198                    val_str = bigint_to_hex(abs_n, true);
199                    if spec.alternate {
200                        prefix = "0X";
201                    }
202                }
203                _ => {
204                    val_str = abs_n.to_string();
205                    if spec.comma {
206                        val_str = format_comma(&val_str);
207                    }
208                }
209            }
210
211            Ok(apply_padding(val_str, &spec, prefix, is_negative))
212        }
213        Value::Ratio(r) => {
214            let val_f64 = r.to_f64();
215            let prec = spec.precision.unwrap_or(6);
216            let mut val_str = match spec.fmt_type {
217                Some('f') => alloc::format!("{:.1$}", val_f64, prec),
218                Some('e') => alloc::format!("{:.1$e}", val_f64, prec),
219                Some('E') => alloc::format!("{:.1$E}", val_f64, prec),
220                _ => {
221                    let mut s = r.to_string();
222                    if spec.comma {
223                        s = format_comma(&s);
224                    }
225                    s
226                }
227            };
228            if val_str.len() < spec.width && spec.zero_pad {
229                let is_negative = val_f64 < 0.0;
230                let clean_str = if is_negative {
231                    val_str.trim_start_matches('-')
232                } else {
233                    &val_str
234                };
235                let pad_len = spec
236                    .width
237                    .saturating_sub(clean_str.len() + if is_negative { 1 } else { 0 });
238                let mut padded = String::new();
239                if is_negative {
240                    padded.push('-');
241                }
242                for _ in 0..pad_len {
243                    padded.push('0');
244                }
245                padded.push_str(clean_str);
246                val_str = padded;
247            }
248            Ok(val_str)
249        }
250        Value::Complex(c) => Ok(c.to_string()),
251        Value::Str(s) => Ok(s.clone()),
252        _ => Ok(val.to_string()),
253    }
254}
255
256// ガード実行は「Some(lambda) かつ引数数が一致」を段階的に確認する構造が読みやすいため、
257// if let / match のネスト折り畳み提案はこの関数では抑制する。
258#[allow(clippy::collapsible_if, clippy::collapsible_match)]
259pub(crate) fn builtin_os_template_eval(env: &mut Env, args: &[AST]) -> Result<Value, String> {
260    let vals = eval_args(env, args)?;
261    if vals.len() != 3 {
262        return Err(String::from("os.template-eval requires exactly 3 arguments: (os.template-eval template values formats)"));
263    }
264    let template = match &vals[0] {
265        Value::Str(s) => s,
266        _ => {
267            return Err(String::from(
268                "os.template-eval first argument must be a string",
269            ))
270        }
271    };
272    let values = match &vals[1] {
273        Value::List(l) => l,
274        _ => {
275            return Err(String::from(
276                "os.template-eval second argument must be a list",
277            ))
278        }
279    };
280    let formats = match &vals[2] {
281        Value::List(l) => l,
282        _ => {
283            return Err(String::from(
284                "os.template-eval third argument must be a list",
285            ))
286        }
287    };
288
289    if values.len() != formats.len() {
290        return Err(String::from(
291            "os.template-eval error: values list and formats list must have the same length",
292        ));
293    }
294
295    // 1. 事前ガードの実行
296    {
297        let guard_opt = TEMPLATE_GUARD.lock();
298        if let Some(guard) = guard_opt.as_ref() {
299            if let Value::Lambda(arg_names, body_ast, closure_env) = guard {
300                if arg_names.len() == 2 {
301                    let mut local_env = Env::with_parent(*closure_env.clone());
302                    local_env.set(arg_names[0].clone(), Value::Str(template.clone()));
303                    local_env.set(arg_names[1].clone(), Value::List(values.clone()));
304
305                    let res = eval(&mut local_env, body_ast)?;
306                    let res = force_eval(&mut local_env, res)?;
307
308                    if !super::super::eval::is_truthy(&res)? {
309                        return Err(String::from(
310                            "Security Error: Template evaluation blocked by pre-guard",
311                        ));
312                    }
313                }
314            }
315        }
316    }
317
318    // 2. 文字列構築
319    let mut result = String::new();
320    let mut val_iter = values.iter();
321    let mut fmt_iter = formats.iter();
322    let mut chars = template.chars().peekable();
323
324    while let Some(c) = chars.next() {
325        if c == '$' && chars.peek() == Some(&'{') {
326            chars.next(); // consume '{'
327            if chars.next() == Some('}') {
328                if let (Some(val), Some(fmt_val)) = (val_iter.next(), fmt_iter.next()) {
329                    let spec_str = match fmt_val {
330                        Value::Str(s) => s,
331                        _ => {
332                            return Err(String::from(
333                                "os.template-eval error: format specifier must be a string",
334                            ))
335                        }
336                    };
337                    result.push_str(&format_value(val, spec_str)?);
338                } else {
339                    return Err(String::from(
340                        "os.template-eval error: Not enough values for placeholders",
341                    ));
342                }
343            } else {
344                return Err(String::from(
345                    "os.template-eval error: Invalid template placeholder structure",
346                ));
347            }
348        } else {
349            result.push(c);
350        }
351    }
352
353    // 3. 事後ガードの実行
354    {
355        let guard_opt = TEMPLATE_POST_GUARD.lock();
356        if let Some(guard) = guard_opt.as_ref() {
357            if let Value::Lambda(arg_names, body_ast, closure_env) = guard {
358                if arg_names.len() == 1 {
359                    let mut local_env = Env::with_parent(*closure_env.clone());
360                    local_env.set(arg_names[0].clone(), Value::Str(result.clone()));
361
362                    let res = eval(&mut local_env, body_ast)?;
363                    let res = force_eval(&mut local_env, res)?;
364
365                    if !super::super::eval::is_truthy(&res)? {
366                        return Err(String::from(
367                            "Security Error: Template evaluation blocked by post-guard",
368                        ));
369                    }
370
371                    if let Value::Str(sanitized) = res {
372                        result = sanitized;
373                    }
374                }
375            }
376        }
377    }
378
379    Ok(Value::Str(result))
380}
381
382pub(crate) fn builtin_os_set_template_guard(env: &mut Env, args: &[AST]) -> Result<Value, String> {
383    let vals = eval_args(env, args)?;
384    if vals.len() != 1 {
385        return Err(String::from(
386            "os.set_template_guard requires exactly 1 argument",
387        ));
388    }
389    let guard = vals[0].clone();
390    if !matches!(guard, Value::Lambda(_, _, _)) && !matches!(guard, Value::Nil) {
391        return Err(String::from(
392            "os.set_template_guard argument must be a lambda or nil",
393        ));
394    }
395    *TEMPLATE_GUARD.lock() = if matches!(guard, Value::Nil) {
396        None
397    } else {
398        Some(guard)
399    };
400    Ok(Value::Nil)
401}
402
403pub(crate) fn builtin_os_set_template_post_guard(env: &mut Env, args: &[AST]) -> Result<Value, String> {
404    let vals = eval_args(env, args)?;
405    if vals.len() != 1 {
406        return Err(String::from(
407            "os.set_template_post_guard requires exactly 1 argument",
408        ));
409    }
410    let guard = vals[0].clone();
411    if !matches!(guard, Value::Lambda(_, _, _)) && !matches!(guard, Value::Nil) {
412        return Err(String::from(
413            "os.set_template_post_guard argument must be a lambda or nil",
414        ));
415    }
416    *TEMPLATE_POST_GUARD.lock() = if matches!(guard, Value::Nil) {
417        None
418    } else {
419        Some(guard)
420    };
421    Ok(Value::Nil)
422}