dellingr 0.2.0

An embeddable, pure-Rust Lua VM with precise instruction-cost accounting
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! A Lua VM designed for game scripting with cost budgets and host callbacks.
//!
//! # Features
//!
//! - **Cost budgets**: Control script execution with configurable operation costs
//! - **Host callbacks**: Redirect print output and handle errors
//! - **Stack traces**: Detailed error messages with source locations
//!
//! # Example
//!
//! ```
//! use dellingr::{State, ArgCount, RetCount};
//!
//! let mut state = State::new();
//! state.load_string("print('Hello!')").unwrap();
//! state.call(ArgCount::Fixed(0), RetCount::Fixed(0)).unwrap();
//! ```

#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![cfg_attr(not(test), deny(clippy::float_cmp))]
#![warn(missing_docs)]

mod compiler;
mod host;
mod instr;
mod lua_std;
#[doc(hidden)]
mod patterns;
mod vm;
mod vm_aux;

/// Error types returned by the VM and parser. Surfaced through [`Result`].
pub mod error;

pub use host::{DefaultCallbacks, HostCallbacks};
pub use instr::{ArgCount, RetCount};
pub use vm::Anchor;
pub use vm::LuaType;
pub use vm::RustFunc;
pub use vm::State;

use compiler::Bytecode;
use instr::Instr;
use std::sync::Arc;

// Compile-time witness that `State` can be moved across threads. This is the
// load-bearing property for sharing dellingr with multi-threaded async
// runtimes (axum, tokio worker pools): embedders can hold a `Mutex<State>`
// behind an `Arc` and dispatch calls from any worker. `State` is
// deliberately NOT `Sync` - cost-budgeted dispatch and the bytecode cache
// invariants only have well-defined semantics under exclusive access.
const _: () = {
    const fn assert_send<T: Send>() {}
    assert_send::<State>();
};

/// Custom result type for evaluating Lua.
pub type Result<T> = std::result::Result<T, error::Error>;

/// Cost breakdown for a single scope (function or main chunk).
#[derive(Debug, Default, Clone)]
pub struct ScopeCost {
    /// Name of this scope
    pub name: String,
    /// Minimum cost of this scope alone (not including nested)
    pub own_cost: u64,
    /// Total cost including all nested scopes
    pub total_cost: u64,
    /// Number of arithmetic operations (+, -, *, /, %, ^)
    pub arithmetic_ops: u64,
    /// Number of unary negation operations
    pub negations: u64,
    /// Number of table creations ({})
    pub table_creations: u64,
    /// Number of table field writes (`t.x = v`, `t[k] = v`)
    pub table_writes: u64,
    /// Number of array elements initialized
    pub array_elements: u64,
    /// Number of function calls
    pub function_calls: u64,
    /// Total instruction count
    pub instructions: u64,
    /// Nested scopes (functions defined in this scope)
    pub nested: Vec<ScopeCost>,
}

impl ScopeCost {
    fn analyze_chunk(chunk: &Bytecode, name: String) -> Self {
        let mut scope = ScopeCost {
            name,
            ..Default::default()
        };

        for inst in &chunk.code {
            scope.instructions += 1;
            match inst.opcode() {
                // Arithmetic (cost 1 each)
                Instr::OP_ADD
                | Instr::OP_SUBTRACT
                | Instr::OP_MULTIPLY
                | Instr::OP_DIVIDE
                | Instr::OP_POW
                | Instr::OP_MOD => {
                    scope.arithmetic_ops += 1;
                    scope.own_cost += 1;
                }
                // Unary negation (cost 1)
                Instr::OP_NEGATE => {
                    scope.negations += 1;
                    scope.own_cost += 1;
                }
                // Table creation (cost 1)
                Instr::OP_NEW_TABLE => {
                    scope.table_creations += 1;
                    scope.own_cost += 1;
                }
                // Table writes (cost 1 each)
                Instr::OP_INIT_FIELD
                | Instr::OP_INIT_INDEX
                | Instr::OP_SET_FIELD
                | Instr::OP_SET_TABLE => {
                    scope.table_writes += 1;
                    scope.own_cost += 1;
                }
                // Array initialization (cost = n elements)
                Instr::OP_SET_LIST => {
                    let n = inst.a();
                    if n == 0 {
                        scope.own_cost += 1;
                    } else {
                        scope.array_elements += n as u64;
                        scope.own_cost += n as u64;
                    }
                }
                // Function calls
                Instr::OP_CALL => {
                    scope.function_calls += 1;
                }
                _ => {}
            }
        }

        // Recursively analyze nested functions
        for (i, nested_chunk) in chunk.nested.iter().enumerate() {
            let nested_name = match &nested_chunk.name {
                Some(name) => name.clone(),
                None => format!("anonymous #{}", i + 1),
            };
            let nested_scope = Self::analyze_chunk(nested_chunk, nested_name);
            scope.nested.push(nested_scope);
        }

        // Calculate total cost (own + all nested)
        scope.total_cost = scope.own_cost + scope.nested.iter().map(|n| n.total_cost).sum::<u64>();

        scope
    }

    fn fmt_indent(&self, f: &mut std::fmt::Formatter<'_>, indent: usize) -> std::fmt::Result {
        let pad = "  ".repeat(indent);

        if self.own_cost == 0 && self.nested.is_empty() {
            writeln!(f, "{}{}: cost 0 (free)", pad, self.name)?;
            return Ok(());
        }

        // Header with cost
        if self.nested.is_empty() {
            writeln!(f, "{}{}: cost {}", pad, self.name, self.own_cost)?;
        } else {
            writeln!(
                f,
                "{}{}: cost {} (own) / {} (total)",
                pad, self.name, self.own_cost, self.total_cost
            )?;
        }

        // Breakdown if there's any cost
        if self.own_cost > 0 {
            let inner_pad = "  ".repeat(indent + 1);
            if self.arithmetic_ops > 0 {
                writeln!(f, "{}arithmetic: {}", inner_pad, self.arithmetic_ops)?;
            }
            if self.negations > 0 {
                writeln!(f, "{}negation: {}", inner_pad, self.negations)?;
            }
            if self.table_creations > 0 {
                writeln!(f, "{}table creation: {}", inner_pad, self.table_creations)?;
            }
            if self.table_writes > 0 {
                writeln!(f, "{}table writes: {}", inner_pad, self.table_writes)?;
            }
            if self.array_elements > 0 {
                writeln!(f, "{}array elements: {}", inner_pad, self.array_elements)?;
            }
        }

        // Nested scopes
        for nested in &self.nested {
            nested.fmt_indent(f, indent + 1)?;
        }

        Ok(())
    }
}

/// Static cost analysis of a Lua script.
///
/// This analyzes the bytecode without executing it. The actual runtime cost
/// depends on which code paths are taken and how many loop iterations occur.
#[derive(Debug, Default)]
pub struct CostAnalysis {
    /// Root scope (main chunk)
    pub root: ScopeCost,
}

impl CostAnalysis {
    /// Collect totals across all scopes
    fn totals(&self) -> ScopeTotals {
        let mut totals = ScopeTotals::default();
        self.root.accumulate(&mut totals);
        totals
    }
}

#[derive(Default)]
struct ScopeTotals {
    total_cost: u64,
    arithmetic_ops: u64,
    negations: u64,
    table_creations: u64,
    table_writes: u64,
    array_elements: u64,
    function_calls: u64,
    instructions: u64,
    function_count: u64,
}

impl ScopeCost {
    fn accumulate(&self, totals: &mut ScopeTotals) {
        totals.total_cost += self.own_cost;
        totals.arithmetic_ops += self.arithmetic_ops;
        totals.negations += self.negations;
        totals.table_creations += self.table_creations;
        totals.table_writes += self.table_writes;
        totals.array_elements += self.array_elements;
        totals.function_calls += self.function_calls;
        totals.instructions += self.instructions;
        totals.function_count += self.nested.len() as u64;
        for nested in &self.nested {
            nested.accumulate(totals);
        }
    }
}

impl std::fmt::Display for CostAnalysis {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let totals = self.totals();

        writeln!(f, "=== Cost Analysis ===")?;
        writeln!(f)?;
        writeln!(f, "Minimum cost (static): {}", totals.total_cost)?;
        writeln!(f)?;
        writeln!(f, "--- Costed Operations ---")?;
        if totals.arithmetic_ops > 0 {
            writeln!(
                f,
                "  Arithmetic (+,-,*,/,%,^): {} ops",
                totals.arithmetic_ops
            )?;
        }
        if totals.negations > 0 {
            writeln!(f, "  Unary negation (-):       {} ops", totals.negations)?;
        }
        if totals.table_creations > 0 {
            writeln!(
                f,
                "  Table creation {{}}:        {} ops",
                totals.table_creations
            )?;
        }
        if totals.table_writes > 0 {
            writeln!(f, "  Table writes (t[k]=v):    {} ops", totals.table_writes)?;
        }
        if totals.array_elements > 0 {
            writeln!(
                f,
                "  Array elements:           {} elements",
                totals.array_elements
            )?;
        }
        writeln!(f)?;
        writeln!(f, "--- Statistics ---")?;
        writeln!(f, "  Total instructions:   {}", totals.instructions)?;
        writeln!(f, "  Function definitions: {}", totals.function_count)?;
        writeln!(f, "  Function calls:       {}", totals.function_calls)?;
        writeln!(f)?;
        writeln!(f, "--- Per-Scope Breakdown ---")?;
        self.root.fmt_indent(f, 0)?;
        Ok(())
    }
}

/// Analyze the cost of a Lua script without executing it.
///
/// Returns a `CostAnalysis` with per-scope cost breakdown.
/// The actual runtime cost depends on control flow and loop iterations.
///
/// For repeated analysis of the same source, prefer `Engine::compile` followed
/// by `Engine::analyze_cost(&program)` so the parse is paid once.
pub fn analyze_cost(source: &str) -> Result<CostAnalysis> {
    let bc = compiler::parse_str(source)?;
    let root = ScopeCost::analyze_chunk(&bc, "main".to_string());
    Ok(CostAnalysis { root })
}

/// A factory for compiling Lua source and creating new `State`s.
///
/// `Engine` is `Send + Sync`: a single instance can be shared across worker
/// threads via `Arc`. Compile a `Program` once on the engine, then load it
/// into per-thread (or per-request) `State`s.
///
/// ```ignore
/// use std::sync::Arc;
/// let engine = Arc::new(dellingr::Engine::new());
/// let program = engine.compile("return 1 + 2").unwrap();
///
/// // On each worker thread:
/// let mut state = engine.new_state();
/// state.load(&program).unwrap();
/// state.call(dellingr::ArgCount::Fixed(0), dellingr::RetCount::Fixed(1)).unwrap();
/// ```
#[derive(Debug)]
pub struct Engine {
    install_stdlib: bool,
}

impl Default for Engine {
    fn default() -> Self {
        Self::new()
    }
}

impl Engine {
    /// Create an engine that installs the standard library on each new `State`.
    pub fn new() -> Self {
        Self {
            install_stdlib: true,
        }
    }

    /// Create an engine whose new `State`s start with empty global namespaces,
    /// matching `lua_newstate` in the reference C API.
    pub fn raw() -> Self {
        Self {
            install_stdlib: false,
        }
    }

    /// Compile a Lua source string into an executable `Program`.
    pub fn compile(&self, source: &str) -> Result<Program> {
        let bc = compiler::parse_str(source)?;
        Ok(Program(Arc::new(bc)))
    }

    /// Compile a Lua source string with a source name used in error messages
    /// and stack traces (e.g. a filename or `"[fleet:123]"`).
    pub fn compile_named(&self, source: &str, name: impl Into<String>) -> Result<Program> {
        let bc = compiler::parse_str_named(source, Some(name.into()))?;
        Ok(Program(Arc::new(bc)))
    }

    /// Statically analyze the cost of a compiled `Program`. No execution.
    pub fn analyze_cost(&self, program: &Program) -> CostAnalysis {
        let root = ScopeCost::analyze_chunk(&program.0, "main".to_string());
        CostAnalysis { root }
    }

    /// Create a new `State` configured by this engine.
    pub fn new_state(&self) -> State {
        self.new_state_with_callbacks(Box::new(DefaultCallbacks))
    }

    /// Create a new `State` configured by this engine with custom callbacks.
    pub fn new_state_with_callbacks(&self, callbacks: Box<dyn HostCallbacks + Send>) -> State {
        if self.install_stdlib {
            State::with_callbacks(callbacks)
        } else {
            State::empty_with_callbacks(callbacks)
        }
    }
}

/// A compiled, executable Lua program. Cheap to clone (refcounted) and safe
/// to share across threads. Load with `State::load` to execute.
#[derive(Clone, Debug)]
pub struct Program(Arc<Bytecode>);

impl Program {
    /// Returns the optional source name attached at compile time.
    pub fn source_name(&self) -> Option<&str> {
        self.0.source.as_deref()
    }
}

impl State {
    /// Load a compiled `Program` onto this `State`'s stack as a callable
    /// closure. Pair with `state.call(ArgCount::Fixed(0), ...)` to execute.
    ///
    /// The same `Program` can be loaded into many different `State`s and run
    /// concurrently from different threads (each State holds its own caches
    /// and heap; only the immutable bytecode is shared).
    pub fn load(&mut self, program: &Program) -> Result<()> {
        // Mirror load_string_named: track the source for callback context.
        self.current_source = program.0.source.clone();
        self.push_chunk(Arc::clone(&program.0));
        Ok(())
    }
}