///|
let default_max_recursion_depth : Int = 100
///|
/// Feature-flag set controlling which optional Starlark dialect features are
/// enabled. The default dialect is spec-conformant: the standard features
/// `allow_set`, `allow_lambda`, `allow_bytes`, and `allow_float` default to
/// `true`, while the non-standard extensions `allow_recursion`, `allow_while`,
/// `allow_top_level_control`, `allow_global_reassign`, and `load_binds_globally`
/// default to `false` — matching starlark-go's zero-value `FileOptions` for the
/// extensions. The one intentional divergence is `allow_set`: starlark-go's
/// zero value still has `Set = false`, but `allow_set` here controls only the
/// `{...}` set literal and `{x for x in ...}` set comprehension syntax (mbt
/// extensions not in the spec). The `set()` built-in is part of the Starlark
/// spec and is always available regardless of this flag.
pub struct Options {
priv allow_set : Bool
priv allow_recursion : Bool
priv allow_lambda : Bool
priv allow_while : Bool
priv allow_bytes : Bool
priv allow_float : Bool
priv allow_global_reassign : Bool
priv allow_top_level_control : Bool
priv load_binds_globally : Bool
}
///|
/// Returns the default `Options`: the spec-conformant Starlark dialect. The
/// standard features (`allow_set`, `allow_lambda`, `allow_bytes`, `allow_float`)
/// are enabled; the non-standard extensions (`allow_recursion`, `allow_while`,
/// `allow_top_level_control`, `allow_global_reassign`, `load_binds_globally`)
/// are disabled and must be opted into explicitly, matching starlark-go's
/// zero-value `FileOptions` for those extensions. `allow_set` intentionally
/// diverges from that zero value (`Set = false`) because the `{...}` literal
/// and comprehension syntax is a useful mbt extension. The `set()` built-in
/// itself is always available and is not controlled by this flag.
///
/// ```mbt check
/// test {
/// let opts = Options::default()
/// // Standard features are on.
/// inspect(opts.allow_float(), content="true")
/// inspect(opts.allow_lambda(), content="true")
/// // Non-standard extensions are off.
/// inspect(opts.allow_recursion(), content="false")
/// inspect(opts.allow_while(), content="false")
/// }
/// ```
pub fn Options::default() -> Options {
{
allow_set: true,
allow_recursion: false,
allow_lambda: true,
allow_while: false,
allow_bytes: true,
allow_float: true,
allow_global_reassign: false,
allow_top_level_control: false,
load_binds_globally: false,
}
}
///|
/// Returns `true` if `{...}` set literals and set comprehensions are allowed.
pub fn Options::allow_set(self : Options) -> Bool {
self.allow_set
}
///|
/// Returns `true` if recursive function calls are allowed at runtime.
pub fn Options::allow_recursion(self : Options) -> Bool {
self.allow_recursion
}
///|
/// Returns `true` if `lambda` expressions are allowed.
pub fn Options::allow_lambda(self : Options) -> Bool {
self.allow_lambda
}
///|
/// Returns `true` if `while` loops are allowed.
pub fn Options::allow_while(self : Options) -> Bool {
self.allow_while
}
///|
/// Returns `true` if `bytes` literals and the `bytes()` builtin are allowed.
pub fn Options::allow_bytes(self : Options) -> Bool {
self.allow_bytes
}
///|
/// Returns `true` if floating-point literals and `float` arithmetic are
/// allowed.
pub fn Options::allow_float(self : Options) -> Bool {
self.allow_float
}
///|
/// Returns `true` if module-level globals may be reassigned after their
/// initial binding.
pub fn Options::allow_global_reassign(self : Options) -> Bool {
self.allow_global_reassign
}
///|
/// Returns `true` if `if`, `for`, and `while` statements are allowed at
/// the module (top) level.
pub fn Options::allow_top_level_control(self : Options) -> Bool {
self.allow_top_level_control
}
///|
/// Returns `true` if `load`-imported names are bound at module (global) scope
/// rather than file-local scope.
pub fn Options::load_binds_globally(self : Options) -> Bool {
self.load_binds_globally
}
///|
/// Returns a copy of `self` with `load_binds_globally` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `load_binds_globally` flag.
///
/// Returns a new `Options` with the `load_binds_globally` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_load_binds_globally(true)
/// inspect(opts.load_binds_globally(), content="true")
/// // Other flags are unchanged.
/// inspect(opts.allow_set(), content="true")
/// }
/// ```
pub fn Options::with_load_binds_globally(self : Options, v : Bool) -> Options {
{ ..self, load_binds_globally: v }
}
///|
/// Returns a copy of `self` with `allow_recursion` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_recursion` flag.
///
/// Returns a new `Options` with the `allow_recursion` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_recursion(true)
/// inspect(opts.allow_recursion(), content="true")
/// // Other flags are unchanged.
/// inspect(opts.allow_float(), content="true")
/// }
/// ```
pub fn Options::with_allow_recursion(self : Options, v : Bool) -> Options {
{ ..self, allow_recursion: v }
}
///|
/// Returns a copy of `self` with `allow_set` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_set` flag.
///
/// Returns a new `Options` with the `allow_set` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_set(false)
/// inspect(opts.allow_set(), content="false")
/// // Other flags are unchanged.
/// inspect(opts.allow_recursion(), content="false")
/// }
/// ```
pub fn Options::with_allow_set(self : Options, v : Bool) -> Options {
{ ..self, allow_set: v }
}
///|
/// Returns a copy of `self` with `allow_global_reassign` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_global_reassign` flag.
///
/// Returns a new `Options` with the `allow_global_reassign` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_global_reassign(true)
/// inspect(opts.allow_global_reassign(), content="true")
/// // Other flags are unchanged.
/// inspect(opts.allow_float(), content="true")
/// }
/// ```
pub fn Options::with_allow_global_reassign(self : Options, v : Bool) -> Options {
{ ..self, allow_global_reassign: v }
}
///|
/// Returns a copy of `self` with `allow_lambda` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_lambda` flag.
///
/// Returns a new `Options` with the `allow_lambda` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_lambda(false)
/// inspect(opts.allow_lambda(), content="false")
/// // Other flags are unchanged.
/// inspect(opts.allow_recursion(), content="false")
/// }
/// ```
pub fn Options::with_allow_lambda(self : Options, v : Bool) -> Options {
{ ..self, allow_lambda: v }
}
///|
/// Returns a copy of `self` with `allow_while` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_while` flag.
///
/// Returns a new `Options` with the `allow_while` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_while(true)
/// inspect(opts.allow_while(), content="true")
/// // Other flags are unchanged.
/// inspect(opts.allow_float(), content="true")
/// }
/// ```
pub fn Options::with_allow_while(self : Options, v : Bool) -> Options {
{ ..self, allow_while: v }
}
///|
/// Returns a copy of `self` with `allow_bytes` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_bytes` flag.
///
/// Returns a new `Options` with the `allow_bytes` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_bytes(false)
/// inspect(opts.allow_bytes(), content="false")
/// // Other flags are unchanged.
/// inspect(opts.allow_recursion(), content="false")
/// }
/// ```
pub fn Options::with_allow_bytes(self : Options, v : Bool) -> Options {
{ ..self, allow_bytes: v }
}
///|
/// Returns a copy of `self` with `allow_float` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_float` flag.
///
/// Returns a new `Options` with the `allow_float` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_float(false)
/// inspect(opts.allow_float(), content="false")
/// // Other flags are unchanged.
/// inspect(opts.allow_lambda(), content="true")
/// }
/// ```
pub fn Options::with_allow_float(self : Options, v : Bool) -> Options {
{ ..self, allow_float: v }
}
///|
/// Returns a copy of `self` with `allow_top_level_control` set to `v`.
///
/// Parameters:
///
/// - `self` : The options to copy.
/// - `v` : The new value for the `allow_top_level_control` flag.
///
/// Returns a new `Options` with the `allow_top_level_control` flag set to `v`.
///
/// ```mbt check
/// test {
/// let opts = Options::default().with_allow_top_level_control(true)
/// inspect(opts.allow_top_level_control(), content="true")
/// // Other flags are unchanged.
/// inspect(opts.allow_float(), content="true")
/// }
/// ```
pub fn Options::with_allow_top_level_control(
self : Options,
v : Bool,
) -> Options {
{ ..self, allow_top_level_control: v }
}
///|
/// The set of predeclared built-in bindings shared across all modules.
/// `Universe::standard()` provides the default Starlark built-ins; embedders
/// may extend it with `set()`.
pub struct Universe {
priv bindings : Map[String, @value.Value]
}
///|
/// Creates an empty `Universe` with no predeclared names.
pub fn Universe::new() -> Universe {
{ bindings: Map([]) }
}
///|
/// Creates a `Universe` wrapping an existing binding map.
///
/// Parameters:
///
/// - `m` : The map to wrap; keys are name strings, values are the bindings.
///
/// Returns a `Universe` backed by `m`.
pub fn Universe::from_map(m : Map[String, @value.Value]) -> Universe {
{ bindings: m }
}
///|
/// Creates a `Universe` pre-populated with all standard Starlark built-ins
/// (`None`, `True`, `False`, `print`, `range`, etc.).
pub fn Universe::standard() -> Universe {
let m : Map[String, @value.Value] = Map([])
m["None"] = @value.Value::None
m["True"] = @value.Value::Bool(true)
m["False"] = @value.Value::Bool(false)
for name in builtin_names {
m[name] = @value.Value::Builtin(@value.StarlarkBuiltinFunc::dispatch(name))
}
{ bindings: m }
}
///|
/// Returns the value bound to `name` in the universe, or `None`.
///
/// Parameters:
///
/// - `self` : The universe to look up in.
/// - `name` : The predeclared name to retrieve.
///
/// Returns `Some(value)` if `name` is present, `None` otherwise.
pub fn Universe::get(self : Universe, name : String) -> @value.Value? {
self.bindings.get(name)
}
///|
/// Returns `true` if `name` is present in the universe.
///
/// Parameters:
///
/// - `self` : The universe to query.
/// - `name` : The name to test for presence.
///
/// Returns `true` if `name` exists in the universe.
pub fn Universe::has(self : Universe, name : String) -> Bool {
self.bindings.contains(name)
}
///|
/// Returns the predeclared names in lexicographic order.
///
/// Parameters:
///
/// - `self` : The universe whose names to retrieve.
///
/// Returns an array of all names sorted lexicographically.
pub fn Universe::keys(self : Universe) -> Array[String] {
let ks = self.bindings.keys().collect()
ks.sort()
ks
}
///|
/// Adds or replaces the binding for `name` in the universe.
///
/// Parameters:
///
/// - `self` : The universe to modify.
/// - `name` : The predeclared name to bind.
/// - `value` : The value to associate with `name`.
pub fn Universe::set(
self : Universe,
name : String,
value : @value.Value,
) -> Unit {
self.bindings[name] = value
}
///|
/// Calls `f(name, value)` for every binding in the universe.
///
/// Parameters:
///
/// - `self` : The universe to iterate.
/// - `f` : Callback receiving each name and its bound value.
pub fn Universe::each(
self : Universe,
f : (String, @value.Value) -> Unit,
) -> Unit {
self.bindings.each(f)
}
///|
/// Returns all bound values in the universe.
///
/// Parameters:
///
/// - `self` : The universe whose values to retrieve.
///
/// Returns an array of all bound values.
pub fn Universe::values(self : Universe) -> Array[@value.Value] {
self.bindings.values().collect()
}
///|
/// Removes the binding for `name`.
///
/// Parameters:
///
/// - `self` : The universe to modify.
/// - `name` : The name to remove.
///
/// Returns `true` if `name` was present (and removed), `false` if absent.
pub fn Universe::delete(self : Universe, name : String) -> Bool {
if self.bindings.contains(name) {
self.bindings.remove(name)
true
} else {
false
}
}
///|
/// Per-execution extra bindings injected before user globals. Visible to
/// both the resolver and evaluator but not exported in the resulting Module.
pub struct Predeclared {
priv bindings : Map[String, @value.Value]
}
///|
/// Creates an empty `Predeclared` set.
pub fn Predeclared::new() -> Predeclared {
{ bindings: Map([]) }
}
///|
/// Creates a `Predeclared` set wrapping an existing map.
///
/// Parameters:
///
/// - `m` : The map to wrap; keys are name strings, values are the bindings.
///
/// Returns a `Predeclared` backed by `m`.
pub fn Predeclared::from_map(m : Map[String, @value.Value]) -> Predeclared {
{ bindings: m }
}
///|
/// Adds or replaces the binding for `name`.
///
/// Parameters:
///
/// - `self` : The predeclared set to modify.
/// - `name` : The name to bind.
/// - `v` : The value to associate with `name`.
pub fn Predeclared::set(
self : Predeclared,
name : String,
v : @value.Value,
) -> Unit {
self.bindings[name] = v
}
///|
/// Returns the value bound to `name`, or `None`.
///
/// Parameters:
///
/// - `self` : The predeclared set to look up in.
/// - `name` : The name to retrieve.
///
/// Returns `Some(value)` if `name` is present, `None` otherwise.
pub fn Predeclared::get(self : Predeclared, name : String) -> @value.Value? {
self.bindings.get(name)
}
///|
/// Returns `true` if `name` is present in the predeclared set.
///
/// Parameters:
///
/// - `self` : The predeclared set to query.
/// - `name` : The name to test for presence.
///
/// Returns `true` if `name` exists in the set.
pub fn Predeclared::has(self : Predeclared, name : String) -> Bool {
self.bindings.contains(name)
}
///|
/// Returns the predeclared names in lexicographic order.
///
/// Parameters:
///
/// - `self` : The predeclared set whose names to retrieve.
///
/// Returns an array of all names sorted lexicographically.
pub fn Predeclared::keys(self : Predeclared) -> Array[String] {
let ks = self.bindings.keys().collect()
ks.sort()
ks
}
///|
/// Calls `f(name, value)` for every binding in the set.
///
/// Parameters:
///
/// - `self` : The predeclared set to iterate.
/// - `f` : Callback receiving each name and its bound value.
pub fn Predeclared::each(
self : Predeclared,
f : (String, @value.Value) -> Unit,
) -> Unit {
self.bindings.each(f)
}
///|
/// Returns all bound values in the set.
///
/// Parameters:
///
/// - `self` : The predeclared set whose values to retrieve.
///
/// Returns an array of all bound values.
pub fn Predeclared::values(self : Predeclared) -> Array[@value.Value] {
self.bindings.values().collect()
}
///|
/// Removes the binding for `name`.
///
/// Parameters:
///
/// - `self` : The predeclared set to modify.
/// - `name` : The name to remove.
///
/// Returns `true` if `name` was present (and removed), `false` if absent.
pub fn Predeclared::delete(self : Predeclared, name : String) -> Bool {
if self.bindings.contains(name) {
self.bindings.remove(name)
true
} else {
false
}
}
///|
/// Execution context for a single Starlark evaluation. Carries the print
/// and load callbacks, call stack, recursion limit, step budget, and
/// cancellation state. A `Thread` is not safe for concurrent use.
pub struct Thread {
priv name : String
priv mut print_fn : (Thread, Bytes) -> Unit
priv mut load_fn : ((Thread, String) -> Result[Module, @errors.EvalError])?
priv call_stack : Array[@errors.CallFrame]
priv debug_stack : Array[ActiveCallFrame]
priv max_recursion_depth : Int
priv mut max_steps : Int?
priv mut steps : Int
priv mut cancel_reason : String?
priv mut on_max_steps : ((Thread) -> Unit)?
priv locals : Map[String, @value.Value]
}
///|
/// Creates a `Thread` with the given name, printing to stdout, no loader,
/// and the default recursion depth limit.
///
/// Parameters:
///
/// - `name` : A label for the thread used in diagnostics.
///
/// Returns a new `Thread` ready for Starlark execution.
pub fn Thread::new(name : String) -> Thread {
{
name,
print_fn: fn(_, bytes) { default_println_bytes(bytes) },
load_fn: None,
call_stack: [],
debug_stack: [],
max_recursion_depth: default_max_recursion_depth,
max_steps: None,
steps: 0,
cancel_reason: None,
on_max_steps: None,
locals: Map([]),
}
}
///|
/// Creates a `Thread` with a custom print callback.
///
/// Parameters:
///
/// - `name` : A label for the thread used in diagnostics.
/// - `print_fn` : Callback invoked for each `print()` call; receives the
/// active thread and the formatted message bytes (without trailing newline).
///
/// Returns a new `Thread` with the given print callback.
pub fn Thread::with_print(
name : String,
print_fn : (Thread, Bytes) -> Unit,
) -> Thread {
{
name,
print_fn,
load_fn: None,
call_stack: [],
debug_stack: [],
max_recursion_depth: default_max_recursion_depth,
max_steps: None,
steps: 0,
cancel_reason: None,
on_max_steps: None,
locals: Map([]),
}
}
///|
/// Creates a `Thread` with a load callback for `load()` statements.
///
/// Parameters:
///
/// - `name` : A label for the thread used in diagnostics.
/// - `load_fn` : Callback invoked for each `load("path", ...)` statement;
/// receives the active thread and the module path, returns the loaded module.
///
/// Returns a new `Thread` with the given load callback.
pub fn Thread::with_loader(
name : String,
load_fn : (Thread, String) -> Result[Module, @errors.EvalError],
) -> Thread {
{
name,
print_fn: fn(_, bytes) { default_println_bytes(bytes) },
load_fn: Some(load_fn),
call_stack: [],
debug_stack: [],
max_recursion_depth: default_max_recursion_depth,
max_steps: None,
steps: 0,
cancel_reason: None,
on_max_steps: None,
locals: Map([]),
}
}
///|
/// Creates a `Thread` with a maximum step budget; execution raises an error
/// when the budget is exhausted.
///
/// Parameters:
///
/// - `name` : A label for the thread used in diagnostics.
/// - `max_steps` : Maximum number of evaluation steps before the execution
/// is cancelled with an error.
///
/// Returns a new `Thread` with the given step budget.
pub fn Thread::with_step_budget(name : String, max_steps : Int) -> Thread {
{
name,
print_fn: fn(_, bytes) { default_println_bytes(bytes) },
load_fn: None,
call_stack: [],
debug_stack: [],
max_recursion_depth: default_max_recursion_depth,
max_steps: Some(max_steps),
steps: 0,
cancel_reason: None,
on_max_steps: None,
locals: Map([]),
}
}
///|
/// Stores a thread-local value under `key`. Used by extension libraries (e.g.
/// `time.now()`) to communicate per-thread state such as clock overrides.
///
/// Parameters:
///
/// - `self` : The thread to store the value on.
/// - `key` : The key identifying this thread-local slot.
/// - `value` : The value to store.
pub fn Thread::set_local(
self : Thread,
key : String,
value : @value.Value,
) -> Unit {
self.locals[key] = value
}
///|
/// Retrieves the thread-local value previously stored under `key`, or `None`.
///
/// Parameters:
///
/// - `self` : The thread to retrieve from.
/// - `key` : The key identifying the thread-local slot.
///
/// Returns `Some(value)` if a value was stored under `key`, `None` otherwise.
pub fn Thread::get_local(self : Thread, key : String) -> @value.Value? {
self.locals.get(key)
}
///|
/// Marks the thread as cancelled with the given reason. The evaluator checks
/// this flag at each step and raises an error on the next opportunity. Only
/// the first call takes effect.
///
/// Parameters:
///
/// - `self` : The thread to cancel.
/// - `reason` : A message describing why the thread was cancelled; used in
/// the raised error.
pub fn Thread::cancel(self : Thread, reason : String) -> Unit {
if self.cancel_reason is None {
self.cancel_reason = Some(reason)
}
}
///|
/// Clears a previous cancellation, allowing execution to resume.
pub fn Thread::uncancel(self : Thread) -> Unit {
self.cancel_reason = None
}
///|
/// Resets the accumulated step counter to zero without changing the budget.
pub fn Thread::reset_steps(self : Thread) -> Unit {
self.steps = 0
}
///|
/// Sets the execution-step budget without resetting the accumulated step
/// count. Use `reset_steps` to zero the counter explicitly.
///
/// Parameters:
///
/// - `self` : The thread to update.
/// - `max` : The new maximum step count.
pub fn Thread::set_max_steps(self : Thread, max : Int) -> Unit {
self.max_steps = Some(max)
}
///|
/// Replaces the print callback. Combined with `set_loader` and
/// `set_max_steps`, this lets a single thread carry all three settings.
///
/// Parameters:
///
/// - `self` : The thread to update.
/// - `print_fn` : The new print callback; receives the active thread and the
/// formatted message bytes (without trailing newline).
pub fn Thread::set_print(
self : Thread,
print_fn : (Thread, Bytes) -> Unit,
) -> Unit {
self.print_fn = print_fn
}
///|
/// Replaces the load callback used by `load()` statements.
///
/// Parameters:
///
/// - `self` : The thread to update.
/// - `load_fn` : The new load callback; receives the active thread and the
/// module path, returns the loaded module or an error.
pub fn Thread::set_loader(
self : Thread,
load_fn : (Thread, String) -> Result[Module, @errors.EvalError],
) -> Unit {
self.load_fn = Some(load_fn)
}
///|
/// Registers a callback invoked when the step budget is exhausted, before
/// the error is raised.
///
/// Parameters:
///
/// - `self` : The thread to update.
/// - `cb` : The callback to invoke when the step budget is exhausted.
pub fn Thread::set_on_max_steps(self : Thread, cb : (Thread) -> Unit) -> Unit {
self.on_max_steps = Some(cb)
}
///|
/// Returns the current call-stack depth (number of active Starlark frames).
pub fn Thread::call_stack_depth(self : Thread) -> Int {
self.call_stack.length()
}
///|
/// Returns a snapshot of the active Starlark call frame at `depth` steps
/// from the innermost frame (0 = innermost). Returns `None` if `depth` is
/// out of range or the frame is not a Starlark function frame.
///
/// Parameters:
///
/// - `self` : The thread whose debug call stack to inspect.
/// - `depth` : Distance from the innermost Starlark function frame (0 = innermost).
///
/// Returns `Some(frame)` if a Starlark frame exists at that depth, `None`
/// otherwise.
pub fn Thread::debug_frame(self : Thread, depth : Int) -> DebugFrame? {
let idx = self.debug_stack.length() - 1 - depth
if idx < 0 || idx >= self.debug_stack.length() {
return None
}
Some(DebugFrame::from_active(self.debug_stack[idx]))
}
///|
/// Returns a snapshot of the current call stack (outermost frame first).
pub fn Thread::call_stack(self : Thread) -> @errors.CallStack {
@errors.CallStack::new(self.call_stack.copy())
}
///|
/// Returns the call frame at `n` steps from the innermost frame (0 =
/// innermost, 1 = its caller, etc.), or `None` if out of range.
///
/// Parameters:
///
/// - `self` : The thread whose call stack to inspect.
/// - `n` : Distance from the innermost frame (0 = innermost).
///
/// Returns `Some(frame)` if the index is valid, `None` otherwise.
pub fn Thread::call_frame(self : Thread, n : Int) -> @errors.CallFrame? {
let idx = self.call_stack.length() - 1 - n
if idx < 0 || idx >= self.call_stack.length() {
return None
}
Some(self.call_stack[idx])
}
///|
/// Returns the total number of evaluation steps executed on this thread.
pub fn Thread::execution_steps(self : Thread) -> Int {
self.steps
}
///|
/// Returns the name the thread was created with.
pub fn Thread::name(self : Thread) -> String {
self.name
}
///|
/// Returns the maximum call-stack depth before a recursion-limit error is
/// raised.
pub fn Thread::max_recursion_depth(self : Thread) -> Int {
self.max_recursion_depth
}
///|
/// Returns the current step budget, or `None` if no budget was set.
pub fn Thread::max_steps(self : Thread) -> Int? {
self.max_steps
}
///|
/// The result of executing a Starlark file: a frozen mapping of global names
/// to their values, plus any injected predeclared bindings.
pub struct Module {
priv globals : @value.StarlarkDict
priv mut frozen : Bool
priv predeclared : Map[String, @value.Value]
}
///|
/// Creates an empty, unfrozen `Module`.
pub fn Module::new() -> Module {
{ globals: @value.StarlarkDict::new(), frozen: false, predeclared: Map([]) }
}
///|
/// Creates a frozen `Module` pre-populated from `m`.
///
/// Aborts (rather than returning an error) if any value in `m` is nested
/// beyond `freeze_limit`. This is acceptable for static, shallow maps such as
/// built-in library exports. For user-supplied code, use the `exec_file*`
/// entry points, which call `Module::freeze_checked` and propagate depth
/// errors as a `EvalError`.
///
/// Parameters:
///
/// - `m` : A map of global name strings to their initial values.
///
/// Returns a new, frozen `Module` containing the bindings from `m`.
pub fn Module::from_map(m : Map[String, @value.Value]) -> Module {
let mod = Module::from_map_unfrozen(m)
mod.freeze()
mod
}
///|
fn Module::from_map_unfrozen(m : Map[String, @value.Value]) -> Module {
let mod = Module::new()
m.each(fn(name, v) {
let key = @value.Value::String(@value.StarlarkString::new(name))
mod.globals.set(key, v) |> ignore
})
mod
}
///|
/// Freezes the module: marks the globals dict and all contained values as
/// immutable so they can safely be shared across threads.
pub fn Module::freeze(self : Module) -> Unit {
self.frozen = true
self.globals.freeze()
self.globals.each(fn(_, v) { v.freeze() })
}
///|
/// Like `Module::freeze` but returns `Err` instead of aborting when the
/// nesting depth limit is exceeded.
pub fn Module::freeze_checked(self : Module) -> Result[Unit, String] {
self.frozen = true
self.globals.freeze()
let mut err : String? = None
self.globals.each(fn(_, v) {
if err is None {
match v.freeze_checked() {
Err(e) => err = Some(e)
Ok(_) => ()
}
}
})
match err {
Some(e) => Err(e)
None => Ok(())
}
}
///|
/// Returns `true` if this module has been frozen.
pub fn Module::is_frozen(self : Module) -> Bool {
self.frozen
}
///|
/// Returns the value bound to `name` in the module globals, or `None`.
///
/// Parameters:
///
/// - `self` : The module to look up in.
/// - `name` : The global name to retrieve.
///
/// Returns `Some(value)` if `name` is present, `None` otherwise.
pub fn Module::get(self : Module, name : String) -> @value.Value? {
let key = @value.Value::String(@value.StarlarkString::new(name))
match self.globals.get(key) {
Ok(Some(v)) => Some(v)
_ => None
}
}
///|
/// Returns the number of exported globals in the module.
pub fn Module::globals_count(self : Module) -> Int {
self.globals.length()
}
///|
/// Returns the number of predeclared bindings injected before execution.
pub fn Module::predeclared_count(self : Module) -> Int {
self.predeclared.length()
}
///|
/// Returns the names of all exported globals in insertion order.
///
/// Returns an `Array[String]` of every global name bound in the module.
pub fn Module::global_names(self : Module) -> Array[String] {
let result : Array[String] = []
self.globals.each(fn(k, _) {
match k {
@value.Value::String(s) => result.push(s.raw())
_ => ()
}
})
result
}
///|
/// Returns the names of all predeclared bindings that were injected before
/// execution.
///
/// Returns an `Array[String]` of every predeclared name.
pub fn Module::predeclared_names(self : Module) -> Array[String] {
let result : Array[String] = []
self.predeclared.each(fn(k, _) { result.push(k) })
result
}
///|
let builtin_names : Array[String] = [
"print", "len", "str", "repr", "type", "range", "list", "tuple", "dict", "bool",
"int", "float", "abs", "min", "max", "set", "enumerate", "zip", "sorted", "reversed",
"hasattr", "getattr", "fail", "any", "all", "hash", "dir", "chr", "ord", "bytes",
]
///|
fn is_builtin(name : String) -> Bool {
builtin_names.contains(name)
}
///|
/// Invokes a Starlark callable from host code with positional `args` and
/// keyword `kwargs`.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `func` : The Starlark callable to invoke (function, builtin, or bound method).
/// - `args` : Positional arguments to pass.
/// - `kwargs` : Keyword arguments as `(name, value)` pairs.
///
/// Returns the call result, or an `EvalError` on failure.
pub fn call(
thread : Thread,
func : @value.Value,
args : Array[@value.Value],
kwargs : Array[(String, @value.Value)],
) -> Result[@value.Value, @errors.EvalError] {
let ctx = EvalContext::new(thread, Options::default())
let pos = @errors.Position::new("", 0, 0)
// Route through a VM so a bytecode-compiled function (which carries its own
// module) runs on a VM frame; builtins and bound methods are dispatched
// through the shared value-level `call_value`.
let vm = VM::{ ctx, active: [] }
ctx.vm = Some(vm)
Ok(vm.call_value(func, args, kwargs, pos)) catch {
EvalErr(e) => Err(e)
}
}
///|
/// Applies a Starlark binary operator by name to `x` and `y`.
/// Valid operators: `"+"`, `"-"`, `"*"`, `"/"`, `"//"`, `"%"`,
/// `"&"`, `"|"`, `"^"`, `"<<"`, `">>"`.
///
/// Parameters:
///
/// - `op` : The binary operator name.
/// - `x` : The left-hand operand.
/// - `y` : The right-hand operand.
///
/// Returns the operation result, or an `EvalError` on type mismatch or
/// arithmetic error.
pub fn binary(
op : String,
x : @value.Value,
y : @value.Value,
) -> Result[@value.Value, @errors.EvalError] {
let bop = match op {
"+" => @syntax.BinaryOp::OpAdd
"-" => @syntax.BinaryOp::OpSub
"*" => @syntax.BinaryOp::OpMul
"/" => @syntax.BinaryOp::OpDiv
"//" => @syntax.BinaryOp::OpFloorDiv
"%" => @syntax.BinaryOp::OpMod
"&" => @syntax.BinaryOp::OpBitAnd
"|" => @syntax.BinaryOp::OpBitOr
"^" => @syntax.BinaryOp::OpBitXor
"<<" => @syntax.BinaryOp::OpLShift
">>" => @syntax.BinaryOp::OpRShift
_ => return Err(@errors.EvalError::simple("unknown binary operator: \{op}"))
}
let thread = Thread::new("")
let ctx = EvalContext::new(thread, Options::default())
let pos = @errors.Position::new("", 0, 0)
Ok(eval_binary(ctx, x, bop, y, pos)) catch {
EvalErr(e) => Err(e)
}
}
///|
/// Applies a Starlark unary operator by name to `x`.
/// Valid operators: `"+"`, `"-"`, `"~"`, `"not"`.
///
/// Parameters:
///
/// - `op` : The unary operator name.
/// - `x` : The operand.
///
/// Returns the operation result, or an `EvalError` on type mismatch.
pub fn unary(
op : String,
x : @value.Value,
) -> Result[@value.Value, @errors.EvalError] {
let uop = match op {
"+" => @syntax.UnaryOp::OpPlus
"-" => @syntax.UnaryOp::OpMinus
"~" => @syntax.UnaryOp::OpBitNot
"not" => @syntax.UnaryOp::OpNot
_ => return Err(@errors.EvalError::simple("unknown unary operator: \{op}"))
}
let thread = Thread::new("")
let ctx = EvalContext::new(thread, Options::default())
let pos = @errors.Position::new("", 0, 0)
Ok(eval_unary(ctx, uop, x, pos)) catch {
EvalErr(e) => Err(e)
}
}
///|
/// Applies a Starlark comparison operator by name to `x` and `y`.
/// Valid operators: `"=="`, `"!="`, `"<"`, `"<="`, `">"`, `">="`.
///
/// Parameters:
///
/// - `op` : The comparison operator name.
/// - `x` : The left-hand operand.
/// - `y` : The right-hand operand.
///
/// Returns `Ok(result)` on success, or an `EvalError` on type mismatch.
pub fn compare(
op : String,
x : @value.Value,
y : @value.Value,
) -> Result[Bool, @errors.EvalError] {
let bop = match op {
"==" => @syntax.BinaryOp::OpEq
"!=" => @syntax.BinaryOp::OpNe
"<" => @syntax.BinaryOp::OpLt
"<=" => @syntax.BinaryOp::OpLe
">" => @syntax.BinaryOp::OpGt
">=" => @syntax.BinaryOp::OpGe
_ =>
return Err(
@errors.EvalError::simple("unknown comparison operator: \{op}"),
)
}
let thread = Thread::new("")
let ctx = EvalContext::new(thread, Options::default())
let pos = @errors.Position::new("", 0, 0)
let result = eval_binary(ctx, x, bop, y, pos) catch {
EvalErr(e) => return Err(e)
}
match result {
@value.Value::Bool(b) => Ok(b)
_ => Err(@errors.EvalError::simple("comparison did not return bool"))
}
}
///|
/// Parses, resolves, and executes a Starlark source file; returns the frozen
/// module whose globals are the file's top-level bindings.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `filename` : Source file name used in error messages and position info.
/// - `src` : Starlark source text to execute.
/// - `opts` : Feature flags controlling which dialect features are active.
///
/// Returns the frozen `Module`, or an `EvalError` on parse, resolve, or
/// runtime failure.
///
/// ```mbt check
/// test {
/// let thread = Thread::new("test")
/// let m = exec_file(thread, "test.star", "x = 1 + 2", Options::default()).unwrap()
/// inspect(m.get("x").unwrap().repr(), content="3")
/// // A syntax error yields EvalError.
/// let err = exec_file(Thread::new("t"), "bad.star", "???", Options::default())
/// inspect(err is Err(_), content="true")
/// }
/// ```
pub fn exec_file(
thread : Thread,
filename : String,
src : String,
opts : Options,
) -> Result[Module, @errors.EvalError] {
exec_file_vm(thread, filename, src, opts)
}
///|
/// Like `exec_file` but replaces the default built-in universe with
/// `universe`, allowing the embedder to override or extend predeclared names.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `filename` : Source file name used in error messages and position info.
/// - `src` : Starlark source text to execute.
/// - `opts` : Feature flags controlling which dialect features are active.
/// - `universe` : Custom universe of predeclared bindings to use instead of
/// the standard built-ins.
///
/// Returns the frozen `Module`, or an `EvalError` on failure.
///
/// ```mbt check
/// test {
/// let thread = Thread::new("test")
/// let uni = Universe::from_map({ "answer": @value.Value::new_int(42L) })
/// let m = exec_file_with_universe(
/// thread,
/// "u.star",
/// "x = answer * 2",
/// Options::default(),
/// uni,
/// ).unwrap()
/// inspect(m.get("x").unwrap().repr(), content="84")
/// // A syntax error yields EvalError.
/// let err = exec_file_with_universe(
/// Thread::new("t"),
/// "bad.star",
/// "???",
/// Options::default(),
/// uni,
/// )
/// inspect(err is Err(_), content="true")
/// }
/// ```
pub fn exec_file_with_universe(
thread : Thread,
filename : String,
src : String,
opts : Options,
universe : Universe,
) -> Result[Module, @errors.EvalError] {
let file = match @parser.parse_file(filename, src) {
Ok(f) => f
Err(e) => return Err(@errors.EvalError::simple(e.to_string()))
}
let ctx = EvalContext::new(thread, opts)
universe.bindings.each(fn(name, v) { ctx.global_env.bind(name, v) })
let universe_names : Array[String] = universe.bindings.keys().collect()
let (prog, m) = match
compile_and_run_vm(ctx, file, filename, fn(name) {
universe_names.contains(name)
}) {
Ok(pair) => pair
Err(e) => return Err(e)
}
let mod = Module::from_map_unfrozen(module_output(prog, m, opts))
match mod.freeze_checked() {
Err(e) => return Err(@errors.EvalError::simple(e))
Ok(_) => ()
}
Ok(mod)
}
///|
/// Like `exec_file` but injects extra `predeclared` bindings that are visible
/// to the script but not included in the returned module's globals.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `filename` : Source file name used in error messages and position info.
/// - `src` : Starlark source text to execute.
/// - `opts` : Feature flags controlling which dialect features are active.
/// - `predeclared` : Extra per-execution bindings injected before user globals.
///
/// Returns the frozen `Module`, or an `EvalError` on failure.
///
/// ```mbt check
/// test {
/// let thread = Thread::new("test")
/// let pre = Predeclared::from_map({ "base": @value.Value::new_int(10L) })
/// let m = exec_file_with_predeclared(
/// thread,
/// "p.star",
/// "result = base + 5",
/// Options::default(),
/// pre,
/// ).unwrap()
/// inspect(m.get("result").unwrap().repr(), content="15")
/// // `base` is predeclared, not exported as a module global.
/// inspect(m.get("base") is None, content="true")
/// }
/// ```
pub fn exec_file_with_predeclared(
thread : Thread,
filename : String,
src : String,
opts : Options,
predeclared : Predeclared,
) -> Result[Module, @errors.EvalError] {
let file = match @parser.parse_file(filename, src) {
Ok(f) => f
Err(e) => return Err(@errors.EvalError::simple(e.to_string()))
}
let ctx = EvalContext::new(thread, opts)
predeclared.bindings.each(fn(name, v) { ctx.global_env.bind(name, v) })
let extra_names : Array[String] = predeclared.bindings.keys().collect()
let (prog, rm) = match
compile_and_run_vm(ctx, file, filename, fn(name) {
extra_names.contains(name)
}) {
Ok(pair) => pair
Err(e) => return Err(e)
}
let m = { ..Module::new(), predeclared: predeclared.bindings }
module_output(prog, rm, opts).each(fn(name, v) {
let key = @value.Value::String(@value.StarlarkString::new(name))
m.globals.set(key, v) |> ignore
})
match m.freeze_checked() {
Err(e) => return Err(@errors.EvalError::simple(e))
Ok(_) => ()
}
Ok(m)
}
///|
/// Executes one REPL chunk: parses `src`, resolves it against `globals`,
/// evaluates it, and writes any new or updated bindings back into `globals`.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `filename` : Source file name used in error messages and position info.
/// - `src` : Starlark source chunk to execute (may be statements or an expression).
/// - `globals` : Mutable dict that persists global bindings across REPL chunks.
/// - `opts` : Feature flags controlling which dialect features are active.
///
/// Returns `Ok(())` on success, or an `EvalError` on failure.
///
/// ```mbt check
/// test {
/// let thread = Thread::new("test")
/// let globals = @value.StringDict::new()
/// // First chunk: define x.
/// exec_repl_chunk(thread, "", "x = 6", globals, Options::default())
/// |> Result::unwrap
/// // Second chunk: x is still visible; bindings accumulate across calls.
/// exec_repl_chunk(thread, "", "y = x * 7", globals, Options::default())
/// |> Result::unwrap
/// inspect(globals.get("y").unwrap().repr(), content="42")
/// }
/// ```
pub fn exec_repl_chunk(
thread : Thread,
filename : String,
src : String,
globals : @value.StringDict,
opts : Options,
) -> Result[Unit, @errors.EvalError] {
let file = match @parser.parse_file(filename, src) {
Ok(f) => f
Err(e) => return Err(@errors.EvalError::simple(e.to_string()))
}
// REPL chunks bind globally so each chunk's bindings persist into the next.
let chunk_opts = { ..opts, load_binds_globally: true }
let ctx = EvalContext::new(thread, chunk_opts)
globals.each(fn(k, v) { ctx.global_env.bind(k, v) })
let extra_names = globals.keys()
// Seed this chunk's module-global slots with the values accumulated so far so
// a global the chunk reassigns starts from its previous value.
let (prog, m) = match
compile_and_run_vm(
ctx,
file,
filename,
fn(name) { extra_names.contains(name) },
init=fn(name) { globals.get(name) },
) {
Ok(pair) => pair
Err(e) => return Err(e)
}
module_output(prog, m, chunk_opts).each(fn(name, v) { globals.set(name, v) })
Ok(())
}
///|
/// Parses and evaluates a single Starlark expression with the default options,
/// using `env` as the global environment.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `filename` : Source file name used in error messages and position info.
/// - `src` : Starlark expression source text to evaluate.
/// - `env` : Global environment dict supplying variable bindings.
///
/// Returns the evaluated `Value`, or an `EvalError` on failure.
///
/// ```mbt check
/// test {
/// let thread = Thread::new("test")
/// let env = @value.StringDict::new()
/// // Evaluate an arithmetic expression.
/// inspect(eval_expr(thread, "", "2 + 3", env).unwrap().repr(), content="5")
/// // Supply a binding via env.
/// env.set("n", @value.Value::new_int(10L))
/// inspect(eval_expr(thread, "", "n * 2", env).unwrap().repr(), content="20")
/// // An undefined name yields EvalError.
/// inspect(eval_expr(thread, "", "ghost", env) is Err(_), content="true")
/// }
/// ```
pub fn eval_expr(
thread : Thread,
filename : String,
src : String,
env : @value.StringDict,
) -> Result[@value.Value, @errors.EvalError] {
eval_expr_with_opts(thread, filename, src, Options::default(), env)
}
///|
/// Like `eval_expr` but accepts explicit `opts`.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `filename` : Source file name used in error messages and position info.
/// - `src` : Starlark expression source text to evaluate.
/// - `opts` : Feature flags controlling which dialect features are active.
/// - `env` : Global environment dict supplying variable bindings.
///
/// Returns the evaluated `Value`, or an `EvalError` on failure.
pub fn eval_expr_with_opts(
thread : Thread,
filename : String,
src : String,
opts : Options,
env : @value.StringDict,
) -> Result[@value.Value, @errors.EvalError] {
let expr = match @parser.parse_expr(filename, src) {
Ok(e) => e
Err(e) => return Err(@errors.EvalError::simple(e.to_string()))
}
eval_expr_vm(thread, expr, opts, env)
}
///|
/// Evaluates a pre-parsed expression node under `opts` with the bindings in
/// `env` as the global environment.
///
/// Parameters:
///
/// - `thread` : Execution context providing print/load callbacks and step budget.
/// - `expr` : Pre-parsed expression AST node to evaluate.
/// - `opts` : Feature flags controlling which dialect features are active.
/// - `env` : Global environment dict supplying variable bindings.
///
/// Returns the evaluated `Value`, or an `EvalError` on runtime failure.
pub fn eval_parsed_expr(
thread : Thread,
expr : @syntax.Expr,
opts : Options,
env : @value.StringDict,
) -> Result[@value.Value, @errors.EvalError] {
eval_expr_vm(thread, expr, opts, env)
}