#![deny(missing_docs)]
mod duration;
mod runtasks;
mod strict_mode;
use std::env;
use std::fs;
use std::io::{self, BufRead, Write};
use std::path::Path;
use std::process::ExitCode;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use keleusma::compiler::{CompileOptions, compile_with_options};
use keleusma::lexer::tokenize;
use keleusma::parser::parse;
use keleusma::stddsl;
use keleusma::vm::{DEFAULT_ARENA_CAPACITY, Vm, VmState};
use keleusma::{Arena, Value};
use strict_mode::{PolicyContext, X25519_PRIVATE_KEY_LEN, build_policy_context};
const REPL_BANNER: &str = "Keleusma REPL. Type :help for commands, :quit to exit.";
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
let subcommand = match args.get(1) {
Some(s) => s.as_str(),
None => {
print_help();
return ExitCode::SUCCESS;
}
};
match subcommand {
"run" => run_subcommand(&args[2..]),
"run-tasks" => run_tasks_subcommand(&args[2..]),
"compile" => compile_subcommand(&args[2..]),
"strip" => strip_subcommand(&args[2..]),
"keygen" => keygen_subcommand(&args[2..]),
"repl" => repl_subcommand(&args[2..]),
"--help" | "-h" | "help" => {
print_help();
ExitCode::SUCCESS
}
"--version" | "-V" | "version" => {
println!("keleusma {}", env!("CARGO_PKG_VERSION"));
ExitCode::SUCCESS
}
other => {
if std::path::Path::new(other).is_file() {
let ctx = match build_policy_context() {
Ok(c) => c,
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
};
let verifying = ctx.enrolled_keys.clone();
let decrypting = ctx.decryption_keys.clone();
let mut argv = Vec::with_capacity(args.len() - 1);
argv.push(other.to_string());
argv.extend(args[2..].iter().cloned());
stddsl::shell::set_script_args(argv);
run_file(
other,
&verifying,
&decrypting,
&ctx,
&LoopRunnerConfig::default(),
false,
)
} else {
eprintln!("error: unknown subcommand or missing file `{}`", other);
print_help();
ExitCode::FAILURE
}
}
}
}
fn print_help() {
println!("keleusma: command-line frontend for the Keleusma scripting language");
println!();
println!("Usage:");
println!(" keleusma <subcommand> [options]");
println!(" keleusma <file>.kel (shorthand for `run`)");
println!();
println!("Subcommands:");
println!(" run <file> [--verifying-key <keyfile> ...]");
println!(" [--decryption-key <keyfile> ...]");
println!(" [--tick-interval <duration>] [--quiet] [--print-memory]");
println!(" Compile and execute a script.");
println!(" Pass --verifying-key (repeatable) to verify");
println!(" signed compiled bytecode against the");
println!(" supplied 32-byte Ed25519 public-key files.");
println!(" Pass --tick-interval to rate-limit a");
println!(" productive-divergent `loop main` entry");
println!(" point. Accepts humanized durations such as");
println!(" 100ms, 1s, 1m, 1h, 1d, 1w. Maximum four");
println!(" weeks. --quiet suppresses the stderr warning");
println!(" that fires when an iteration exceeds the");
println!(" configured interval. --print-memory reports the");
println!(" program's worst-case arena footprint and exits");
println!(" without running, for provisioning a host.");
println!(" compile <file> [-o <output>] [--signing-key <keyfile>]");
println!(" [--encryption-key <keyfile>] [--target <name>] [--debug]");
println!(" Compile to bytecode. With --debug, emit");
println!(" strippable debug metadata (source spans for");
println!(" stack traces); remove it later with `strip`.");
println!(" With --signing-key,");
println!(" sign the output with the supplied 32-byte");
println!(" Ed25519 seed file. The source script must");
println!(" declare the entry function with the");
println!(" `signed` modifier; otherwise the resulting");
println!(" bytecode is unsigned and the toolchain");
println!(" refuses the signing key argument silently.");
println!(" --target selects the target descriptor:");
println!(" host (default), wasm32, embedded_32,");
println!(" embedded_16, embedded_8. Programs are");
println!(" validated against the selected target's");
println!(" word, address, and float widths.");
println!(" keygen --seed <out> --public <out>");
println!(" Generate a fresh Ed25519 keypair from the");
println!(" OS RNG. Writes the 32-byte signing seed to");
println!(" one file and the 32-byte verifying key to");
println!(" another. Treat the seed as a private");
println!(" secret; the verifying key may be");
println!(" distributed to hosts that load signed");
println!(" bytecode.");
println!(" run-tasks <manifest.toml> [--quiet]");
println!(" Multi-script runner. Loads tasks from a");
println!(" TOML manifest and drives them through a");
println!(" cooperative scheduler with event queue,");
println!(" supervised restart, and per-task signing");
println!(" and encryption policy. See");
println!(" docs/architecture/RUN_TASKS.md.");
println!(" strip <file.bin> [-o <output>]");
println!(" Remove debug metadata from compiled");
println!(" bytecode, producing a release artefact byte-");
println!(" identical to a non-debug compile. Output");
println!(" defaults to the input path. Refuses signed or");
println!(" encrypted input; strip before signing.");
println!(" repl Start interactive REPL");
println!(" help, --help, -h Show this help");
println!(" version, --version, -V Show version");
println!();
println!("Examples:");
println!(" keleusma run hello.kel");
println!(" keleusma hello.kel");
println!(" keleusma compile hello.kel -o hello.kel.bin");
println!(" keleusma compile hello.kel --debug -o hello.dbg.bin");
println!(" keleusma strip hello.dbg.bin -o hello.kel.bin");
println!(" keleusma keygen --seed key.seed --public key.pub");
println!(" keleusma compile hello.kel --signing-key key.seed -o hello.kel.bin");
println!(" keleusma run hello.kel.bin --verifying-key key.pub");
println!(" keleusma repl");
println!();
println!("Strict-mode policies (signing and encryption):");
println!(" Signing: place 32-byte Ed25519 public keys as `*.pub`");
println!(" files in /etc/keleusma/trusted_keys (Unix) or");
println!(" %PROGRAMDATA%\\keleusma\\trusted_keys (Windows), or in");
println!(" the directory named by KELEUSMA_TRUSTED_KEYS_DIR. The");
println!(" CLI refuses source files, unsigned bytecode, and");
println!(" bytecode signed by keys not in the trust store. The");
println!(" --verifying-key argument is rejected. Set");
println!(" KELEUSMA_REQUIRE_SIGNED=1 to force strict signing mode");
println!(" even with an empty trust store.");
println!();
println!(" Encryption: place 32-byte X25519 private keys as");
println!(" `*.seed` files in /etc/keleusma/decryption_keys (Unix)");
println!(" or the equivalent Windows path, or in the directory");
println!(" named by KELEUSMA_DECRYPTION_KEYS_DIR. The CLI refuses");
println!(" unencrypted bytecode and bytecode encrypted to a key");
println!(" not in the decryption-key store. The --decryption-key");
println!(" argument is rejected. Set KELEUSMA_REQUIRE_ENCRYPTED=1");
println!(" to force strict encryption mode.");
println!();
println!(" The two policies are independent: neither, signing");
println!(" only, encryption only, or both may be active.");
println!();
println!("Examples (encryption):");
println!(" keleusma keygen --kind encryption --seed dest.seed --public dest.pub");
println!(" keleusma compile script.kel --signing-key sign.seed \\");
println!(" --encryption-key dest.pub -o script.kel.bin");
println!(" keleusma run script.kel.bin --verifying-key sign.pub \\");
println!(" --decryption-key dest.seed");
}
fn run_subcommand(args: &[String]) -> ExitCode {
if args.is_empty() {
eprintln!("error: `run` requires a script path");
return ExitCode::FAILURE;
}
let path = &args[0];
let ctx = match build_policy_context() {
Ok(c) => c,
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
};
let mut command_line_keys: Vec<ed25519_dalek::VerifyingKey> = Vec::new();
let mut command_line_decryption_keys: Vec<[u8; X25519_PRIVATE_KEY_LEN]> = Vec::new();
let mut loop_config = LoopRunnerConfig::default();
let mut print_memory = false;
let mut script_args: Vec<String> = Vec::new();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--verifying-key" => {
if i + 1 >= args.len() {
eprintln!(
"error: --verifying-key requires a path to a 32-byte public-key file"
);
return ExitCode::FAILURE;
}
match read_verifying_key(&args[i + 1]) {
Ok(k) => command_line_keys.push(k),
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
}
i += 2;
}
"--decryption-key" => {
if i + 1 >= args.len() {
eprintln!(
"error: --decryption-key requires a path to a 32-byte X25519 seed file"
);
return ExitCode::FAILURE;
}
match read_x25519_private_key(&args[i + 1]) {
Ok(k) => command_line_decryption_keys.push(k),
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
}
i += 2;
}
"--tick-interval" => {
if i + 1 >= args.len() {
eprintln!(
"error: --tick-interval requires a humanized duration (for example 100ms, 1s, 1m, 1h, 1d, 1w)"
);
return ExitCode::FAILURE;
}
match duration::parse(&args[i + 1]) {
Ok(d) => {
loop_config
.tick_interval_nanos
.store(d.as_nanos() as u64, Ordering::Relaxed);
}
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
}
i += 2;
}
"--quiet" => {
loop_config.quiet = true;
i += 1;
}
"--print-memory" => {
print_memory = true;
i += 1;
}
"--" => {
script_args.extend(args[i + 1..].iter().cloned());
break;
}
other if other.starts_with('-') => {
eprintln!("error: unknown option `{}`", other);
return ExitCode::FAILURE;
}
other => {
script_args.push(other.to_string());
i += 1;
}
}
}
if ctx.strict_signing && !command_line_keys.is_empty() {
eprintln!(
"error: strict mode: --verifying-key is rejected; the trust list is system-managed through KELEUSMA_TRUSTED_KEYS_DIR or the platform-conventional directory"
);
return ExitCode::FAILURE;
}
if ctx.strict_encryption && !command_line_decryption_keys.is_empty() {
eprintln!(
"error: strict mode: --decryption-key is rejected; the decryption-key store is system-managed through KELEUSMA_DECRYPTION_KEYS_DIR or the platform-conventional directory"
);
return ExitCode::FAILURE;
}
let mut verifying_keys = ctx.enrolled_keys.clone();
verifying_keys.extend(command_line_keys);
let mut decryption_keys = ctx.decryption_keys.clone();
decryption_keys.extend(command_line_decryption_keys);
let mut argv = Vec::with_capacity(1 + script_args.len());
argv.push(path.clone());
argv.extend(script_args);
stddsl::shell::set_script_args(argv);
run_file(
path,
&verifying_keys,
&decryption_keys,
&ctx,
&loop_config,
print_memory,
)
}
fn run_file(
path: &str,
verifying_keys: &[ed25519_dalek::VerifyingKey],
decryption_keys: &[[u8; X25519_PRIVATE_KEY_LEN]],
policy: &PolicyContext,
loop_config: &LoopRunnerConfig,
print_memory: bool,
) -> ExitCode {
let bytes = match fs::read(path) {
Ok(b) => b,
Err(e) => {
eprintln!("error: reading {}: {}", path, e);
return ExitCode::FAILURE;
}
};
let result = if looks_like_bytecode(&bytes) {
execute_bytecode(
&bytes,
verifying_keys,
decryption_keys,
policy,
loop_config,
print_memory,
)
} else if policy.strict_signing || policy.strict_encryption {
eprintln!(
"error: strict mode: source execution disabled; compile{} the source before running",
if policy.strict_encryption {
", sign, and encrypt"
} else {
" and sign"
}
);
return ExitCode::FAILURE;
} else if !verifying_keys.is_empty() || !decryption_keys.is_empty() {
eprintln!(
"error: --verifying-key or --decryption-key supplied but {} is source, not bytecode",
path
);
return ExitCode::FAILURE;
} else {
let source = match core::str::from_utf8(&bytes) {
Ok(s) => s,
Err(_) => {
eprintln!(
"error: {} is neither valid UTF-8 source nor recognised bytecode",
path
);
return ExitCode::FAILURE;
}
};
execute_source(source, loop_config, print_memory)
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {}", e);
ExitCode::FAILURE
}
}
}
fn looks_like_bytecode(bytes: &[u8]) -> bool {
let after_shebang = if bytes.starts_with(b"#!") {
match bytes.iter().position(|&b| b == b'\n') {
Some(nl) => &bytes[nl + 1..],
None => return false,
}
} else {
bytes
};
after_shebang.starts_with(b"KELE")
}
fn run_tasks_subcommand(args: &[String]) -> ExitCode {
if args.is_empty() {
eprintln!("error: `run-tasks` requires a manifest path");
return ExitCode::FAILURE;
}
let manifest_path = &args[0];
let mut quiet = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--quiet" => {
quiet = true;
i += 1;
}
other => {
eprintln!("error: unknown option `{}`", other);
return ExitCode::FAILURE;
}
}
}
let outcome = runtasks::run(std::path::Path::new(manifest_path), quiet);
outcome.into_exit_code()
}
fn compile_subcommand(args: &[String]) -> ExitCode {
if args.is_empty() {
eprintln!("error: `compile` requires a script path");
return ExitCode::FAILURE;
}
let input = &args[0];
let mut output: Option<String> = None;
let mut signing_key_path: Option<String> = None;
let mut encryption_key_path: Option<String> = None;
let mut target: Option<keleusma::target::Target> = None;
let mut emit_debug = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-o" | "--output" => {
if i + 1 >= args.len() {
eprintln!("error: --output requires a path");
return ExitCode::FAILURE;
}
output = Some(args[i + 1].clone());
i += 2;
}
"--signing-key" => {
if i + 1 >= args.len() {
eprintln!("error: --signing-key requires a path to a 32-byte seed file");
return ExitCode::FAILURE;
}
signing_key_path = Some(args[i + 1].clone());
i += 2;
}
"--encryption-key" => {
if i + 1 >= args.len() {
eprintln!(
"error: --encryption-key requires a path to a 32-byte X25519 public-key file"
);
return ExitCode::FAILURE;
}
encryption_key_path = Some(args[i + 1].clone());
i += 2;
}
"--target" => {
if i + 1 >= args.len() {
eprintln!(
"error: --target requires a target name (host, wasm32, embedded_32, embedded_16, embedded_8)"
);
return ExitCode::FAILURE;
}
match parse_target_name(&args[i + 1]) {
Ok(t) => target = Some(t),
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
}
i += 2;
}
"--debug" => {
emit_debug = true;
i += 1;
}
other => {
eprintln!("error: unknown option `{}`", other);
return ExitCode::FAILURE;
}
}
}
let output_path = output.unwrap_or_else(|| default_output_path(input));
if encryption_key_path.is_some() && signing_key_path.is_none() {
eprintln!(
"error: --encryption-key requires --signing-key; encrypted artefacts must be signed"
);
return ExitCode::FAILURE;
}
if let Ok(policy) = build_policy_context() {
if policy.strict_signing && signing_key_path.is_none() {
eprintln!(
"warning: local host runs strict signing mode; the produced artefact will be unsigned and will not run on this host"
);
}
if policy.strict_signing
&& let Some(ref sign_path) = signing_key_path
&& let Ok(signing_key) = read_signing_key(sign_path)
{
let verifying = signing_key.verifying_key();
if !policy.enrolled_keys.contains(&verifying) {
eprintln!(
"warning: the signing key's verifying counterpart is not in this host's trust list; the produced artefact will not run on this host"
);
}
}
if policy.strict_encryption && encryption_key_path.is_none() {
eprintln!(
"warning: local host runs strict encryption mode; the produced artefact will be unencrypted and will not run on this host"
);
}
}
let source = match fs::read_to_string(input) {
Ok(s) => s,
Err(e) => {
eprintln!("error: reading {}: {}", input, e);
return ExitCode::FAILURE;
}
};
let module = match compile_source_with_target(&source, target.as_ref(), emit_debug) {
Ok(m) => m,
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
};
let bytes = match (signing_key_path, encryption_key_path) {
(Some(sign_path), Some(enc_path)) => {
let signing_key = match read_signing_key(&sign_path) {
Ok(k) => k,
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
};
let recipient_pk = match read_x25519_public_key(&enc_path) {
Ok(k) => k,
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
};
let mut ephemeral_seed = [0u8; X25519_PRIVATE_KEY_LEN];
use rand_core::RngCore;
rand_core::OsRng.fill_bytes(&mut ephemeral_seed);
match keleusma::wire_format::module_to_encrypted_signed_wire_bytes(
&module,
&signing_key,
&recipient_pk,
&ephemeral_seed,
) {
Ok(b) => b,
Err(e) => {
eprintln!("error: encrypting and signing bytecode: {:?}", e);
return ExitCode::FAILURE;
}
}
}
(Some(sign_path), None) => {
let signing_key = match read_signing_key(&sign_path) {
Ok(k) => k,
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
};
match keleusma::wire_format::module_to_signed_wire_bytes(&module, &signing_key) {
Ok(b) => b,
Err(e) => {
eprintln!("error: signing bytecode: {:?}", e);
return ExitCode::FAILURE;
}
}
}
(None, _) => {
match module.to_bytes() {
Ok(b) => b,
Err(e) => {
eprintln!("error: serializing bytecode: {:?}", e);
return ExitCode::FAILURE;
}
}
}
};
if let Err(e) = fs::write(&output_path, &bytes) {
eprintln!("error: writing {}: {}", output_path, e);
return ExitCode::FAILURE;
}
eprintln!("wrote {} ({} bytes)", output_path, bytes.len());
ExitCode::SUCCESS
}
fn keygen_subcommand(args: &[String]) -> ExitCode {
let mut seed_path: Option<String> = None;
let mut pub_path: Option<String> = None;
let mut kind = KeyKind::Signing;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--seed" => {
if i + 1 >= args.len() {
eprintln!("error: --seed requires a path");
return ExitCode::FAILURE;
}
seed_path = Some(args[i + 1].clone());
i += 2;
}
"--public" | "--public-key" | "--verifying-key" => {
if i + 1 >= args.len() {
eprintln!("error: --public requires a path");
return ExitCode::FAILURE;
}
pub_path = Some(args[i + 1].clone());
i += 2;
}
"--kind" => {
if i + 1 >= args.len() {
eprintln!("error: --kind requires either 'signing' or 'encryption'");
return ExitCode::FAILURE;
}
kind = match args[i + 1].as_str() {
"signing" => KeyKind::Signing,
"encryption" => KeyKind::Encryption,
other => {
eprintln!(
"error: --kind must be 'signing' or 'encryption'; got '{}'",
other
);
return ExitCode::FAILURE;
}
};
i += 2;
}
other => {
eprintln!("error: unknown option `{}`", other);
return ExitCode::FAILURE;
}
}
}
let seed_path = match seed_path {
Some(p) => p,
None => {
eprintln!("error: keygen requires --seed <path>");
return ExitCode::FAILURE;
}
};
let pub_path = match pub_path {
Some(p) => p,
None => {
eprintln!("error: keygen requires --public <path>");
return ExitCode::FAILURE;
}
};
if Path::new(&seed_path).exists() {
eprintln!(
"error: refusing to overwrite existing seed file {}; remove or rename first",
seed_path
);
return ExitCode::FAILURE;
}
if Path::new(&pub_path).exists() {
eprintln!(
"error: refusing to overwrite existing public-key file {}; remove or rename first",
pub_path
);
return ExitCode::FAILURE;
}
let (seed_bytes, public_bytes, kind_label) = match kind {
KeyKind::Signing => {
let signing_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng);
let verifying_key = signing_key.verifying_key();
(signing_key.to_bytes(), verifying_key.to_bytes(), "Ed25519")
}
KeyKind::Encryption => {
use rand_core::RngCore;
let mut seed = [0u8; X25519_PRIVATE_KEY_LEN];
rand_core::OsRng.fill_bytes(&mut seed);
let public = keleusma::encryption::public_key_from_private(&seed);
(seed, public, "X25519")
}
};
if let Err(e) = fs::write(&seed_path, seed_bytes) {
eprintln!("error: writing seed file {}: {}", seed_path, e);
return ExitCode::FAILURE;
}
if let Err(e) = fs::write(&pub_path, public_bytes) {
eprintln!("error: writing public-key file {}: {}", pub_path, e);
return ExitCode::FAILURE;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = fs::set_permissions(&seed_path, std::fs::Permissions::from_mode(0o600)) {
eprintln!(
"warning: could not tighten permissions on {}: {}",
seed_path, e
);
}
}
eprintln!(
"wrote {} seed to {} (32 bytes; keep secret)",
kind_label, seed_path
);
let public_role = match kind {
KeyKind::Signing => "distribute to verifiers",
KeyKind::Encryption => "distribute to compilers producing artefacts for this host",
};
eprintln!(
"wrote {} public key to {} (32 bytes; {})",
kind_label, pub_path, public_role
);
ExitCode::SUCCESS
}
#[derive(Debug, Clone, Copy)]
enum KeyKind {
Signing,
Encryption,
}
fn read_signing_key(path: &str) -> Result<ed25519_dalek::SigningKey, String> {
let bytes = fs::read(path).map_err(|e| format!("reading signing key {}: {}", path, e))?;
if bytes.len() != 32 {
return Err(format!(
"signing key file {} must be exactly 32 bytes (raw Ed25519 seed); got {} bytes",
path,
bytes.len()
));
}
let mut seed = [0u8; 32];
seed.copy_from_slice(&bytes);
Ok(ed25519_dalek::SigningKey::from_bytes(&seed))
}
fn read_x25519_private_key(path: &str) -> Result<[u8; X25519_PRIVATE_KEY_LEN], String> {
let bytes = fs::read(path).map_err(|e| format!("reading decryption key {}: {}", path, e))?;
if bytes.len() != X25519_PRIVATE_KEY_LEN {
return Err(format!(
"decryption key file {} must be exactly {} bytes (raw X25519 seed); got {} bytes",
path,
X25519_PRIVATE_KEY_LEN,
bytes.len()
));
}
let mut seed = [0u8; X25519_PRIVATE_KEY_LEN];
seed.copy_from_slice(&bytes);
Ok(seed)
}
fn read_x25519_public_key(path: &str) -> Result<[u8; X25519_PRIVATE_KEY_LEN], String> {
let bytes = fs::read(path).map_err(|e| format!("reading encryption key {}: {}", path, e))?;
if bytes.len() != X25519_PRIVATE_KEY_LEN {
return Err(format!(
"encryption key file {} must be exactly {} bytes (raw X25519 public key); got {} bytes",
path,
X25519_PRIVATE_KEY_LEN,
bytes.len()
));
}
let mut key = [0u8; X25519_PRIVATE_KEY_LEN];
key.copy_from_slice(&bytes);
Ok(key)
}
fn read_verifying_key(path: &str) -> Result<ed25519_dalek::VerifyingKey, String> {
let bytes = fs::read(path).map_err(|e| format!("reading verifying key {}: {}", path, e))?;
if bytes.len() != 32 {
return Err(format!(
"verifying key file {} must be exactly 32 bytes (raw Ed25519 public key); got {} bytes",
path,
bytes.len()
));
}
let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(&bytes);
ed25519_dalek::VerifyingKey::from_bytes(&key_bytes).map_err(|e| {
format!(
"verifying key {} is not a valid Ed25519 public key: {}",
path, e
)
})
}
fn default_output_path(input: &str) -> String {
let path = Path::new(input);
if path.extension().and_then(|s| s.to_str()) == Some("kel") {
format!("{}.bin", input)
} else {
format!("{}.kel.bin", input)
}
}
fn repl_subcommand(_args: &[String]) -> ExitCode {
println!("{}", REPL_BANNER);
let stdin = io::stdin();
let stdout = io::stdout();
let mut prefix = String::new();
let mut input = String::new();
let mut shared_state: Vec<u8> = Vec::new();
loop {
{
let mut out = stdout.lock();
let _ = out.write_all(b"> ");
let _ = out.flush();
}
input.clear();
let bytes_read = match stdin.lock().read_line(&mut input) {
Ok(n) => n,
Err(e) => {
eprintln!("error: reading input: {}", e);
return ExitCode::FAILURE;
}
};
if bytes_read == 0 {
println!();
return ExitCode::SUCCESS;
}
let line = input.trim();
if line.is_empty() {
continue;
}
if let Some(stripped) = line.strip_prefix(':') {
let mut parts = stripped.splitn(2, char::is_whitespace);
let cmd = parts.next().unwrap_or("");
let arg = parts.next().map(str::trim).unwrap_or("");
match cmd {
"quit" | "q" | "exit" => return ExitCode::SUCCESS,
"help" | "h" => print_repl_help(),
"reset" => {
prefix.clear();
shared_state.clear();
println!("session prefix and shared data cleared");
}
"show" => {
if prefix.is_empty() {
println!("(empty session prefix)");
} else {
println!("{}", prefix);
}
}
"save" => repl_save(&prefix, arg),
"load" => repl_load(&mut prefix, &mut shared_state, arg),
"run" => repl_run(&prefix),
other => {
eprintln!("error: unknown REPL command `:{}`", other);
}
}
continue;
}
evaluate_repl_input(&mut prefix, &mut shared_state, line);
}
}
fn print_repl_help() {
println!("Keleusma REPL commands:");
println!(" :help, :h Show this help");
println!(" :quit, :q, :exit Exit the REPL");
println!(" :reset Clear the session prefix");
println!(" :show Display the current session prefix");
println!(" :save <file> Write the session program to a .kel file");
println!(" :load <file> Replace the session with a .kel file's contents");
println!(" :run Run the session program; step a loop/yield main");
println!();
println!("Otherwise, type:");
println!(" An expression to evaluate it (`1 + 2`, `double(21)`)");
println!(
" A declaration to add to the session prefix (`fn`, `struct`, `enum`, `trait`, `impl`, `use`)"
);
}
fn repl_save(prefix: &str, path: &str) {
if path.is_empty() {
eprintln!("error: :save requires a filename, e.g. `:save program.kel`");
return;
}
let mut contents = prefix.trim_end().to_string();
contents.push('\n');
match std::fs::write(path, &contents) {
Ok(()) => println!(
"saved session to {} ({} line(s))",
path,
prefix.lines().count()
),
Err(e) => eprintln!("error: writing {}: {}", path, e),
}
}
fn repl_load(prefix: &mut String, shared_state: &mut Vec<u8>, path: &str) {
if path.is_empty() {
eprintln!("error: :load requires a filename, e.g. `:load program.kel`");
return;
}
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("error: reading {}: {}", path, e);
return;
}
};
*prefix = contents.trim_end().to_string();
shared_state.clear();
let probe = if has_main(prefix) {
prefix.clone()
} else {
format!("{}\n\nfn main() -> Word {{ 0 }}\n", prefix)
};
match compile_source(&probe) {
Ok(_) => println!("loaded {} ({} line(s))", path, prefix.lines().count()),
Err(e) => eprintln!("loaded {} with errors:\n{}", path, e),
}
}
fn print_shared_state(vm: &Vm, names: &[String], shared: &[u8]) {
if names.is_empty() {
return;
}
print!(" shared:");
for (i, name) in names.iter().enumerate() {
if i > 0 {
print!(",");
}
match vm.get_shared(shared, i) {
Ok(v) => {
print!(" {} = ", name);
print_value_inline(&v);
}
Err(_) => print!(" {} = <composite>", name),
}
}
println!();
}
fn repl_run(prefix: &str) {
if !has_main(prefix) {
eprintln!(
"error: :run needs a `main`; define `loop main(tick: Word) -> Word {{ ... yield ... }}` (or `fn main`, `yield main`)"
);
return;
}
let module = match compile_source(prefix) {
Ok(m) => m,
Err(e) => {
eprintln!("error: {}", e);
return;
}
};
let entry_kind = match detect_entry_kind(&module) {
Ok(k) => k,
Err(e) => {
eprintln!("error: {}", e);
return;
}
};
let shared_names: Vec<String> = module
.data_layout
.as_ref()
.map(|dl| {
dl.slots
.iter()
.filter(|s| matches!(s.visibility, keleusma::bytecode::SlotVisibility::Shared))
.map(|s| s.name.clone())
.collect()
})
.unwrap_or_default();
let persistent_bytes = keleusma::vm::required_persistent_capacity_for(&module);
let transient_bytes =
keleusma::vm::auto_arena_capacity_for(&module, &[]).unwrap_or(DEFAULT_ARENA_CAPACITY);
let total = (persistent_bytes + transient_bytes).max(DEFAULT_ARENA_CAPACITY);
let mut arena = Arena::with_capacity(total);
if let Err(e) = arena.resize_persistent(persistent_bytes) {
eprintln!("error: arena: resize_persistent: {:?}", e);
return;
}
let mut vm = match Vm::new(module, &arena) {
Ok(v) => v,
Err(e) => {
eprintln!("error: verify: {:?}", e);
return;
}
};
register_repl_natives(&mut vm);
let mut shared = vec![0u8; vm.shared_data_bytes()];
if matches!(entry_kind, EntryKind::AtomicFn) {
match vm.call_with_shared(&mut shared, &[]) {
Ok(VmState::Finished(v)) => {
print!("=> ");
print_value_inline_ctx(&v, &arena);
println!();
}
Ok(other) => eprintln!("error: fn main did not finish cleanly: {:?}", other),
Err(e) => eprintln!("error: vm: {:?}", e),
}
return;
}
println!(
"stepping {}; :resume [tick] to advance, :stop to exit",
match entry_kind {
EntryKind::LoopMain => "loop main",
EntryKind::YieldMain => "yield main",
EntryKind::AtomicFn => "fn main",
}
);
let stdin = io::stdin();
let mut tick: i64 = 1;
let mut state = match vm.call_with_shared(&mut shared, &[Value::Int(tick)]) {
Ok(s) => s,
Err(e) => {
eprintln!("error: vm: {:?}", e);
return;
}
};
loop {
while matches!(state, VmState::Reset) {
state = match vm.resume_with_shared(&mut shared, Value::Int(tick)) {
Ok(s) => s,
Err(e) => {
eprintln!("error: vm: {:?}", e);
return;
}
};
}
match &state {
VmState::Yielded(v) => {
print!("yield => ");
print_value_inline_ctx(v, &arena);
println!();
print_shared_state(&vm, &shared_names, &shared);
tick = if let Value::Int(n) = v {
n.wrapping_add(1)
} else {
tick.wrapping_add(1)
};
}
VmState::Finished(v) => {
print!("finished => ");
print_value_inline_ctx(v, &arena);
println!();
print_shared_state(&vm, &shared_names, &shared);
return;
}
VmState::BreakpointHit { chunk, op } => {
eprintln!("error: unexpected breakpoint at chunk {} op {}", chunk, op);
return;
}
VmState::Reset => unreachable!("reset states are drained above"),
}
let next = loop {
print!("loop> ");
let _ = io::stdout().flush();
let mut line = String::new();
match stdin.lock().read_line(&mut line) {
Ok(0) => {
println!();
return;
}
Ok(_) => {}
Err(e) => {
eprintln!("error: reading input: {}", e);
return;
}
}
let cmd = line.trim();
let body = cmd.strip_prefix(':').unwrap_or(cmd);
let mut it = body.splitn(2, char::is_whitespace);
let verb = it.next().unwrap_or("");
let arg = it.next().map(str::trim).unwrap_or("");
match verb {
"" | "resume" | "r" | "step" | "s" => {
if !arg.is_empty() {
match arg.parse::<i64>() {
Ok(n) => tick = n,
Err(_) => {
eprintln!("error: :resume expects an integer tick; got `{}`", arg);
continue;
}
}
}
match vm.resume_with_shared(&mut shared, Value::Int(tick)) {
Ok(s) => break s,
Err(e) => {
eprintln!("error: vm: {:?}", e);
return;
}
}
}
"stop" | "quit" | "q" | "exit" => {
println!("stopped stepping");
return;
}
"shared" => print_shared_state(&vm, &shared_names, &shared),
"help" | "h" => {
println!("stepping commands:");
println!(
" :resume [tick] advance to the next yield (default = last yield + 1)"
);
println!(" :shared re-print the shared-data state");
println!(" :stop exit stepping, back to the REPL");
}
other => eprintln!(
"unknown stepping command `:{}`; :resume to step, :stop to exit",
other
),
}
};
state = next;
}
}
fn is_declaration(line: &str) -> bool {
let starters = [
"fn ",
"yield ",
"loop ",
"struct ",
"enum ",
"trait ",
"impl ",
"use ",
"data ",
"shared data ",
"private data ",
"const data ",
"signed fn ",
"signed yield ",
"signed loop ",
"ephemeral fn ",
"ephemeral yield ",
"ephemeral loop ",
"newtype ",
];
starters.iter().any(|s| line.starts_with(s))
}
fn evaluate_repl_input(prefix: &mut String, shared_state: &mut Vec<u8>, line: &str) {
if is_declaration(line) {
let candidate = format!("{}\n{}", prefix.trim_end(), line);
let candidate = candidate.trim().to_string();
let probe = if has_main(&candidate) {
candidate.clone()
} else {
format!("{}\n\nfn main() -> Word {{ 0 }}\n", candidate)
};
match compile_source(&probe) {
Ok(_) => {
*prefix = candidate;
if let Some(name) = extract_decl_name(line) {
println!("defined: {}", name);
} else {
println!("declaration accepted");
}
}
Err(e) => {
eprintln!("error: {}", e);
}
}
return;
}
let expr_form = format!(
"use println\n{}\n\nfn main() -> Word {{\n let _ = println({});\n 0\n}}\n",
prefix.trim_end(),
line
);
match execute_source_repl_silent(&expr_form, shared_state) {
Ok(()) => {}
Err(expr_err) => {
let stmt_form = format!(
"{}\n\nfn main() -> Word {{\n {};\n 0\n}}\n",
prefix.trim_end(),
line
);
match execute_source_repl_silent(&stmt_form, shared_state) {
Ok(()) => {}
Err(_) => {
eprintln!("error: {}", expr_err);
}
}
}
}
}
fn has_main(source: &str) -> bool {
source.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("fn main")
|| trimmed.starts_with("yield main")
|| trimmed.starts_with("loop main")
})
}
fn extract_decl_name(line: &str) -> Option<String> {
let mut tokens = line.split_whitespace();
let kw = tokens.next()?;
let name = match kw {
"fn" | "yield" | "loop" | "struct" | "enum" | "trait" | "data" => {
let next = tokens.next()?;
let end = next.find(['(', '<', '{', ':']).unwrap_or(next.len());
next[..end].to_string()
}
"impl" => {
let rest = tokens.collect::<Vec<&str>>().join(" ");
let head = rest.split('{').next().unwrap_or(&rest).trim().to_string();
format!("impl {}", head)
}
"use" => {
let next = tokens.next()?;
format!("use {}", next.trim_end_matches([';', ','].as_ref()))
}
_ => return None,
};
Some(name)
}
const CLI_NATIVE_SIGNATURES: &str = concat!(
"use shell::set_tick_interval(Text) -> ()\n",
"use shell::tick_interval() -> Text\n",
);
fn build_preamble() -> String {
let mut s = String::new();
s.push_str(stddsl::Math::SIGNATURES);
s.push_str(stddsl::Audio::SIGNATURES);
s.push_str(stddsl::Shell::SIGNATURES);
s.push_str(CLI_NATIVE_SIGNATURES);
s
}
fn preamble_line_count() -> u32 {
build_preamble().bytes().filter(|b| *b == b'\n').count() as u32
}
fn strip_module_bytes(bytes: &[u8]) -> Result<Vec<u8>, String> {
let mut module = keleusma::bytecode::Module::from_bytes(bytes)
.map_err(|e| format!("reading bytecode: {:?}", e))?;
for chunk in &mut module.chunks {
chunk.debug_pool = None;
}
module
.to_bytes()
.map_err(|e| format!("encoding stripped bytecode: {:?}", e))
}
fn strip_subcommand(args: &[String]) -> ExitCode {
if args.is_empty() {
eprintln!("error: `strip` requires a bytecode path");
return ExitCode::FAILURE;
}
let input = &args[0];
let mut output: Option<String> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-o" | "--output" => {
if i + 1 >= args.len() {
eprintln!("error: --output requires a path");
return ExitCode::FAILURE;
}
output = Some(args[i + 1].clone());
i += 2;
}
other => {
eprintln!("error: unknown option `{}`", other);
return ExitCode::FAILURE;
}
}
}
let bytes = match fs::read(input) {
Ok(b) => b,
Err(e) => {
eprintln!("error: reading {}: {}", input, e);
return ExitCode::FAILURE;
}
};
if keleusma::wire_format::header_requires_signature(&bytes)
|| keleusma::wire_format::header_requires_encryption(&bytes)
{
eprintln!(
"error: cannot strip a signed or encrypted artefact; stripping rewrites the body and invalidates the signature. Strip before signing: compile, then strip, then sign."
);
return ExitCode::FAILURE;
}
let stripped = match strip_module_bytes(&bytes) {
Ok(b) => b,
Err(e) => {
eprintln!("error: {}", e);
return ExitCode::FAILURE;
}
};
let out_path = output.unwrap_or_else(|| input.clone());
match fs::write(&out_path, &stripped) {
Ok(()) => {
eprintln!(
"stripped {} -> {} ({} bytes)",
input,
out_path,
stripped.len()
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: writing {}: {}", out_path, e);
ExitCode::FAILURE
}
}
}
fn compile_source(source: &str) -> Result<keleusma::bytecode::Module, String> {
compile_source_with_target(source, None, false)
}
fn compile_source_with_target(
source: &str,
target: Option<&keleusma::target::Target>,
emit_debug: bool,
) -> Result<keleusma::bytecode::Module, String> {
let mut combined = build_preamble();
if let Some(rest) = source.strip_prefix("#!") {
match rest.find('\n') {
Some(nl) => {
combined.push('\n');
combined.push_str(&rest[nl + 1..]);
}
None => {
}
}
} else {
combined.push_str(source);
}
let preamble_lines = preamble_line_count();
let tokens = tokenize(&combined)
.map_err(|e| format_err_with_offset("lex", &e.message, e.span, preamble_lines))?;
let program = parse(&tokens)
.map_err(|e| format_err_with_offset("parse", &e.message, e.span, preamble_lines))?;
let host_target;
let resolved_target = match target {
Some(t) => t,
None => {
host_target = keleusma::target::Target::host();
&host_target
}
};
let options = CompileOptions { emit_debug };
let (module, _warnings) = compile_with_options(&program, resolved_target, &options)
.map_err(|e| format_err_with_offset("compile", &e.message, e.span, preamble_lines))?;
Ok(module)
}
fn parse_target_name(name: &str) -> Result<keleusma::target::Target, String> {
match name {
"host" => Ok(keleusma::target::Target::host()),
"wasm32" => Ok(keleusma::target::Target::wasm32()),
"embedded_32" => Ok(keleusma::target::Target::embedded_32()),
"embedded_16" => Ok(keleusma::target::Target::embedded_16()),
"embedded_8" => Ok(keleusma::target::Target::embedded_8()),
other => Err(format!(
"unknown target `{}`; expected one of: host, wasm32, embedded_32, embedded_16, embedded_8",
other
)),
}
}
fn module_arena_sizing(module: &keleusma::bytecode::Module) -> (usize, usize, usize) {
let persistent = keleusma::vm::required_persistent_capacity_for(module);
let transient =
keleusma::vm::auto_arena_capacity_for(module, &[]).unwrap_or(DEFAULT_ARENA_CAPACITY);
let total = (persistent + transient).max(DEFAULT_ARENA_CAPACITY);
(persistent, transient, total)
}
fn allocate_module_arena(module: &keleusma::bytecode::Module) -> Result<Arena, String> {
let (persistent, _transient, total) = module_arena_sizing(module);
let mut arena = Arena::try_with_capacity(total).map_err(|_| {
format!("out of memory: this program needs a {total}-byte arena, which this host cannot allocate")
})?;
arena
.resize_persistent(persistent)
.map_err(|e| format!("arena: resize_persistent: {:?}", e))?;
Ok(arena)
}
fn print_module_memory(module: &keleusma::bytecode::Module) {
let (persistent, transient, total) = module_arena_sizing(module);
println!("arena: {total} bytes total (persistent {persistent}, transient {transient})");
}
fn execute_source(
source: &str,
loop_config: &LoopRunnerConfig,
print_memory: bool,
) -> Result<(), String> {
let module = compile_source(source)?;
if print_memory {
print_module_memory(&module);
return Ok(());
}
let entry_kind = detect_entry_kind(&module)?;
let arena = allocate_module_arena(&module)?;
let mut vm = Vm::new(module, &arena).map_err(|e| format!("verify: {:?}", e))?;
drive_to_completion(&mut vm, &arena, entry_kind, loop_config)
}
fn execute_source_repl_silent(source: &str, shared_state: &mut Vec<u8>) -> Result<(), String> {
let module = compile_source(source)?;
let entry_kind = detect_entry_kind(&module)?;
if !matches!(entry_kind, EntryKind::AtomicFn) {
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).map_err(|e| format!("verify: {:?}", e))?;
return drive_to_completion(&mut vm, &arena, entry_kind, &LoopRunnerConfig::default());
}
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).map_err(|e| format!("verify: {:?}", e))?;
register_repl_natives(&mut vm);
shared_state.resize(vm.shared_data_bytes(), 0);
let result = vm
.call_with_shared(shared_state.as_mut_slice(), &[])
.map_err(|e| format!("vm: {:?}", e))?;
match result {
VmState::Finished(_) => Ok(()),
VmState::Yielded(v) => Err(format!("REPL wrapper yielded unexpectedly: {:?}", v)),
VmState::Reset => Err(String::from("REPL wrapper reset unexpectedly")),
VmState::BreakpointHit { chunk, op } => Err(format!(
"REPL wrapper hit a breakpoint at chunk {} op {} unexpectedly",
chunk, op
)),
}
}
fn register_repl_natives<'a, 'arena>(vm: &mut Vm<'a, 'arena>) {
vm.register_native_with_ctx_closure("println", |ctx, args| {
if let Some(arg) = args.first() {
print_value_inline_ctx(arg, ctx.arena);
}
println!();
Ok(Value::Unit)
});
vm.register_library(stddsl::Math);
vm.register_library(stddsl::Audio);
vm.register_library(stddsl::Shell);
let stub_interval: Arc<AtomicU64> = Arc::new(AtomicU64::new(0));
let s1 = stub_interval.clone();
vm.register_native_closure("shell::set_tick_interval", move |args| {
let arg = args.first().ok_or_else(|| {
keleusma::vm::VmError::NativeError(String::from(
"shell::set_tick_interval: expected one Text argument",
))
})?;
let s: &str = match arg {
Value::StaticStr(s) => s.as_str(),
other => {
return Err(keleusma::vm::VmError::TypeError(format!(
"shell::set_tick_interval: expected Text, got {:?}",
other
)));
}
};
let dur = duration::parse(s).map_err(keleusma::vm::VmError::NativeError)?;
s1.store(dur.as_nanos() as u64, Ordering::Relaxed);
Ok(Value::Unit)
});
let s2 = stub_interval;
vm.register_native_closure("shell::tick_interval", move |_args| {
let nanos = s2.load(Ordering::Relaxed);
let dur = Duration::from_nanos(nanos);
Ok(Value::StaticStr(duration::format(dur)))
});
}
fn execute_bytecode(
bytes: &[u8],
verifying_keys: &[ed25519_dalek::VerifyingKey],
decryption_keys: &[[u8; X25519_PRIVATE_KEY_LEN]],
policy: &PolicyContext,
loop_config: &LoopRunnerConfig,
print_memory: bool,
) -> Result<(), String> {
let signed = keleusma::wire_format::header_requires_signature(bytes);
let encrypted = keleusma::wire_format::header_requires_encryption(bytes);
if policy.strict_signing && !signed {
return Err(String::from("strict mode: unsigned bytecode disabled"));
}
if policy.strict_encryption && !encrypted {
return Err(String::from("strict mode: unencrypted bytecode disabled"));
}
let module = load_module(bytes, verifying_keys, decryption_keys, policy)?;
if print_memory {
print_module_memory(&module);
return Ok(());
}
let arena = allocate_module_arena(&module)?;
let entry_kind = detect_entry_kind(&module)?;
let mut vm = Vm::new(module, &arena).map_err(|e| format!("verify: {:?}", e))?;
for key in verifying_keys {
vm.register_verifying_key(*key);
}
drive_to_completion(&mut vm, &arena, entry_kind, loop_config)
}
#[derive(Debug, Clone, Default)]
struct LoopRunnerConfig {
tick_interval_nanos: Arc<AtomicU64>,
quiet: bool,
}
#[derive(Debug, Clone, Copy)]
enum EntryKind {
AtomicFn,
YieldMain,
LoopMain,
}
fn detect_entry_kind(module: &keleusma::bytecode::Module) -> Result<EntryKind, String> {
let entry_idx = module
.entry_point
.ok_or_else(|| String::from("module has no entry point; cannot determine entry kind"))?;
let entry = module
.chunks
.get(entry_idx)
.ok_or_else(|| format!("entry_point {} out of bounds", entry_idx))?;
use keleusma::bytecode::BlockType;
match entry.block_type {
BlockType::Func => {
if entry.param_count != 0 {
return Err(format!(
"fn main: CLI runner expects zero parameters; got {}",
entry.param_count
));
}
Ok(EntryKind::AtomicFn)
}
BlockType::Stream => {
if entry.param_count != 1 {
return Err(format!(
"loop main: CLI runner expects exactly one parameter (tick: Word); got {}",
entry.param_count
));
}
Ok(EntryKind::LoopMain)
}
BlockType::Reentrant => {
if entry.param_count != 1 {
return Err(format!(
"yield main: CLI runner expects exactly one parameter (tick: Word); got {}",
entry.param_count
));
}
Ok(EntryKind::YieldMain)
}
}
}
pub(crate) fn load_module(
bytes: &[u8],
verifying_keys: &[ed25519_dalek::VerifyingKey],
decryption_keys: &[[u8; X25519_PRIVATE_KEY_LEN]],
policy: &PolicyContext,
) -> Result<keleusma::bytecode::Module, String> {
let signed = keleusma::wire_format::header_requires_signature(bytes);
let encrypted = keleusma::wire_format::header_requires_encryption(bytes);
if encrypted {
if decryption_keys.is_empty() {
return Err(String::from(
"encrypted bytecode requires --decryption-key or an enrolled decryption-key store",
));
}
let mut last_err: Option<keleusma::bytecode::LoadError> = None;
let mut signature_rejected = false;
for key in decryption_keys {
match keleusma::wire_format::decrypt_encrypted_signed_to_signed_bytes(
bytes,
verifying_keys,
key,
) {
Ok(plaintext) => {
let mut module = keleusma::bytecode::Module::from_bytes(&plaintext)
.map_err(|e| format!("decoded module: {:?}", e))?;
module.flags &= !keleusma::wire_format::FLAG_REQUIRES_SIGNATURE;
return Ok(module);
}
Err(e) => {
if matches!(e, keleusma::bytecode::LoadError::InvalidSignature) {
signature_rejected = true;
}
last_err = Some(e);
}
}
}
let err = last_err.expect("at least one key attempted");
Err(if policy.strict_encryption {
if signature_rejected {
String::from(
"strict mode: artefact decrypted but its signature is not from an enrolled key (InvalidSignature)",
)
} else {
format!(
"strict mode: no enrolled decryption key matches the artefact ({:?})",
err
)
}
} else {
format!("decrypt_encrypted_signed_to_signed_bytes: {:?}", err)
})
} else if signed {
keleusma::wire_format::verify_module_signature(bytes, verifying_keys).map_err(|e| {
if policy.strict_signing {
format!(
"strict mode: signature does not match any enrolled key ({:?})",
e
)
} else {
format!("verify_module_signature: {:?}", e)
}
})?;
let mut module = keleusma::bytecode::Module::from_bytes(bytes)
.map_err(|e| format!("module: {:?}", e))?;
module.flags &= !keleusma::wire_format::FLAG_REQUIRES_SIGNATURE;
Ok(module)
} else {
if !verifying_keys.is_empty() {
return Err(String::from(
"--verifying-key supplied but the bytecode does not carry FLAG_REQUIRES_SIGNATURE",
));
}
if !decryption_keys.is_empty() {
return Err(String::from(
"--decryption-key supplied but the bytecode does not carry FLAG_ENCRYPTED",
));
}
keleusma::bytecode::Module::from_bytes(bytes).map_err(|e| format!("module: {:?}", e))
}
}
fn drive_to_completion(
vm: &mut Vm,
arena: &Arena,
entry_kind: EntryKind,
config: &LoopRunnerConfig,
) -> Result<(), String> {
keleusma::utility_natives::register_utility_natives(vm);
vm.register_native_with_ctx_closure("println", |ctx, args| {
if let Some(arg) = args.first() {
print_value_inline_ctx(arg, ctx.arena);
}
println!();
Ok(Value::Unit)
});
vm.register_library(stddsl::Math);
vm.register_library(stddsl::Audio);
vm.register_library(stddsl::Shell);
let interval_for_set = config.tick_interval_nanos.clone();
vm.register_native_closure("shell::set_tick_interval", move |args| {
let arg = args.first().ok_or_else(|| {
keleusma::vm::VmError::NativeError(String::from(
"shell::set_tick_interval: expected one Text argument",
))
})?;
let s: &str = match arg {
Value::StaticStr(s) => s.as_str(),
other => {
return Err(keleusma::vm::VmError::TypeError(format!(
"shell::set_tick_interval: expected Text, got {:?}",
other
)));
}
};
let dur = duration::parse(s).map_err(keleusma::vm::VmError::NativeError)?;
interval_for_set.store(dur.as_nanos() as u64, Ordering::Relaxed);
Ok(Value::Unit)
});
let interval_for_get = config.tick_interval_nanos.clone();
vm.register_native_closure("shell::tick_interval", move |_args| {
let nanos = interval_for_get.load(Ordering::Relaxed);
let dur = Duration::from_nanos(nanos);
Ok(Value::StaticStr(duration::format(dur)))
});
match entry_kind {
EntryKind::AtomicFn => drive_atomic_fn(vm, arena),
EntryKind::YieldMain => drive_yield_main(vm, arena, config),
EntryKind::LoopMain => drive_loop_main(vm, arena, config),
}
}
fn drive_atomic_fn(vm: &mut Vm, arena: &Arena) -> Result<(), String> {
let mut shared = vec![0u8; vm.shared_data_bytes()];
match vm
.call_with_shared(&mut shared, &[])
.map_err(|e| format!("vm: {:?}", e))?
{
VmState::Finished(v) => {
print_value(&v, arena);
Ok(())
}
VmState::Yielded(v) => Err(format!(
"fn main yielded unexpectedly (atomic fn should run to completion): {:?}",
v
)),
VmState::Reset => Err(String::from(
"fn main reset unexpectedly (atomic fn should run to completion)",
)),
VmState::BreakpointHit { chunk, op } => Err(format!(
"fn main hit a breakpoint at chunk {} op {}; the CLI does not arm breakpoints",
chunk, op
)),
}
}
fn drive_loop_main(vm: &mut Vm, _arena: &Arena, config: &LoopRunnerConfig) -> Result<(), String> {
let mut tick: i64 = 1;
let mut iteration_start = Instant::now();
let mut shared = vec![0u8; vm.shared_data_bytes()];
let mut state = vm
.call_with_shared(&mut shared, &[Value::Int(tick)])
.map_err(|e| format!("vm: {:?}", e))?;
loop {
match state {
VmState::Finished(v) => {
return Err(format!(
"loop main finished unexpectedly (productive divergent loops should never return): {:?}",
v
));
}
VmState::Yielded(v) => {
let elapsed = iteration_start.elapsed();
let yielded = match v {
Value::Int(n) => n,
other => {
return Err(format!(
"loop main yielded a non-Word value (signature requires Word): {:?}",
other
));
}
};
tick = yielded.wrapping_add(1);
apply_tick_interval(elapsed, config);
iteration_start = Instant::now();
state = vm
.resume_with_shared(&mut shared, Value::Int(tick))
.map_err(|e| format!("vm: {:?}", e))?;
}
VmState::BreakpointHit { chunk, op } => {
return Err(format!(
"loop main hit a breakpoint at chunk {} op {}; the CLI does not arm breakpoints",
chunk, op
));
}
VmState::Reset => {
let elapsed = iteration_start.elapsed();
apply_tick_interval(elapsed, config);
iteration_start = Instant::now();
state = vm
.resume_with_shared(&mut shared, Value::Int(tick))
.map_err(|e| format!("vm: {:?}", e))?;
}
}
}
}
fn drive_yield_main(vm: &mut Vm, _arena: &Arena, config: &LoopRunnerConfig) -> Result<(), String> {
let mut tick: i64 = 1;
let mut iteration_start = Instant::now();
let mut shared = vec![0u8; vm.shared_data_bytes()];
let mut state = vm
.call_with_shared(&mut shared, &[Value::Int(tick)])
.map_err(|e| format!("vm: {:?}", e))?;
loop {
match state {
VmState::Finished(v) => {
match v {
Value::Unit => {}
other => println!("{:?}", other),
}
return Ok(());
}
VmState::Yielded(v) => {
let elapsed = iteration_start.elapsed();
let yielded = match v {
Value::Int(n) => n,
other => {
return Err(format!(
"yield main yielded a non-Word value (signature requires Word): {:?}",
other
));
}
};
tick = yielded.wrapping_add(1);
apply_tick_interval(elapsed, config);
iteration_start = Instant::now();
state = vm
.resume_with_shared(&mut shared, Value::Int(tick))
.map_err(|e| format!("vm: {:?}", e))?;
}
VmState::BreakpointHit { chunk, op } => {
return Err(format!(
"yield main hit a breakpoint at chunk {} op {}; the CLI does not arm breakpoints",
chunk, op
));
}
VmState::Reset => {
let elapsed = iteration_start.elapsed();
apply_tick_interval(elapsed, config);
iteration_start = Instant::now();
state = vm
.resume_with_shared(&mut shared, Value::Int(tick))
.map_err(|e| format!("vm: {:?}", e))?;
}
}
}
}
fn apply_tick_interval(elapsed: Duration, config: &LoopRunnerConfig) {
let nanos = config.tick_interval_nanos.load(Ordering::Relaxed);
if nanos == 0 {
return;
}
let interval = Duration::from_nanos(nanos);
if elapsed >= interval {
if !config.quiet {
eprintln!(
"warning: iteration time ({}) exceeded tick interval ({}); resuming immediately without sleep",
duration::format(elapsed),
duration::format(interval),
);
}
return;
}
std::thread::sleep(interval - elapsed);
}
fn format_err_with_offset(
stage: &str,
msg: &str,
span: keleusma::token::Span,
preamble_lines: u32,
) -> String {
if span.line == 0 && span.column == 0 {
return format!("{}: {}", stage, msg);
}
if span.line <= preamble_lines {
return format!(
"{}: [preamble line {}:{}] {}",
stage, span.line, span.column, msg
);
}
let adjusted_line = span.line - preamble_lines;
format!("{}: {}:{}: {}", stage, adjusted_line, span.column, msg)
}
pub(crate) fn format_value(v: &Value) -> String {
match v {
Value::Int(n) => n.to_string(),
Value::Float(f) => f.to_string(),
Value::Bool(b) => b.to_string(),
Value::StaticStr(s) => s.clone(),
Value::Unit => "()".to_string(),
Value::None => "None".to_string(),
Value::Tuple(keleusma::bytecode::TupleBody::Boxed(items)) => {
let parts: Vec<String> = items.iter().map(format_value).collect();
format!("({})", parts.join(", "))
}
Value::Tuple(keleusma::bytecode::TupleBody::Flat(fc)) => {
format!("(<flat tuple: {} bytes>)", fc.byte_len())
}
Value::Array(keleusma::bytecode::ArrayBody::Boxed(items)) => {
let parts: Vec<String> = items.iter().map(format_value).collect();
format!("[{}]", parts.join(", "))
}
Value::Array(keleusma::bytecode::ArrayBody::Flat(fc)) => {
format!("[<flat array: {} bytes>]", fc.byte_len())
}
Value::Enum(keleusma::bytecode::EnumBody::Boxed(b)) => {
if b.type_name == "Option" && b.variant == "Some" {
if let Some(f) = b.fields.first() {
format!("Some({})", format_value(f))
} else {
"Some".to_string()
}
} else if b.fields.is_empty() {
b.variant.clone()
} else {
let parts: Vec<String> = b.fields.iter().map(format_value).collect();
format!("{}({})", b.variant, parts.join(", "))
}
}
Value::Struct(keleusma::bytecode::StructBody::Boxed(b)) => {
let parts: Vec<String> = b
.fields
.iter()
.map(|(k, v)| format!("{}: {}", k, format_value(v)))
.collect();
format!("{} {{ {} }}", b.type_name, parts.join(", "))
}
Value::Struct(keleusma::bytecode::StructBody::Flat(fc)) => {
format!("<flat struct: {} bytes>", fc.byte_len())
}
other => format!("{:?}", other),
}
}
fn print_value_inline(v: &Value) {
print!("{}", format_value(v));
}
fn print_value_inline_ctx(v: &Value, arena: &Arena) {
match v {
Value::KStr(h) => match h.get(arena) {
Ok(s) => print!("{}", s),
Err(_) => print!("<stale KStr>"),
},
other => print_value_inline(other),
}
}
fn print_value(v: &Value, arena: &Arena) {
match v {
Value::KStr(h) => match h.get(arena) {
Ok(s) => println!("{}", s),
Err(_) => println!("<stale KStr>"),
},
other => println!("{}", format_value(other)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shebang_line_is_skipped() {
let with = compile_source("#!/usr/bin/env keleusma\nfn main() -> Word { 42 }\n");
assert!(
with.is_ok(),
"shebang script failed to compile: {:?}",
with.err()
);
}
#[test]
fn no_shebang_still_compiles() {
assert!(compile_source("fn main() -> Word { 42 }\n").is_ok());
}
#[test]
fn compile_debug_then_strip_reproduces_release_bytes() {
let src = "fn helper() -> Word { 1 }\nfn main() -> Word { helper() }";
let release_bytes = compile_source_with_target(src, None, false)
.expect("compile release")
.to_bytes()
.expect("encode release");
let debug_module = compile_source_with_target(src, None, true).expect("compile debug");
assert!(
debug_module.chunks.iter().any(|c| c.debug_pool.is_some()),
"a --debug compile should attach debug metadata"
);
let debug_bytes = debug_module.to_bytes().expect("encode debug");
assert_ne!(
debug_bytes, release_bytes,
"the debug build should differ from the release build before stripping"
);
let stripped = strip_module_bytes(&debug_bytes).expect("strip");
assert_eq!(
stripped, release_bytes,
"stripped debug bytecode must be byte-identical to the release build"
);
let restripped = strip_module_bytes(&release_bytes).expect("re-strip");
assert_eq!(restripped, release_bytes);
}
#[test]
fn shebang_preserves_source_line_numbers() {
let err =
compile_source("#!/usr/bin/env keleusma\nfn main() -> Word {\n undefined_thing\n}\n")
.expect_err("undefined identifier should fail to compile");
assert!(
err.contains("3:"),
"expected an error on line 3, got: {}",
err
);
}
}