use std::error::Error;
use std::fs;
use std::sync::Arc;
use std::thread;
use airsl::{Engine, InstructionLimit, Policy, ResourceLimits, Script};
use tempfile::TempDir;
const DOUBLE: &str = "return tonumber(arg[1]) * 2";
const REMEMBER: &str = "SESSION = 'written by the first script' return arg[0]";
const RECALL: &str = "return tostring(SESSION)";
const RUNAWAY: &str = "while true do end";
const TIGHT_INSTRUCTIONS: InstructionLimit = InstructionLimit::count(100_000);
const WORKERS: i64 = 4;
fn main() -> Result<(), Box<dyn Error>> {
let engine = Engine::builder().policy(Policy::confined()).build()?;
arguments_are_per_evaluation(&engine)?;
println!();
the_instruction_counter_is_per_evaluation()?;
println!();
globals_outlive_the_evaluation(&engine)?;
println!();
require_is_rooted_per_script_and_cached_per_engine(&engine)?;
println!();
one_engine_across_threads()?;
Ok(())
}
fn arguments_are_per_evaluation(engine: &Engine) -> Result<(), Box<dyn Error>> {
println!("the arg table is written before every evaluation:");
let double = Script::from_source(DOUBLE, "double")?;
for n in 1..=3i64 {
let run = double.clone().with_args([n.to_string()]);
println!(" arg[1] = {n} doubles to {}", engine.eval_to::<i64>(&run)?);
}
let reporter = Script::from_source("return arg[0]", "reporter")?;
println!(
" arg[0] is the chunk's own name: {}",
engine.eval_to::<String>(&reporter)?
);
Ok(())
}
fn the_instruction_counter_is_per_evaluation() -> Result<(), Box<dyn Error>> {
let engine = Engine::builder()
.policy(
Policy::confined()
.with_limits(ResourceLimits::none().with_instructions(Some(TIGHT_INSTRUCTIONS))),
)
.build()?;
let stopped = engine
.eval(&Script::from_source(RUNAWAY, "runaway")?)
.err()
.ok_or("the instruction ceiling did not stop a non-terminating script")?;
println!("the instruction counter is reset before every evaluation:");
println!(" the first evaluation exhausts the budget: {stopped}");
let after = engine.eval_to::<i64>(&Script::from_source("return 7", "after")?)?;
println!(" the next evaluation gets the whole ceiling again, and returns {after}");
Ok(())
}
fn globals_outlive_the_evaluation(engine: &Engine) -> Result<(), Box<dyn Error>> {
let recall = Script::from_source(RECALL, "recall")?;
println!("the globals a script writes outlive it:");
println!(
" SESSION, before anything wrote it: {}",
engine.eval_to::<String>(&recall)?
);
let author = engine.eval_to::<String>(&Script::from_source(REMEMBER, "remember")?)?;
println!(" a script named `{author}` assigns it");
println!(
" SESSION, read back by an unrelated script: {}",
engine.eval_to::<String>(&recall)?
);
Ok(())
}
fn require_is_rooted_per_script_and_cached_per_engine(
engine: &Engine,
) -> Result<(), Box<dyn Error>> {
let dir = TempDir::new()?;
for name in ["alpha", "beta"] {
let root = dir.path().join(name);
fs::create_dir(&root)?;
fs::write(root.join("lib.lua"), format!("return '{name}'"))?;
fs::write(root.join("main.lua"), "return require('lib')")?;
}
println!("the require root belongs to the script, not to the engine:");
for name in ["alpha", "beta"] {
let path = dir.path().join(name).join("main.lua");
let script = Script::from_file(&path)?.with_name(format!("{name}/main.lua"))?;
println!(
" {name}/main.lua resolves require('lib') against its own directory: {}",
engine.eval_to::<String>(&script)?
);
}
let alpha = dir.path().join("alpha");
fs::write(
alpha.join("counter.lua"),
"COUNT = (COUNT or 0) + 1 return COUNT",
)?;
fs::write(alpha.join("count.lua"), "return require('counter')")?;
let count = Script::from_file(alpha.join("count.lua"))?.with_name("alpha/count.lua")?;
let mut runs = Vec::new();
for _ in 0..3 {
runs.push(engine.eval_to::<i64>(&count)?.to_string());
}
println!();
println!("the require module cache belongs to the engine, not to the script:");
println!(
" one counting module, required by three evaluations: {}",
runs.join(", ")
);
Ok(())
}
fn one_engine_across_threads() -> Result<(), Box<dyn Error>> {
let engine = Arc::new(Engine::builder().policy(Policy::confined()).build()?);
let mut workers = Vec::new();
for id in 0..WORKERS {
let engine = Arc::clone(&engine);
workers.push(thread::spawn(move || {
let script = Script::from_source(DOUBLE, "double")?.with_args([id.to_string()]);
engine.eval_to::<i64>(&script).map(|doubled| (id, doubled))
}));
}
let mut results = Vec::new();
for worker in workers {
results.push(worker.join().map_err(|_| "a worker thread panicked")??);
}
results.sort_unstable();
println!("one engine shared by {WORKERS} threads through an Arc:");
for (id, doubled) in results {
println!(" thread {id} saw its own argument and doubled it to {doubled}");
}
println!(" evaluations are serialised: sharing avoids rebuilding, it does not parallelise");
Ok(())
}