use fusevm::{ChunkBuilder, Op, Value};
use std::collections::{HashMap, HashSet};
use std::fmt;
use crate::assoc::{self, ArrayNames, Target};
use crate::expr::{self, BinOp, Expr, UnOp};
use crate::parser::{Command, Part, Script, Word};
use crate::procs::Signature;
pub mod ext {
pub const DIV: u16 = 0;
pub const MOD: u16 = 1;
pub const POW: u16 = 2;
pub const IN: u16 = 3;
pub const NI: u16 = 4;
pub const PUTS: u16 = 5;
pub const EVAL: u16 = 6;
pub const MATCH: u16 = 7;
pub const ERROR: u16 = 8;
pub const CATCH_END: u16 = 9;
pub const CORO_CREATE: u16 = 10;
pub const CORO_RESUME: u16 = 11;
pub const CORO_YIELD: u16 = 12;
pub const CORO_YIELDTO: u16 = 13;
pub const CORO_INFO: u16 = 14;
pub const FFI_CALL: u16 = 63;
pub const BOOL: u16 = 15;
pub const CANON: u16 = 47;
pub const EVAL_FRAME: u16 = 58;
pub const UPLEVEL: u16 = 59;
pub const APPLY: u16 = 60;
pub const STR_CMP: u16 = 62;
pub const LIST_BASE: u16 = 16;
pub const LIST: u16 = 16;
pub const LLENGTH: u16 = 17;
pub const LINDEX: u16 = 18;
pub const LAPPEND: u16 = 19;
pub const LRANGE: u16 = 20;
pub const LREVERSE: u16 = 21;
pub const LINSERT: u16 = 22;
pub const LREPLACE: u16 = 23;
pub const LSEARCH: u16 = 24;
pub const LSORT: u16 = 25;
pub const JOIN: u16 = 26;
pub const SPLIT: u16 = 27;
pub const CONCAT: u16 = 28;
pub const LAPPEND_VAR: u16 = 33;
pub const LAPPEND_SLOT: u16 = 34;
pub const FOREACH_INIT: u16 = 29;
pub const FOREACH_MORE: u16 = 30;
pub const FOREACH_TAKE: u16 = 31;
pub const FOREACH_ADVANCE: u16 = 32;
pub const LASSIGN: u16 = 48;
pub const LSET: u16 = 49;
pub const LPOP: u16 = 50;
pub const LEDIT: u16 = 51;
pub const LREPEAT: u16 = 52;
pub const LREMOVE: u16 = 53;
pub const LSEQ: u16 = 54;
pub const LMAP_INIT: u16 = 55;
pub const LMAP_COLLECT: u16 = 56;
pub const LMAP_RESULT: u16 = 57;
pub const BIT_AND: u16 = 40;
pub const BIT_OR: u16 = 41;
pub const BIT_XOR: u16 = 42;
pub const SHL: u16 = 43;
pub const SHR: u16 = 44;
pub const BIT_NOT: u16 = 45;
pub const UPLUS: u16 = 46;
pub const ASSOC_BASE: u16 = 64;
pub const SCALAR: u16 = ASSOC_BASE;
pub const ELEM_GET: u16 = ASSOC_BASE + 1;
pub const ELEM_SET: u16 = ASSOC_BASE + 2;
pub const ELEM_INCR: u16 = ASSOC_BASE + 3;
pub const UNSET_ELEM: u16 = ASSOC_BASE + 4;
pub const UNSET_VAR: u16 = ASSOC_BASE + 5;
pub const ARR_EXISTS: u16 = ASSOC_BASE + 6;
pub const ARR_SIZE: u16 = ASSOC_BASE + 7;
pub const ARR_NAMES: u16 = ASSOC_BASE + 8;
pub const ARR_GET: u16 = ASSOC_BASE + 9;
pub const ARR_UNSET: u16 = ASSOC_BASE + 10;
pub const ARR_SET: u16 = ASSOC_BASE + 11;
pub const DICT_CREATE: u16 = ASSOC_BASE + 12;
pub const DICT_GET: u16 = ASSOC_BASE + 13;
pub const DICT_EXISTS: u16 = ASSOC_BASE + 14;
pub const DICT_REMOVE: u16 = ASSOC_BASE + 15;
pub const DICT_MERGE: u16 = ASSOC_BASE + 16;
pub const DICT_KEYS: u16 = ASSOC_BASE + 17;
pub const DICT_VALUES: u16 = ASSOC_BASE + 18;
pub const DICT_SIZE: u16 = ASSOC_BASE + 19;
pub const DICT_SET: u16 = ASSOC_BASE + 20;
pub const DICT_PAIRS: u16 = ASSOC_BASE + 21;
pub const DICT_INCR: u16 = ASSOC_BASE + 22;
pub const STRING_BASE: u16 = 128;
pub const REGEXP_BASE: u16 = 192;
pub const INFO_BASE: u16 = 208;
}
pub mod ext_wide {
pub const CATCH: u16 = 0;
pub const DBG_LINE: u16 = 1;
pub const ERROR_AT: u16 = 2;
}
fn defers_to_run_time(msg: &str) -> bool {
msg.starts_with("wrong # args:")
|| msg.starts_with("invalid command name ")
|| msg.starts_with("unknown or ambiguous subcommand ")
|| msg.contains("is not supported yet")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileError {
pub msg: String,
pub line: usize,
}
impl fmt::Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (line {})", self.msg, self.line)
}
}
impl std::error::Error for CompileError {}
pub fn compile(script: &Script) -> Result<fusevm::Chunk, CompileError> {
lower(script, false)
}
pub fn compile_debug(script: &Script) -> Result<fusevm::Chunk, CompileError> {
lower(script, true)
}
fn lower(script: &Script, debug: bool) -> Result<fusevm::Chunk, CompileError> {
let first = Compiler::run(script, ArrayNames::new(), debug)?;
let (mut chunk, tolerant, incr_sites, procs) = if first.seen_arrays.is_empty() {
let procs = signature_table(&first);
(
first.b.build(),
first.tolerant_reads,
first.incr_sites,
procs,
)
} else {
let second = Compiler::run(script, first.seen_arrays, debug)?;
let reads = second.tolerant_reads.clone();
let incrs = second.incr_sites.clone();
let procs = signature_table(&second);
(second.b.build(), reads, incrs, procs)
};
chunk.int_overflow_deopt = true;
crate::runtime::note_tolerant_reads(&chunk, &tolerant);
crate::runtime::note_incr_sites(&chunk, &incr_sites);
crate::runtime::note_procs(&chunk, &procs);
Ok(chunk)
}
fn signature_table(c: &Compiler) -> Vec<(String, crate::runtime::ProcParams)> {
c.procs
.iter()
.map(|(name, sig)| {
let params = sig
.params
.iter()
.map(|p| (p.name.clone(), p.default.clone()))
.collect();
(name.clone(), params)
})
.collect()
}
pub(crate) struct LoopCtx {
pub(crate) depth: usize,
pub(crate) catch_depth: usize,
pub(crate) breaks: Vec<usize>,
pub(crate) continues: Vec<usize>,
}
#[derive(Default)]
pub(crate) struct Scope {
pub locals: HashMap<String, u16>,
pub globals: HashSet<String>,
pub next_slot: u16,
}
pub(crate) enum Body {
Script(Script),
Deferred(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Place {
Slot(u16),
Global(u16),
}
pub(crate) struct Compiler {
pub(crate) b: ChunkBuilder,
pub(crate) tolerant_reads: Vec<usize>,
pub(crate) incr_sites: Vec<usize>,
pub(crate) depth: usize,
pub(crate) loops: Vec<LoopCtx>,
pub(crate) line: usize,
pub(crate) command_line: usize,
pub(crate) arrays: ArrayNames,
pub(crate) seen_arrays: ArrayNames,
pub(crate) scope: Option<Scope>,
pub(crate) procs: HashMap<String, Signature>,
pub(crate) defined: HashSet<String>,
pub(crate) coros: HashSet<String>,
pub(crate) catch_depth: usize,
pub(crate) body_depth: usize,
pub(crate) top_level: bool,
pub(crate) static_ctx: bool,
pub(crate) debug: bool,
pub(crate) subst_depth: usize,
pub(crate) deferrable: bool,
}
impl Compiler {
fn run(script: &Script, arrays: ArrayNames, debug: bool) -> Result<Compiler, CompileError> {
let mut c = Compiler {
b: ChunkBuilder::new(),
tolerant_reads: Vec::new(),
incr_sites: Vec::new(),
depth: 0,
loops: Vec::new(),
line: 1,
command_line: 1,
arrays,
seen_arrays: ArrayNames::new(),
scope: None,
procs: HashMap::new(),
defined: HashSet::new(),
coros: HashSet::new(),
catch_depth: 0,
body_depth: 0,
top_level: true,
static_ctx: true,
debug,
subst_depth: 0,
deferrable: false,
};
crate::procs::prescan(&mut c.procs, script);
crate::coro::prescan(&mut c.coros, script);
c.script_value(script)?;
Ok(c)
}
pub(crate) fn emit(&mut self, op: Op, delta: i32) -> usize {
let idx = self.b.emit(op, self.line as u32);
self.depth = (self.depth as i32 + delta) as usize;
idx
}
pub(crate) fn error<T>(&self, msg: impl Into<String>) -> Result<T, CompileError> {
Err(self.err(msg))
}
fn defer(&mut self, msg: &str, args: &[Word]) -> Result<(), CompileError> {
for arg in args {
self.word(arg)?;
self.emit(Op::Pop, -1);
}
self.push_str(msg);
self.emit(Op::ExtendedWide(ext_wide::ERROR_AT, self.command_line), -1);
self.push_empty();
Ok(())
}
pub(crate) fn err(&self, msg: impl Into<String>) -> CompileError {
CompileError {
msg: msg.into(),
line: self.command_line,
}
}
pub(crate) fn deferrable_err(&mut self, msg: impl Into<String>) -> CompileError {
self.deferrable = true;
self.err(msg)
}
pub(crate) fn push_value(&mut self, v: Value) {
let idx = self.b.add_constant(v);
self.emit(Op::LoadConst(idx), 1);
}
pub(crate) fn push_empty(&mut self) {
self.push_value(Value::Str(std::sync::Arc::new(String::new())));
}
pub(crate) fn push_str(&mut self, text: &str) {
self.push_value(Value::Str(std::sync::Arc::new(text.to_string())));
}
pub(crate) fn push_text(&mut self, text: &str) {
let v = literal_value(text);
self.push_value(v);
}
fn slot_of(&mut self, name: &str) -> Option<u16> {
let scope = self.scope.as_mut()?;
if scope.globals.contains(name) {
return None;
}
if let Some(slot) = scope.locals.get(name) {
return Some(*slot);
}
let slot = scope.next_slot;
scope.next_slot += 1;
scope.locals.insert(name.to_string(), slot);
Some(slot)
}
fn grows_itself(&self, name: &str, word: &Word) -> bool {
!word.expand
&& word.parts.len() > 1
&& !self.is_array(name)
&& matches!(&word.parts[0], Part::Var(first) if first == name)
&& word.parts[1..]
.iter()
.all(|part| matches!(part, Part::Lit(_) | Part::Var(_)))
}
fn append_parts(&mut self, name: &str, parts: &[Part]) -> Result<(), CompileError> {
let id = self.append_target(name);
for part in parts {
self.part(part)?;
}
let argc = parts.len() + 2;
let Ok(argc8) = u8::try_from(argc) else {
return self.error("too many arguments for one command");
};
self.emit(Op::Extended(id, argc8), 1 - argc as i32);
Ok(())
}
pub(crate) fn declared_globals(&self) -> Option<String> {
let scope = self.scope.as_ref()?;
let mut names: Vec<&str> = scope.globals.iter().map(String::as_str).collect();
names.sort_unstable();
Some(crate::list::join(&names))
}
pub(crate) fn var_place(&mut self, name: &str) -> Place {
match self.slot_of(name) {
Some(slot) => Place::Slot(slot),
None => Place::Global(self.b.add_name(name)),
}
}
pub(crate) fn emit_get_var(&mut self, name: &str) {
match self.var_place(name) {
Place::Slot(slot) => self.emit(Op::GetSlot(slot), 1),
Place::Global(idx) => self.emit(Op::GetVar(idx), 1),
};
}
pub(crate) fn emit_set_var(&mut self, name: &str) {
match self.var_place(name) {
Place::Slot(slot) => self.emit(Op::SetSlot(slot), -1),
Place::Global(idx) => self.emit(Op::SetVar(idx), -1),
};
}
pub(crate) fn nested_value(&mut self, script: &Script) -> Result<(), CompileError> {
self.in_body(|c| c.script_value(script))
}
pub(crate) fn nested_effect(&mut self, script: &Script) -> Result<(), CompileError> {
self.in_body(|c| c.script_effect(script))
}
fn in_body(
&mut self,
emit: impl FnOnce(&mut Self) -> Result<(), CompileError>,
) -> Result<(), CompileError> {
let outer = std::mem::replace(&mut self.top_level, false);
let outer_static = std::mem::replace(&mut self.static_ctx, false);
self.body_depth += 1;
let result = emit(self);
self.body_depth -= 1;
self.top_level = outer;
self.static_ctx = outer_static;
result
}
fn subst_value(&mut self, script: &Script) -> Result<(), CompileError> {
let outer = std::mem::replace(&mut self.top_level, false);
self.subst_depth += 1;
let result = self.script_value(script);
self.subst_depth -= 1;
self.top_level = outer;
result
}
pub(crate) fn script_value(&mut self, script: &Script) -> Result<(), CompileError> {
if script.commands.is_empty() {
self.push_empty();
return Ok(());
}
for (i, cmd) in script.commands.iter().enumerate() {
if i > 0 {
self.emit(Op::Pop, -1);
}
self.command(cmd)?;
}
Ok(())
}
pub(crate) fn script_effect(&mut self, script: &Script) -> Result<(), CompileError> {
for cmd in &script.commands {
self.command(cmd)?;
self.emit(Op::Pop, -1);
}
Ok(())
}
pub(crate) fn word(&mut self, word: &Word) -> Result<(), CompileError> {
if word.expand {
return self.error("{*} argument expansion is not supported yet");
}
match word.parts.len() {
0 => self.push_empty(),
1 => self.part(&word.parts[0])?,
_ => {
let mut pending = 0usize;
for part in &word.parts {
self.part(part)?;
pending += 1;
if pending == u8::MAX as usize {
self.concat_parts(pending)?;
pending = 1;
}
}
if pending > 1 {
self.concat_parts(pending)?;
}
}
}
Ok(())
}
fn concat_parts(&mut self, count: usize) -> Result<(), CompileError> {
let Ok(argc) = u8::try_from(count) else {
return self.error("too many parts in one word");
};
self.emit(
Op::Extended(crate::cmd_string::ext::CAT, argc),
1 - count as i32,
);
Ok(())
}
fn part(&mut self, part: &Part) -> Result<(), CompileError> {
match part {
Part::Lit(text) => {
self.push_value(literal_value(text));
Ok(())
}
Part::Var(name) => {
self.scalar_get(name);
Ok(())
}
Part::Elem { name, index } => self.elem_get(name, index),
Part::Script(script) => self.subst_value(script),
}
}
pub(crate) fn literal_of<'w>(
&self,
word: &'w Word,
what: &str,
) -> Result<&'w str, CompileError> {
word.as_literal()
.ok_or_else(|| self.err(format!("{what} must be a literal in this phase")))
}
pub(crate) fn target_of(&self, word: &Word) -> Result<Target, CompileError> {
assoc::target_of(word)
.ok_or_else(|| self.err("variable name must be a literal in this phase".to_string()))
}
pub(crate) fn var_name_of(&self, word: &Word) -> Result<String, CompileError> {
match self.target_of(word)? {
Target::Scalar(name) => Ok(name),
Target::Elem { .. } => self.error("this command does not take an array element yet"),
}
}
pub const BUILTINS: &'static [&'static str] = &[
"set",
"eval",
"uplevel",
"apply",
"puts",
"expr",
"incr",
"if",
"while",
"for",
"foreach",
"switch",
"string",
"append",
"format",
"break",
"continue",
"proc",
"return",
"global",
"catch",
"error",
"array",
"dict",
"unset",
"coroutine",
"yield",
"yieldto",
"info",
];
fn command(&mut self, cmd: &Command) -> Result<(), CompileError> {
self.line = cmd.line;
if self.body_depth == 0 {
self.command_line = cmd.line;
}
if self.debug && self.subst_depth == 0 {
self.emit(Op::ExtendedWide(ext_wide::DBG_LINE, cmd.line), 0);
}
let Some(first) = cmd.words.first() else {
self.push_empty();
return Ok(());
};
let name = self.literal_of(first, "command name")?.to_string();
let args = &cmd.words[1..];
let mark = self.b.current_pos();
let depth = self.depth;
self.deferrable = false;
let outcome = self.dispatch(&name, args);
let marked = std::mem::take(&mut self.deferrable);
match outcome {
Err(e) if (defers_to_run_time(&e.msg) || marked) && self.b.current_pos() == mark => {
self.depth = depth;
self.defer(&e.msg, args)
}
Err(e) if marked => {
self.deferrable = true;
Err(e)
}
outcome => outcome,
}
}
fn dispatch(&mut self, name: &str, args: &[Word]) -> Result<(), CompileError> {
match name {
"set" => self.cmd_set(args),
"eval" => self.cmd_eval(args),
"uplevel" => self.cmd_uplevel(args),
"apply" => self.cmd_apply(args),
"puts" => self.cmd_puts(args),
"expr" => self.cmd_expr(args),
"incr" => self.cmd_incr(args),
"if" => self.cmd_if(args),
"while" => self.cmd_while(args),
"for" => self.cmd_for(args),
"foreach" => self.cmd_foreach(args),
"switch" => self.cmd_switch(args),
"string" | "append" | "format" => self.cmd_string_family(name, args),
"break" => self.cmd_loop_exit(args, true),
"continue" => self.cmd_loop_exit(args, false),
"proc" => self.cmd_proc(args),
"return" => self.cmd_return(args),
"global" => self.cmd_global(args),
"catch" => self.cmd_catch(args),
"error" => self.cmd_error(args),
"array" => self.cmd_array(args),
"dict" => self.cmd_dict(args),
"unset" => self.cmd_unset(args),
"coroutine" => self.cmd_coroutine(args),
"yield" => self.cmd_yield(args),
"yieldto" => self.cmd_yieldto(args),
"info" => self.cmd_info(args),
"regexp" | "regsub" => crate::regexp::compile(self, name, args),
name if name == crate::rust_ffi::COMPILE_COMMAND => self.cmd_rust_compile(args),
other if self.coros.contains(other) => self.call_coro(other, args),
other if self.procs.contains_key(other) => self.call_proc(other, args),
other if crate::rust_ffi::is_exported(other) => self.call_ffi(other, args),
other => crate::cmd_list::compile(self, other, args),
}
}
fn cmd_set(&mut self, args: &[Word]) -> Result<(), CompileError> {
match args.len() {
1 => match self.target_of(&args[0])? {
Target::Scalar(name) => {
self.scalar_get(&name);
Ok(())
}
Target::Elem { name, index } => self.elem_get(&name, &index),
},
2 => match self.target_of(&args[0])? {
Target::Scalar(name) => {
if self.grows_itself(&name, &args[1]) {
return self.append_parts(&name, &args[1].parts[1..]);
}
self.scalar_set_guard(&name);
self.word(&args[1])?;
self.emit(Op::Dup, 1);
self.emit_set_var(&name);
Ok(())
}
Target::Elem { name, index } => self.elem_set(&name, &index, &args[1]),
},
_ => self.error("wrong # args: should be \"set varName ?newValue?\""),
}
}
fn cmd_eval(&mut self, args: &[Word]) -> Result<(), CompileError> {
if args.is_empty() {
return self.error("wrong # args: should be \"eval arg ?arg ...?\"");
}
if let Some(declared) = self.declared_globals() {
let count = u8::try_from(args.len() + 1)
.map_err(|_| self.err("too many arguments for \"eval\"".to_string()))?;
self.push_str(&declared);
for arg in args {
self.word(arg)?;
}
self.emit(Op::Extended(ext::EVAL_FRAME, count), -(args.len() as i32));
return Ok(());
}
let count = u8::try_from(args.len())
.map_err(|_| self.err("too many arguments for \"eval\"".to_string()))?;
for arg in args {
self.word(arg)?;
}
self.emit(Op::Extended(ext::EVAL, count), 1 - args.len() as i32);
Ok(())
}
fn cmd_uplevel(&mut self, args: &[Word]) -> Result<(), CompileError> {
if args.is_empty() {
return self.error("wrong # args: should be \"uplevel ?level? command ?arg ...?\"");
}
let has_level = args.len() > 1
&& args[0]
.as_literal()
.is_some_and(looks_like_a_level);
let declared = self.declared_globals().unwrap_or_default();
let count = u8::try_from(args.len() + if has_level { 1 } else { 2 })
.map_err(|_| self.err("too many arguments for \"uplevel\"".to_string()))?;
self.push_str(&declared);
if has_level {
self.word(&args[0])?;
for arg in &args[1..] {
self.word(arg)?;
}
} else {
self.push_str("1");
for arg in args {
self.word(arg)?;
}
}
let pushed = if has_level { args.len() + 1 } else { args.len() + 2 };
self.emit(Op::Extended(ext::UPLEVEL, count), 1 - pushed as i32);
Ok(())
}
fn cmd_apply(&mut self, args: &[Word]) -> Result<(), CompileError> {
if args.is_empty() {
return self.error("wrong # args: should be \"apply lambdaExpr ?arg ...?\"");
}
let count = u8::try_from(args.len())
.map_err(|_| self.err("too many arguments for \"apply\"".to_string()))?;
for arg in args {
self.word(arg)?;
}
self.emit(Op::Extended(ext::APPLY, count), 1 - args.len() as i32);
Ok(())
}
fn cmd_puts(&mut self, args: &[Word]) -> Result<(), CompileError> {
let (newline, value) = match args {
[v] => (true, v),
[flag, v] if flag.as_literal() == Some("-nonewline") => (false, v),
_ => return self.error("wrong # args: should be \"puts ?-nonewline? string\""),
};
self.word(value)?;
self.emit(Op::Extended(ext::PUTS, u8::from(newline)), 0);
Ok(())
}
fn cmd_expr(&mut self, args: &[Word]) -> Result<(), CompileError> {
if args.is_empty() {
return self.error("wrong # args: should be \"expr arg ?arg ...?\"");
}
let mut text = String::new();
for (i, w) in args.iter().enumerate() {
let piece = self.literal_of(w, "expression")?;
if i > 0 {
text.push(' ');
}
text.push_str(piece);
}
let parsed = expr::parse(&text).map_err(|e| self.deferrable_err(e.msg))?;
if matches!(&parsed, Expr::Float(v, _) if v.is_nan()) {
self.push_str("domain error: argument not in valid range");
self.emit(Op::Extended(ext::ERROR, 0), -1);
self.push_empty();
return Ok(());
}
self.expr(&parsed)?;
if !Self::yields_number(&parsed) {
self.emit(Op::Extended(ext::CANON, 0), 0);
}
Ok(())
}
fn cmd_incr(&mut self, args: &[Word]) -> Result<(), CompileError> {
let (name, by) = match args {
[n] => (n, None),
[n, by] => (n, Some(by)),
_ => return self.error("wrong # args: should be \"incr varName ?increment?\""),
};
if let Some(text) = by.and_then(|w| w.as_literal()) {
if crate::runtime::tcl_int(&Value::Str(std::sync::Arc::new(text.to_string()))).is_err()
{
return self.error(format!(
"expected integer but got {}",
crate::runtime::named(text, 50)
));
}
}
let name = match self.target_of(name)? {
Target::Scalar(name) => name,
Target::Elem { name, index } => return self.elem_incr(&name, &index, by),
};
let read_at = self.b.current_pos();
self.scalar_get(&name);
if self.b.current_pos() == read_at + 1 {
self.tolerant_reads.push(read_at);
}
match by {
Some(w) => self.word(w)?,
None => {
self.emit(Op::LoadInt(1), 1);
}
}
self.incr_sites.push(self.b.current_pos());
self.emit(Op::Add, -1);
self.emit(Op::Dup, 1);
self.emit_set_var(&name);
Ok(())
}
fn cmd_if(&mut self, args: &[Word]) -> Result<(), CompileError> {
let mut i = 0;
let mut end_jumps = Vec::new();
let branch_depth = self.depth;
loop {
let Some(cond) = args.get(i) else {
return self.error("wrong # args: no expression after \"if\" argument");
};
self.expr_word(cond)?;
let jump_false = self.emit(Op::JumpIfFalse(usize::MAX), -1);
i += 1;
if args.get(i).and_then(|w| w.as_literal()) == Some("then") {
i += 1;
}
let Some(body) = args.get(i) else {
return self.error("wrong # args: no script following \"if\" argument");
};
self.body(body)?;
i += 1;
end_jumps.push(self.emit(Op::Jump(usize::MAX), 0));
let else_start = self.b.current_pos();
self.b.patch_jump(jump_false, else_start);
self.depth = branch_depth;
match args.get(i).and_then(|w| w.as_literal()) {
Some("elseif") => {
i += 1;
continue;
}
Some("else") => {
i += 1;
let Some(body) = args.get(i) else {
return self.error("wrong # args: no script following \"else\" argument");
};
self.body(body)?;
i += 1;
break;
}
None if i == args.len() => {
self.push_empty();
break;
}
Some(other) => {
return self.error(format!("expected \"elseif\" or \"else\", got \"{other}\""))
}
None => return self.error("non-literal clause after \"if\" body"),
}
}
if i != args.len() {
return self.error("wrong # args: extra arguments after \"if\" script");
}
let end = self.b.current_pos();
for j in end_jumps {
self.b.patch_jump(j, end);
}
Ok(())
}
fn cmd_while(&mut self, args: &[Word]) -> Result<(), CompileError> {
let [cond, body] = args else {
return self.error("wrong # args: should be \"while test command\"");
};
let script = self.body_of(body)?;
self.rotated_loop(|c| c.emit_body(&script), |_| Ok(()), |c| c.expr_word(cond))?;
self.push_empty();
Ok(())
}
fn cmd_foreach(&mut self, args: &[Word]) -> Result<(), CompileError> {
let Some((body, pairs)) = args.split_last() else {
return self.error(
"wrong # args: should be \"foreach varList list ?varList list ...? command\"",
);
};
if pairs.is_empty() || pairs.len() % 2 != 0 {
return self.error(
"wrong # args: should be \"foreach varList list ?varList list ...? command\"",
);
}
let mut names = Vec::new();
for pair in pairs.chunks(2) {
let text = self
.literal_of(&pair[0], "foreach variable list")?
.to_string();
let vars = crate::list::split(&text).map_err(|msg| CompileError {
msg,
line: self.line,
})?;
if vars.is_empty() {
return self.error("foreach varlist is empty");
}
let count = vars.len();
for name in vars {
if name.ends_with(')') && name.contains('(') {
return self.error("array variables are not supported yet");
}
names.push(name);
}
self.push_value(Value::Int(count as i64));
self.word(&pair[1])?;
}
let lists = u8::try_from(pairs.len() / 2)
.map_err(|_| self.err("too many lists for \"foreach\"".to_string()))?;
let width = u8::try_from(names.len())
.map_err(|_| self.err("too many variables for \"foreach\"".to_string()))?;
self.emit(
Op::Extended(ext::FOREACH_INIT, lists),
1 - pairs.len() as i32,
);
let script = self.body_of(body)?;
let taken: Vec<String> = names.iter().rev().cloned().collect();
self.rotated_loop(
|c| {
c.emit(Op::Extended(ext::FOREACH_TAKE, width), i32::from(width));
for name in &taken {
c.emit_set_var(name);
}
c.emit_body(&script)
},
|c| {
c.emit(Op::Extended(ext::FOREACH_ADVANCE, 0), 0);
Ok(())
},
|c| {
c.emit(Op::Extended(ext::FOREACH_MORE, 0), 1);
Ok(())
},
)?;
self.emit(Op::Pop, -1);
self.push_empty();
Ok(())
}
fn cmd_loop_exit(&mut self, args: &[Word], is_break: bool) -> Result<(), CompileError> {
let word = if is_break { "break" } else { "continue" };
if !args.is_empty() {
return self.error(format!("wrong # args: should be \"{word}\""));
}
let Some(ctx) = self.loops.last() else {
return self.error(format!("invoked \"{word}\" outside of a loop"));
};
if ctx.catch_depth != self.catch_depth {
return self.error(format!(
"\"{word}\" out of a \"catch\" script is not supported"
));
}
let surplus = self.depth.saturating_sub(ctx.depth);
for _ in 0..surplus {
self.emit(Op::Pop, -1);
}
let jump = self.emit(Op::Jump(usize::MAX), 0);
let ctx = self.loops.last_mut().expect("loop context");
if is_break {
ctx.breaks.push(jump);
} else {
ctx.continues.push(jump);
}
self.push_empty();
Ok(())
}
pub(crate) fn rotated_loop<B, S, C>(
&mut self,
body: B,
step: S,
cond: C,
) -> Result<(), CompileError>
where
B: FnOnce(&mut Self) -> Result<(), CompileError>,
S: FnOnce(&mut Self) -> Result<(), CompileError>,
C: FnOnce(&mut Self) -> Result<(), CompileError>,
{
let entry = self.depth;
let enter = self.emit(Op::Jump(usize::MAX), 0);
let top = self.b.current_pos();
self.loops.push(LoopCtx {
depth: entry,
catch_depth: self.catch_depth,
breaks: Vec::new(),
continues: Vec::new(),
});
let emitted = body(self).and_then(|()| {
let at = self.b.current_pos();
step(self).map(|()| at)
});
let ctx = self.loops.pop().expect("loop context");
let step_at = emitted?;
let cond_at = self.b.current_pos();
self.b.patch_jump(enter, cond_at);
for j in ctx.continues {
self.b.patch_jump(j, step_at);
}
debug_assert_eq!(self.depth, entry, "rotated loop body is unbalanced");
cond(self)?;
self.emit(Op::JumpIfTrue(top), -1);
let end = self.b.current_pos();
for j in ctx.breaks {
self.b.patch_jump(j, end);
}
Ok(())
}
pub(crate) fn body(&mut self, word: &Word) -> Result<(), CompileError> {
match self.body_script(word) {
Ok(script) => self.nested_value(&script),
Err(e) if std::mem::take(&mut self.deferrable) => self.raise_at_run_time(&e.msg),
Err(e) => Err(e),
}
}
pub(crate) fn raise_at_run_time(&mut self, msg: &str) -> Result<(), CompileError> {
self.push_str(msg);
self.emit(Op::ExtendedWide(ext_wide::ERROR_AT, self.command_line), -1);
self.push_empty();
Ok(())
}
pub(crate) fn body_script(&mut self, word: &Word) -> Result<Script, CompileError> {
let text = self.literal_of(word, "script body")?;
crate::parser::parse(text).map_err(|e| self.deferrable_err(e.msg))
}
pub(crate) fn body_of(&mut self, word: &Word) -> Result<Body, CompileError> {
match self.body_script(word) {
Ok(script) => Ok(Body::Script(script)),
Err(e) if std::mem::take(&mut self.deferrable) => Ok(Body::Deferred(e.msg)),
Err(e) => Err(e),
}
}
pub(crate) fn emit_body(&mut self, body: &Body) -> Result<(), CompileError> {
match body {
Body::Script(script) => self.nested_effect(script),
Body::Deferred(msg) => {
let msg = msg.clone();
self.raise_at_run_time(&msg)?;
self.emit(Op::Pop, -1);
Ok(())
}
}
}
pub(crate) fn emit_body_value(&mut self, body: &Body) -> Result<(), CompileError> {
match body {
Body::Script(script) => self.nested_value(script),
Body::Deferred(msg) => {
let msg = msg.clone();
self.raise_at_run_time(&msg)
}
}
}
pub(crate) fn expr_word(&mut self, word: &Word) -> Result<(), CompileError> {
let text = self.literal_of(word, "condition")?.to_string();
let parsed = expr::parse(&text).map_err(|e| self.deferrable_err(e.msg))?;
self.condition(&parsed)
}
pub(crate) fn condition(&mut self, e: &Expr) -> Result<(), CompileError> {
self.expr(e)?;
if !Self::yields_number(e) || Self::can_be_nan(e) {
self.emit(Op::Extended(ext::BOOL, 0), 0);
}
Ok(())
}
fn can_be_nan(e: &Expr) -> bool {
if Self::yields_integer(e) {
return false;
}
match e {
Expr::Float(v, _) => v.is_nan(),
Expr::Unary(UnOp::Plus | UnOp::Neg, operand) => Self::can_be_nan(operand),
Expr::Ternary(_, then, other) => Self::can_be_nan(then) || Self::can_be_nan(other),
_ => true,
}
}
fn numeric_operand(&mut self, e: &Expr) -> Result<(), CompileError> {
if let Expr::Float(v, text) = e {
if !v.is_finite() || crate::runtime::format_double(*v) != **text {
self.push_str(text);
return Ok(());
}
}
self.expr(e)
}
fn string_operand(&mut self, e: &Expr) -> Result<(), CompileError> {
match e {
Expr::Int(_, text) | Expr::Float(_, text) => {
self.push_str(text);
Ok(())
}
other => self.expr(other),
}
}
fn str_cmp(op: &BinOp) -> Option<u8> {
match op {
BinOp::StrLt => Some(0),
BinOp::StrGt => Some(1),
BinOp::StrLe => Some(2),
BinOp::StrGe => Some(3),
BinOp::StrEq => Some(4),
BinOp::StrNe => Some(5),
_ => None,
}
}
fn yields_number(e: &Expr) -> bool {
match e {
Expr::Int(_, _) | Expr::Float(_, _) => true,
Expr::Subst(_) => false,
Expr::Unary(UnOp::Plus, operand) => Self::yields_number(operand),
Expr::Unary(_, _) => true,
Expr::Binary(_, _, _) => true,
Expr::Ternary(_, then, other) => {
Self::yields_number(then) && Self::yields_number(other)
}
Expr::Call(_, _) => true,
}
}
fn may_be_non_finite(e: &Expr) -> bool {
match e {
Expr::Float(f, _) => !f.is_finite(),
Expr::Int(_, _) | Expr::Subst(_) => false,
Expr::Unary(_, operand) => Self::may_be_non_finite(operand),
Expr::Binary(BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div, a, b) => {
Self::may_be_non_finite(a) || Self::may_be_non_finite(b)
}
Expr::Binary(_, _, _) => false,
Expr::Ternary(_, then, other) => {
Self::may_be_non_finite(then) || Self::may_be_non_finite(other)
}
Expr::Call(_, _) => false,
}
}
fn fits_machine_int(e: &Expr) -> bool {
match e {
Expr::Int(_, _) => true,
Expr::Float(_, _) | Expr::Subst(_) => false,
Expr::Unary(UnOp::Not, _) => true,
Expr::Unary(_, _) => false,
Expr::Binary(
BinOp::Lt
| BinOp::Gt
| BinOp::Le
| BinOp::Ge
| BinOp::Eq
| BinOp::Ne
| BinOp::StrLt
| BinOp::StrGt
| BinOp::StrLe
| BinOp::StrGe
| BinOp::StrEq
| BinOp::StrNe
| BinOp::In
| BinOp::Ni
| BinOp::And
| BinOp::Or,
_,
_,
) => true,
Expr::Binary(BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor, a, b) => {
Self::fits_machine_int(a) && Self::fits_machine_int(b)
}
Expr::Binary(BinOp::Shr, a, _) => Self::fits_machine_int(a),
Expr::Binary(_, _, _) => false,
Expr::Ternary(_, then, other) => {
Self::fits_machine_int(then) && Self::fits_machine_int(other)
}
Expr::Call(_, _) => false,
}
}
fn yields_integer(e: &Expr) -> bool {
match e {
Expr::Int(_, _) => true,
Expr::Float(_, _) | Expr::Subst(_) => false,
Expr::Unary(UnOp::Not, _) => true,
Expr::Unary(_, operand) => Self::yields_integer(operand),
Expr::Binary(
BinOp::Lt
| BinOp::Gt
| BinOp::Le
| BinOp::Ge
| BinOp::Eq
| BinOp::Ne
| BinOp::StrLt
| BinOp::StrGt
| BinOp::StrLe
| BinOp::StrGe
| BinOp::StrEq
| BinOp::StrNe
| BinOp::In
| BinOp::Ni
| BinOp::And
| BinOp::Or,
_,
_,
) => true,
Expr::Binary(_, a, b) => Self::yields_integer(a) && Self::yields_integer(b),
Expr::Ternary(_, then, other) => {
Self::yields_integer(then) && Self::yields_integer(other)
}
Expr::Call(_, _) => false,
}
}
fn expr(&mut self, e: &Expr) -> Result<(), CompileError> {
match e {
Expr::Int(v, _) => {
self.emit(Op::LoadInt(*v), 1);
Ok(())
}
Expr::Float(v, _) => {
self.emit(Op::LoadFloat(*v), 1);
Ok(())
}
Expr::Subst(parts) => {
let word = Word {
parts: parts.clone(),
..Word::default()
};
self.word(&word)
}
Expr::Unary(UnOp::Not, operand)
if !Self::yields_number(operand) || Self::can_be_nan(operand) =>
{
self.numeric_operand(operand)?;
self.emit(Op::Extended(ext::BOOL, 1), 0);
Ok(())
}
Expr::Unary(UnOp::BitNot, operand) if !Self::fits_machine_int(operand) => {
self.expr(operand)?;
self.emit(Op::Extended(ext::BIT_NOT, 1), 0);
Ok(())
}
Expr::Unary(UnOp::Plus, operand) if !Self::yields_number(operand) => {
self.expr(operand)?;
self.emit(Op::Extended(ext::UPLUS, 1), 0);
Ok(())
}
Expr::Unary(op, operand) => {
self.numeric_operand(operand)?;
match op {
UnOp::Neg => self.emit(Op::Negate, 0),
UnOp::Plus => 0, UnOp::BitNot => self.emit(Op::BitNot, 0),
UnOp::Not => self.emit(Op::LogNot, 0),
};
Ok(())
}
Expr::Binary(BinOp::And, a, b) => self.short_circuit(a, b, false),
Expr::Binary(BinOp::Or, a, b) => self.short_circuit(a, b, true),
Expr::Binary(op, a, b) if Self::str_cmp(op).is_some() => {
let which = Self::str_cmp(op).expect("guarded above");
self.string_operand(a)?;
self.string_operand(b)?;
self.emit(Op::Extended(ext::STR_CMP, which), -1);
Ok(())
}
Expr::Binary(op, a, b) => {
let integral = Self::fits_machine_int(a) && Self::fits_machine_int(b);
self.numeric_operand(a)?;
self.numeric_operand(b)?;
let native = match op {
BinOp::Add => Some(Op::Add),
BinOp::Sub => Some(Op::Sub),
BinOp::Mul => Some(Op::Mul),
BinOp::BitAnd if integral => Some(Op::BitAnd),
BinOp::BitOr if integral => Some(Op::BitOr),
BinOp::BitXor if integral => Some(Op::BitXor),
BinOp::Lt => Some(Op::NumLt),
BinOp::Gt => Some(Op::NumGt),
BinOp::Le => Some(Op::NumLe),
BinOp::Ge => Some(Op::NumGe),
BinOp::Eq => Some(Op::NumEq),
BinOp::Ne => Some(Op::NumNe),
_ => None,
};
match native {
Some(op) => {
let checks_nan = matches!(op, Op::Add | Op::Sub | Op::Mul)
&& (Self::may_be_non_finite(a) || Self::may_be_non_finite(b));
self.emit(op, -1);
if checks_nan {
self.emit(Op::Extended(ext::CANON, 0), 0);
}
}
None => {
let id = match op {
BinOp::Div => ext::DIV,
BinOp::Mod => ext::MOD,
BinOp::Pow => ext::POW,
BinOp::In => ext::IN,
BinOp::Ni => ext::NI,
BinOp::BitAnd => ext::BIT_AND,
BinOp::BitOr => ext::BIT_OR,
BinOp::BitXor => ext::BIT_XOR,
BinOp::Shl => ext::SHL,
BinOp::Shr => ext::SHR,
_ => unreachable!("binary op {op:?} has no lowering"),
};
self.emit(Op::Extended(id, 2), -1);
}
}
Ok(())
}
Expr::Ternary(cond, then, other) => {
self.condition(cond)?;
let to_else = self.emit(Op::JumpIfFalse(usize::MAX), -1);
let branch_depth = self.depth;
self.expr(then)?;
let to_end = self.emit(Op::Jump(usize::MAX), 0);
let else_start = self.b.current_pos();
self.b.patch_jump(to_else, else_start);
self.depth = branch_depth;
self.expr(other)?;
let end = self.b.current_pos();
self.b.patch_jump(to_end, end);
Ok(())
}
Expr::Call(name, _) => {
self.error(format!("math function \"{name}\" is not supported yet"))
}
}
}
fn short_circuit(&mut self, a: &Expr, b: &Expr, on_true: bool) -> Result<(), CompileError> {
self.condition(a)?;
let jump = if on_true {
self.emit(Op::JumpIfTrueKeep(usize::MAX), 0)
} else {
self.emit(Op::JumpIfFalseKeep(usize::MAX), 0)
};
self.emit(Op::Pop, -1);
self.condition(b)?;
let end = self.b.current_pos();
self.b.patch_jump(jump, end);
self.emit(Op::LogNot, 0);
self.emit(Op::LogNot, 0);
Ok(())
}
}
fn looks_like_a_level(word: &str) -> bool {
match word.strip_prefix('#') {
Some(rest) => !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()),
None => !word.is_empty() && word.bytes().all(|b| b.is_ascii_digit()),
}
}
pub(crate) fn literal_value(text: &str) -> Value {
if let Ok(i) = text.parse::<i64>() {
if i.to_string() == text {
return Value::Int(i);
}
}
Value::Str(std::sync::Arc::new(text.to_string()))
}