Skip to main content

bop/
builtins.rs

1//! Language-level builtins (`range`, `str`, `int`, `type`, `len`, ...) and
2//! the shared argument-validation helpers used across the runtime.
3//!
4//! These are pure-data operations on `Value`. Host-backed builtins like
5//! file I/O live in `bop-sys` instead.
6
7#[cfg(feature = "no_std")]
8use alloc::{format, string::{String, ToString}, vec::Vec};
9
10use crate::error::BopError;
11use crate::memory::bop_would_exceed;
12use crate::parser::{VariantDecl, VariantKind};
13use crate::value::Value;
14
15// ─── Engine-wide builtin types ────────────────────────────────────
16//
17// `Result` and `RuntimeError` are pre-declared in every engine
18// (walker, VM, AOT) so:
19//
20//   - `try` / `try_call` can construct `Result::Ok(..)` /
21//     `Result::Err(RuntimeError { .. })` without requiring the
22//     program to have imported `std.result` first;
23//   - user programs can write `Result::Ok(..)` or match on
24//     `RuntimeError { message, line }` out of the box;
25//   - engine-to-engine behaviour stays in lockstep — each engine
26//     seeds its type table from these same helpers, so the
27//     shapes can't drift.
28//
29// The combinator fns (`is_ok`, `unwrap`, `map`, …) stay in
30// `std.result`; only the bare type shapes live here.
31
32/// The canonical `Result { Ok(value), Err(error) }` enum shape,
33/// seeded into every engine's type registry at construction time.
34pub fn builtin_result_variants() -> Vec<VariantDecl> {
35    alloc_import::vec![
36        VariantDecl {
37            name: String::from("Ok"),
38            kind: VariantKind::Tuple(alloc_import::vec![String::from("value")]),
39        },
40        VariantDecl {
41            name: String::from("Err"),
42            kind: VariantKind::Tuple(alloc_import::vec![String::from("error")]),
43        },
44    ]
45}
46
47/// The canonical `RuntimeError { message, line }` struct field
48/// list. `try_call` produces these directly; declaring them as a
49/// builtin lets user code pattern-match the same shape.
50pub fn builtin_runtime_error_fields() -> Vec<String> {
51    alloc_import::vec![String::from("message"), String::from("line")]
52}
53
54/// The canonical `Iter { Next(value), Done }` enum shape —
55/// lazy iterators' return type from `.next()`. Seeded into every
56/// engine's type registry alongside `Result` so user code can
57/// pattern-match `Iter::Next(v) | Iter::Done` without importing
58/// anything.
59pub fn builtin_iter_variants() -> Vec<VariantDecl> {
60    alloc_import::vec![
61        VariantDecl {
62            name: String::from("Next"),
63            kind: VariantKind::Tuple(alloc_import::vec![String::from("value")]),
64        },
65        VariantDecl {
66            name: String::from("Done"),
67            kind: VariantKind::Unit,
68        },
69    ]
70}
71
72/// Build `Iter::Next(value)` with the builtin module path so the
73/// caller's pattern against `Iter::Next(v)` fires regardless of
74/// which module the iterator's `.next()` was declared in.
75pub fn make_iter_next(value: Value) -> Value {
76    let mut items: Vec<Value> = Vec::with_capacity(1);
77    items.push(value);
78    Value::new_enum_tuple(
79        String::from(crate::value::BUILTIN_MODULE_PATH),
80        String::from("Iter"),
81        String::from("Next"),
82        items,
83    )
84}
85
86/// Build the `Iter::Done` sentinel. Carries the builtin module
87/// path for the same matching reason as [`make_iter_next`].
88pub fn make_iter_done() -> Value {
89    Value::new_enum_unit(
90        String::from(crate::value::BUILTIN_MODULE_PATH),
91        String::from("Iter"),
92        String::from("Done"),
93    )
94}
95
96// Small alias so this file compiles both under std and no_std. The
97// parser module already uses `alloc::vec!` under no_std, so the
98// engines follow the same convention here. Nothing clever — just a
99// re-export that picks the right `vec!` macro per config.
100#[cfg(not(feature = "no_std"))]
101use std as alloc_import;
102#[cfg(feature = "no_std")]
103use alloc as alloc_import;
104
105pub fn builtin_range(
106    args: &[Value],
107    line: u32,
108    rand_state: &mut u64,
109) -> Result<Value, BopError> {
110    let _ = rand_state; // unused here, keeping signature uniform
111    // `range` operates in integer space — matches Python and
112    // keeps `range(5)[2]` predictable. Float args error out.
113    let (start, end, step) = match args.len() {
114        1 => {
115            let n = expect_int("range", &args[0], line)?;
116            (0i64, n, 1i64)
117        }
118        2 => {
119            let start = expect_int("range", &args[0], line)?;
120            let end = expect_int("range", &args[1], line)?;
121            (start, end, if start <= end { 1 } else { -1 })
122        }
123        3 => {
124            let start = expect_int("range", &args[0], line)?;
125            let end = expect_int("range", &args[1], line)?;
126            let step = expect_int("range", &args[2], line)?;
127            if step == 0 {
128                return Err(error(line, "range step can't be 0"));
129            }
130            (start, end, step)
131        }
132        _ => return Err(error(line, "range takes 1, 2, or 3 arguments")),
133    };
134
135    let mut result = Vec::new();
136    let mut i = start;
137    let max_items = 10_000usize;
138    if step > 0 {
139        while i < end && result.len() < max_items {
140            result.push(Value::Int(i));
141            i = match i.checked_add(step) {
142                Some(v) => v,
143                None => break,
144            };
145        }
146    } else {
147        while i > end && result.len() < max_items {
148            result.push(Value::Int(i));
149            i = match i.checked_add(step) {
150                Some(v) => v,
151                None => break,
152            };
153        }
154    }
155    Ok(Value::new_array(result))
156}
157
158/// Convert a finite `f64` that's already integer-valued into a
159/// `Value::Int` when it fits in `i64`; fall back to
160/// `Value::Number` otherwise. Non-finite inputs stay as
161/// `Number` (the caller's `f64::floor` / `ceil` / `round`
162/// already handled `NaN` / `±inf` correctly).
163pub fn finite_to_int_or_number(n: f64) -> Value {
164    if n.is_finite() && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
165        Value::Int(n as i64)
166    } else {
167        Value::Number(n)
168    }
169}
170
171/// `panic(message)` — raise a non-fatal runtime error carrying
172/// `message`. Useful for stdlib helpers (`unwrap`, `expect`,
173/// `assert_*`) that need to bail with a readable message from an
174/// expression position where a plain `return` isn't enough.
175///
176/// Non-fatal, so `try_call` catches it — same contract as any
177/// other runtime error the program raises.
178pub fn builtin_panic(args: &[Value], line: u32) -> Result<Value, BopError> {
179    expect_args("panic", args, 1, line)?;
180    let message = match &args[0] {
181        Value::Str(s) => s.as_str().to_string(),
182        // Non-string arguments are stringified via Display so a
183        // caller that hands us a struct or int still gets a
184        // useful trace — cheaper than rejecting and forcing the
185        // caller to add `.to_str()`.
186        other => format!("{}", other),
187    };
188    Err(error(line, message))
189}
190
191pub fn builtin_rand(args: &[Value], line: u32, rand_state: &mut u64) -> Result<Value, BopError> {
192    expect_args("rand", args, 1, line)?;
193    let n = expect_int("rand", &args[0], line)?;
194    if n <= 0 {
195        return Err(error(line, "rand needs a positive number"));
196    }
197    // Simple PCG-style PRNG for deterministic behaviour
198    *rand_state = rand_state
199        .wrapping_mul(6364136223846793005)
200        .wrapping_add(1442695040888963407);
201    let value = (*rand_state >> 33) % (n as u64);
202    Ok(Value::Int(value as i64))
203}
204
205// ─── Helpers (also used by evaluator / VM / AOT) ────────────────────────────
206
207pub fn expect_args(
208    name: &str,
209    args: &[Value],
210    expected: usize,
211    line: u32,
212) -> Result<(), BopError> {
213    if args.len() != expected {
214        Err(error(
215            line,
216            format!(
217                "`{}` expects {} argument{}, but got {}",
218                name,
219                expected,
220                if expected == 1 { "" } else { "s" },
221                args.len()
222            ),
223        ))
224    } else {
225        Ok(())
226    }
227}
228
229pub fn expect_number(
230    func_name: &str,
231    val: &Value,
232    line: u32,
233) -> Result<f64, BopError> {
234    match val {
235        Value::Int(n) => Ok(*n as f64),
236        Value::Number(n) => Ok(*n),
237        _ => Err(error(
238            line,
239            format!(
240                "`{}` expects a number, but got {}",
241                func_name,
242                val.type_name()
243            ),
244        )),
245    }
246}
247
248/// Like [`expect_number`] but strictly requires an `Int`. Used
249/// by builtins that have to produce exact integer counts
250/// (e.g. `range`, `rand`). `Number` inputs are rejected rather
251/// than silently truncated.
252pub fn expect_int(
253    func_name: &str,
254    val: &Value,
255    line: u32,
256) -> Result<i64, BopError> {
257    match val {
258        Value::Int(n) => Ok(*n),
259        _ => Err(error(
260            line,
261            format!(
262                "`{}` expects an int, but got {}",
263                func_name,
264                val.type_name()
265            ),
266        )),
267    }
268}
269
270pub fn error(line: u32, message: impl Into<String>) -> BopError {
271    BopError {
272        line: Some(line),
273        column: None,
274        message: message.into(),
275        friendly_hint: None,
276        is_fatal: false,
277        is_try_return: false,
278    }
279}
280
281/// Like [`error`] but takes a niche-packed column alongside
282/// the line. Call sites with an `Expr` or `Stmt` in hand
283/// prefer this over `error` so the rendered carat points at
284/// the offending character rather than just the line start.
285pub fn error_at(
286    line: u32,
287    column: Option<core::num::NonZeroU32>,
288    message: impl Into<String>,
289) -> BopError {
290    BopError {
291        line: Some(line),
292        column: column.map(|c| c.get()),
293        message: message.into(),
294        friendly_hint: None,
295        is_fatal: false,
296        is_try_return: false,
297    }
298}
299
300pub fn error_with_hint(
301    line: u32,
302    message: impl Into<String>,
303    hint: impl Into<String>,
304) -> BopError {
305    BopError {
306        line: Some(line),
307        column: None,
308        message: message.into(),
309        friendly_hint: Some(hint.into()),
310        is_fatal: false,
311        is_try_return: false,
312    }
313}
314
315/// Column-aware variant of [`error_with_hint`]. Same hint
316/// payload, plus a `column` slot so the renderer can draw the
317/// carat.
318pub fn error_with_hint_at(
319    line: u32,
320    column: Option<core::num::NonZeroU32>,
321    message: impl Into<String>,
322    hint: impl Into<String>,
323) -> BopError {
324    BopError {
325        line: Some(line),
326        column: column.map(|c| c.get()),
327        message: message.into(),
328        friendly_hint: Some(hint.into()),
329        is_fatal: false,
330        is_try_return: false,
331    }
332}
333
334/// Fatal variant of [`error_with_hint`] — `is_fatal = true`
335/// blocks `try_call` from swallowing it. Used by resource-
336/// limit violations (`too many steps`, `Memory limit
337/// exceeded`) so a script can't wrap a step-bomb in
338/// `try_call` and keep running.
339pub fn error_fatal_with_hint(
340    line: u32,
341    message: impl Into<String>,
342    hint: impl Into<String>,
343) -> BopError {
344    BopError {
345        line: Some(line),
346        column: None,
347        message: message.into(),
348        friendly_hint: Some(hint.into()),
349        is_fatal: true,
350        is_try_return: false,
351    }
352}
353
354/// Fatal variant of [`error`] (no hint). Same uncatchable
355/// contract as [`error_fatal_with_hint`].
356pub fn error_fatal(line: u32, message: impl Into<String>) -> BopError {
357    BopError {
358        line: Some(line),
359        column: None,
360        message: message.into(),
361        friendly_hint: None,
362        is_fatal: true,
363        is_try_return: false,
364    }
365}
366
367// ─── `try_call` result construction ────────────────────────────
368//
369// The `try_call(f)` builtin is Lua's `pcall` renamed — it calls
370// `f` (a zero-arg callable), catches any non-fatal `BopError`,
371// and reports the outcome as a `Result::Ok(value)` or
372// `Result::Err(RuntimeError { message, line })` structurally-
373// shaped value. These helpers construct those values directly
374// via `Value::new_enum_tuple` / `Value::new_struct` and
375// therefore don't require the program to have declared
376// `Result` or `RuntimeError` — they produce the same shape
377// either way, so user code can pattern-match them regardless.
378//
379// Fatal errors (`is_fatal == true`) are deliberately *not*
380// wrapped — `try_call`'s callers never see them. See
381// [`BopError::is_fatal`] for why.
382
383/// Build the `Result::Ok(value)` variant `try_call` returns on a
384/// successful call. `Result` is an engine builtin, so the value
385/// carries `<builtin>` as its module path — any program that
386/// matches it via `Result::Ok(v)` resolves `Result` to the same
387/// builtin in its own type-binding scope.
388pub fn make_try_call_ok(value: Value) -> Value {
389    let mut items: Vec<Value> = Vec::with_capacity(1);
390    items.push(value);
391    Value::new_enum_tuple(
392        String::from(crate::value::BUILTIN_MODULE_PATH),
393        String::from("Result"),
394        String::from("Ok"),
395        items,
396    )
397}
398
399/// Build the `Result::Err(RuntimeError { message, line })`
400/// variant `try_call` returns on a caught non-fatal error.
401/// `RuntimeError` is also a builtin — same `<builtin>` module
402/// path as `Result`.
403pub fn make_try_call_err(err: &BopError) -> Value {
404    let message = Value::new_str(err.message.clone());
405    // Line numbers are integers — use Int now that phase 6
406    // distinguishes them from floats.
407    let line = Value::Int(err.line.unwrap_or(0) as i64);
408    let mut fields: Vec<(String, Value)> = Vec::with_capacity(2);
409    fields.push((String::from("message"), message));
410    fields.push((String::from("line"), line));
411    let rt_err = Value::new_struct(
412        String::from(crate::value::BUILTIN_MODULE_PATH),
413        String::from("RuntimeError"),
414        fields,
415    );
416    let mut items: Vec<Value> = Vec::with_capacity(1);
417    items.push(rt_err);
418    Value::new_enum_tuple(
419        String::from(crate::value::BUILTIN_MODULE_PATH),
420        String::from("Result"),
421        String::from("Err"),
422        items,
423    )
424}
425
426/// Pre-flight check for string repeat
427pub fn check_string_repeat_memory(len: usize, count: usize, line: u32) -> Result<(), BopError> {
428    let result_len = len.saturating_mul(count);
429    if bop_would_exceed(result_len) {
430        Err(error_fatal_with_hint(
431            line,
432            "Memory limit exceeded",
433            "This string repeat would use too much memory.",
434        ))
435    } else {
436        Ok(())
437    }
438}
439
440/// Pre-flight check for string concat
441pub fn check_string_concat_memory(a_len: usize, b_len: usize, line: u32) -> Result<(), BopError> {
442    let result_len = a_len + b_len;
443    if bop_would_exceed(result_len) {
444        Err(error_fatal_with_hint(
445            line,
446            "Memory limit exceeded",
447            "This string concatenation would use too much memory.",
448        ))
449    } else {
450        Ok(())
451    }
452}
453
454/// Pre-flight check for array concat
455pub fn check_array_concat_memory(a_len: usize, b_len: usize, line: u32) -> Result<(), BopError> {
456    let result_bytes = (a_len + b_len) * core::mem::size_of::<Value>();
457    if bop_would_exceed(result_bytes) {
458        Err(error_fatal_with_hint(
459            line,
460            "Memory limit exceeded",
461            "This array concatenation would use too much memory.",
462        ))
463    } else {
464        Ok(())
465    }
466}