diff --git a/interpreter/src/builtins.rs b/interpreter/src/builtins.rs new file mode 100644 index 0000000..08ed295 --- /dev/null +++ b/interpreter/src/builtins.rs @@ -0,0 +1,439 @@ +// This file is part of the uutils awk package. +// +// For the full copyright and license information, please view the LICENSE +// files that was distributed with this source code. + +//! Built-in function implementations for the VM. +//! +//! Call sites lower to [`crate::ir::Instruction::IntrinsicCall`]; the VM passes +//! argument registers as a slice via [`Registers::get_range`](crate::vm::Registers) +//! and dispatches through [`Interpreter::call_builtin`]. Arity overloading +//! (e.g. `length`, `substr`, `and`) is handled per function. +//! +//! Behavior follows the [gawk built-in function](https://www.gnu.org/software/gawk/manual/html_node/Built_002din.html) +//! documentation. Non-trivial builtins are stubbed with `todo!` until implemented. + +use parser::{AriadneSpan, BuiltinFunction}; + +use crate::{ + InterpreterError, + vm::{Interpreter, types::Value}, +}; + +/// Bit width used by gawk bitwise ops on ordinary (non-MPFR) numbers. +const BIT_MASK: u64 = (1u64 << 53) - 1; + +#[derive(Debug)] +pub(crate) enum BuiltinError { + /// Wrong number of arguments; `expected` is the arity bound shown to the user. + Arity { expected: u8, given: u8 }, +} + +impl BuiltinError { + pub(crate) fn into_interpreter_error(self, span: AriadneSpan) -> InterpreterError { + match self { + Self::Arity { expected, given } => { + InterpreterError::ArityMismatch(span, expected, given) + } + } + } +} + +impl<'a> Interpreter<'a> { + /// Dispatch a built-in function over `args`. + /// + /// Kept on [`Interpreter`] so future builtins can use `ExecMode`, I/O, and + /// other VM state without reshaping the call site. Currently `&self` is + /// enough; switch to `&mut self` (and copy/own args at the call site) when + /// a builtin needs to mutate the VM. + pub(crate) fn call_builtin( + &self, + fun: BuiltinFunction, + args: &[Value<'a>], + ) -> Result, BuiltinError> { + match fun { + BuiltinFunction::Int => Ok(Value::Float(require_args(args, 1, 1)?[0].to_int() as f64)), + BuiltinFunction::Sqrt => Ok(Value::Float(require_args(args, 1, 1)?[0].to_num().sqrt())), + BuiltinFunction::Exp => Ok(Value::Float(require_args(args, 1, 1)?[0].to_num().exp())), + BuiltinFunction::Log => Ok(Value::Float(require_args(args, 1, 1)?[0].to_num().ln())), + BuiltinFunction::Sin => Ok(Value::Float(require_args(args, 1, 1)?[0].to_num().sin())), + BuiltinFunction::Cos => Ok(Value::Float(require_args(args, 1, 1)?[0].to_num().cos())), + BuiltinFunction::Atan2 => { + let args = require_args(args, 2, 2)?; + Ok(Value::Float(args[0].to_num().atan2(args[1].to_num()))) + } + BuiltinFunction::Length => self.builtin_length(args), + BuiltinFunction::Index => index(args), + BuiltinFunction::Substr => substr(args), + BuiltinFunction::Toupper => Ok(map_string(require_args(args, 1, 1)?, |b| { + b.to_ascii_uppercase() + })), + BuiltinFunction::Tolower => Ok(map_string(require_args(args, 1, 1)?, |b| { + b.to_ascii_lowercase() + })), + BuiltinFunction::And => bitwise_variadic(args, |a, b| a & b), + BuiltinFunction::Or => bitwise_variadic(args, |a, b| a | b), + BuiltinFunction::Xor => bitwise_variadic(args, |a, b| a ^ b), + BuiltinFunction::Compl => { + let n = to_bits(&require_args(args, 1, 1)?[0]); + Ok(Value::Float((BIT_MASK ^ n) as f64)) + } + BuiltinFunction::Lshift => shift(args, true), + BuiltinFunction::Rshift => shift(args, false), + BuiltinFunction::Strtonum => Ok(Value::Float(strtonum(&require_args(args, 1, 1)?[0]))), + BuiltinFunction::Typeof => Ok(typeof_value(&require_args(args, 1, 1)?[0])), + BuiltinFunction::Isarray => { + // FIXME: array arguments currently hit scalar_context before dispatch. + let v = &require_args(args, 1, 1)?[0]; + Ok(Value::Int(matches!(v, Value::Array(_)) as isize)) + } + // Placeholders — call glue and dispatch exist; bodies come later. + BuiltinFunction::Split + | BuiltinFunction::Sub + | BuiltinFunction::Gsub + | BuiltinFunction::Match + | BuiltinFunction::Sprintf + | BuiltinFunction::Gensub + | BuiltinFunction::Patsplit + | BuiltinFunction::Close + | BuiltinFunction::Fflush + | BuiltinFunction::System + | BuiltinFunction::Rand + | BuiltinFunction::Srand + | BuiltinFunction::Systime + | BuiltinFunction::Mktime + | BuiltinFunction::Strftime + | BuiltinFunction::Asort + | BuiltinFunction::Asorti => todo!("built-in {fun}"), + } + } + + fn builtin_length(&self, args: &[Value<'a>]) -> Result, BuiltinError> { + match args { + [] => { + // `length()` — length of `$0`. Unassigned/`$0` before input → 0. + Ok(Value::Float( + value_length(self.symbols.record(Value::Int(0))) as f64, + )) + } + [v] => Ok(Value::Float(value_length(v) as f64)), + _ => Err(BuiltinError::Arity { expected: 1, given: args.len() as u8 }), + } + } +} + +fn require_args<'a, 'b>( + args: &'b [Value<'a>], + min: u8, + max: u8, +) -> Result<&'b [Value<'a>], BuiltinError> { + let given = args.len() as u8; + if given < min || given > max { + let expected = if given > max { max } else { min }; + return Err(BuiltinError::Arity { expected, given }); + } + Ok(args) +} + +fn value_length(v: &Value<'_>) -> usize { + match v { + Value::Array(arr) => arr.borrow().len(), + other => { + let mut buf = Vec::new(); + other.write_string(&mut buf); + buf.len() + } + } +} + +fn index<'a>(args: &[Value<'a>]) -> Result, BuiltinError> { + let args = require_args(args, 2, 2)?; + let hay = value_bytes(&args[0]); + let needle = value_bytes(&args[1]); + if needle.is_empty() { + return Ok(Value::Float(1.)); + } + let pos = hay + .windows(needle.len()) + .position(|w| w == needle.as_slice()) + .map_or(0, |i| i + 1); + Ok(Value::Float(pos as f64)) +} + +fn substr<'a>(args: &[Value<'a>]) -> Result, BuiltinError> { + let args = require_args(args, 2, 3)?; + let s = value_bytes(&args[0]); + let start = args[1].to_int(); + // gawk/POSIX: start < 1 is treated as 1. + let start_idx = if start <= 0 { + 0 + } else { + (start as usize).saturating_sub(1) + }; + if start_idx >= s.len() { + return Ok(Value::String(b"".into())); + } + let end = if let Some(n) = args.get(2) { + let n = n.to_int(); + if n <= 0 { + start_idx + } else { + (start_idx + n as usize).min(s.len()) + } + } else { + s.len() + }; + Ok(Value::String(s[start_idx..end].to_vec().into())) +} + +fn map_string<'a>(args: &[Value<'a>], map: impl Fn(u8) -> u8) -> Value<'a> { + let mut buf = value_bytes(&args[0]); + for b in &mut buf { + *b = map(*b); + } + Value::String(buf.into()) +} + +fn bitwise_variadic<'a>( + args: &[Value<'a>], + op: impl Fn(u64, u64) -> u64, +) -> Result, BuiltinError> { + let args = require_args(args, 2, u8::MAX)?; + let mut acc = to_bits(&args[0]); + for arg in &args[1..] { + acc = op(acc, to_bits(arg)) & BIT_MASK; + } + Ok(Value::Float(acc as f64)) +} + +fn shift<'a>(args: &[Value<'a>], left: bool) -> Result, BuiltinError> { + let args = require_args(args, 2, 2)?; + let shift = args[1].to_int(); + if shift < 0 { + // FIXME: gawk fatals on negative shift counts; wire a proper runtime error. + return Ok(Value::Float(0.)); + } + let shift = shift as u32; + let n = to_bits(&args[0]); + let result = if shift >= 64 { + 0 + } else if left { + (n << shift) & BIT_MASK + } else { + n >> shift + }; + Ok(Value::Float(result as f64)) +} + +fn to_bits(v: &Value<'_>) -> u64 { + let n = v.to_num(); + // FIXME: gawk fatals on negative bitwise operands; do not coerce to 0. + if !n.is_finite() || n < 0. { + return 0; + } + (n.trunc() as u64) & BIT_MASK +} + +fn strtonum(v: &Value<'_>) -> f64 { + let bytes = value_bytes(v); + let Ok(s) = std::str::from_utf8(&bytes) else { + return 0.; + }; + let s = s.trim_start(); + if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + return u64::from_str_radix(hex, 16).map_or(0., |n| n as f64); + } + if s.len() > 1 && s.starts_with('0') && s.bytes().all(|b| (b'0'..=b'7').contains(&b)) { + // Leading zero and only octal digits → octal, as in gawk `strtonum`. + return u64::from_str_radix(s, 8).map_or(0., |n| n as f64); + } + s.parse().unwrap_or(0.) +} + +fn typeof_value<'a>(v: &Value<'_>) -> Value<'a> { + let name: &[u8] = match v { + Value::Int(_) | Value::Float(_) | Value::Bool(_) => b"number", + Value::String(_) => b"string", + Value::Regex(_) => b"regexp", + Value::Array(_) => b"array", + Value::Untyped | Value::Unassigned => b"untyped", + }; + Value::String(name.into()) +} + +fn value_bytes(v: &Value<'_>) -> Vec { + let mut buf = Vec::with_capacity(v.string_size_hint()); + v.write_string(&mut buf); + buf +} + +#[cfg(test)] +mod tests { + use bumpalo::Bump; + use parser::MetadataStore; + + use super::*; + use crate::{ExecMode, ir::lower::CodeGen, vm::types::Value}; + + fn with_interp(f: impl FnOnce(&mut Interpreter<'_>)) { + let arena = Bump::new(); + let cg = CodeGen::new(&arena); + let mut interp = Interpreter::new(ExecMode::Uu, cg, MetadataStore::new()); + f(&mut interp); + } + + #[test] + fn int_truncates_toward_zero() { + with_interp(|intrp| { + assert_eq!( + intrp + .call_builtin(BuiltinFunction::Int, &[Value::Float(3.7)]) + .unwrap() + .to_num(), + 3. + ); + assert_eq!( + intrp + .call_builtin(BuiltinFunction::Int, &[Value::Float(-3.7)]) + .unwrap() + .to_num(), + -3. + ); + }); + } + + #[test] + fn length_of_string_and_empty_record() { + with_interp(|intrp| { + assert_eq!( + intrp + .call_builtin(BuiltinFunction::Length, &[Value::String(b"abc".into())]) + .unwrap() + .to_num(), + 3. + ); + assert_eq!( + intrp + .call_builtin(BuiltinFunction::Length, &[]) + .unwrap() + .to_num(), + 0. + ); + }); + } + + #[test] + fn index_and_substr() { + with_interp(|intrp| { + assert_eq!( + intrp + .call_builtin( + BuiltinFunction::Index, + &[ + Value::String(b"foobar".into()), + Value::String(b"bar".into()) + ], + ) + .unwrap() + .to_num(), + 4. + ); + let s = intrp + .call_builtin( + BuiltinFunction::Substr, + &[ + Value::String(b"abcdef".into()), + Value::Int(2), + Value::Int(3), + ], + ) + .unwrap(); + let mut buf = Vec::new(); + s.write_string(&mut buf); + assert_eq!(buf, b"bcd"); + }); + } + + #[test] + fn bitwise_and_or_xor_compl() { + with_interp(|intrp| { + assert_eq!( + intrp + .call_builtin(BuiltinFunction::And, &[Value::Int(7), Value::Int(3)]) + .unwrap() + .to_num(), + 3. + ); + assert_eq!( + intrp + .call_builtin( + BuiltinFunction::Or, + &[Value::Int(1), Value::Int(2), Value::Int(4)], + ) + .unwrap() + .to_num(), + 7. + ); + assert_eq!( + intrp + .call_builtin(BuiltinFunction::Xor, &[Value::Int(7), Value::Int(3)]) + .unwrap() + .to_num(), + 4. + ); + assert_eq!( + intrp + .call_builtin(BuiltinFunction::Compl, &[Value::Int(0)]) + .unwrap() + .to_num(), + BIT_MASK as f64 + ); + }); + } + + #[test] + fn arity_mismatch_is_reported() { + with_interp(|intrp| { + let err = intrp + .call_builtin(BuiltinFunction::And, &[Value::Int(1)]) + .unwrap_err(); + assert!(matches!(err, BuiltinError::Arity { expected: 2, given: 1 })); + }); + } + + #[test] + fn builtin_error_converts_with_span() { + let err = BuiltinError::Arity { expected: 2, given: 1 }; + let span = (parser::FileCache(None), 0..1); + assert!(matches!( + err.into_interpreter_error(span), + InterpreterError::ArityMismatch(_, 2, 1) + )); + } + + // --- FIXME coverage (ignored until behavior is implemented) --- + + #[ignore = "FIXME: gawk fatals on negative shift counts"] + #[test] + fn negative_shift_is_fatal() { + with_interp(|intrp| { + let err = intrp.call_builtin(BuiltinFunction::Lshift, &[Value::Int(1), Value::Int(-1)]); + assert!( + err.is_err(), + "expected fatal for negative shift, got {err:?}" + ); + }); + } + + #[ignore = "FIXME: gawk fatals on negative bitwise operands"] + #[test] + fn negative_bitwise_operand_is_fatal() { + with_interp(|intrp| { + let err = intrp.call_builtin(BuiltinFunction::And, &[Value::Int(-1), Value::Int(1)]); + assert!( + err.is_err(), + "expected fatal for negative bitwise operand, got {err:?}" + ); + }); + } +} diff --git a/interpreter/src/ir.rs b/interpreter/src/ir.rs index 2825b1e..802552a 100644 --- a/interpreter/src/ir.rs +++ b/interpreter/src/ir.rs @@ -16,7 +16,7 @@ mod tests; use std::fmt::{self, Debug, Display, Formatter}; -use parser::{Command, Redirection}; +use parser::{BuiltinFunction, Command, Redirection}; pub type RegWidth = u8; pub type IxWidth = u32; @@ -69,7 +69,7 @@ pub enum Instruction { StoreR { dest: Reg, src: Arg, arg: Arg, ty: ArgTy, tys: ArgTy }, StoreA { dest: Reg, ty_place: ArgTy, start: Reg, end: Reg, var: NonLocal, arg: Reg }, LoadA { dest: Reg, ty_place: ArgTy, start: Reg, end: Reg, var: NonLocal }, - IntrinsicCall { dest: Reg, start: Reg, end: Reg, name: NonLocal }, + IntrinsicCall { dest: Reg, start: Reg, end: Reg, fun: BuiltinFunction }, OutputCall { start: Reg, end: Reg, cmd: Command, redir: Option }, UserCall { dest: Reg, start: Reg, end: Reg, name: NonLocal }, IndirectCall { dest: Reg, start: Reg, end: Reg, name: Arg, ty: ArgTy }, @@ -261,8 +261,8 @@ impl Display for Instruction { write!(f, "{op}")?; fmt_arg(f, arg, ty, " ") } - Self::IntrinsicCall { dest, start, end, name } => { - write!(f, "{dest} <- {op} {name}, {start}..{end}") + Self::IntrinsicCall { dest, start, end, fun } => { + write!(f, "{dest} <- {op} {fun}, {start}..{end}") } Self::IndirectCall { dest, start, end, name, ty } => { write!(f, "{dest} <- {op}")?; diff --git a/interpreter/src/ir/lower.rs b/interpreter/src/ir/lower.rs index c68a8ad..757ef93 100644 --- a/interpreter/src/ir/lower.rs +++ b/interpreter/src/ir/lower.rs @@ -653,6 +653,10 @@ impl<'a> CodeGen<'a> { let (start, end, ()) = this.gen_call_convention(args, |_| ()); this.emit(Instruction::UserCall { dest, start, end, name }); } + ExprNode::BuiltinCall(fun, args) => { + let (start, end, ()) = this.gen_call_convention(args, |_| ()); + this.emit(Instruction::IntrinsicCall { dest, start, end, fun: *fun }); + } ExprNode::IndirectCall(place, args) => { let (start, end, ()) = this.gen_call_convention(args, |_| ()); let TypedArg(name, ty) = diff --git a/interpreter/src/ir/tests.rs b/interpreter/src/ir/tests.rs index 4862162..863e4a1 100644 --- a/interpreter/src/ir/tests.rs +++ b/interpreter/src/ir/tests.rs @@ -355,3 +355,26 @@ fn array_multi_index_assignment_lowers_storea() { assert!(bc.contains("astore"), "expected StoreA:\n{bc}"); }); } + +#[test] +fn builtin_call_lowers_to_icall() { + with_lower("BEGIN { print int(3.7) }", |cg| { + let bc = format!("{}", cg.bc); + assert!( + bc.contains(" <- icall int,"), + "expected IntrinsicCall for int():\n{bc}" + ); + }); +} + +#[test] +fn builtin_call_nested_in_expression_lowers_icall() { + with_lower("BEGIN { x = length(\"abc\") + sqrt(4) }", |cg| { + let bc = format!("{}", cg.bc); + assert!( + bc.contains(" <- icall length,"), + "expected length icall:\n{bc}" + ); + assert!(bc.contains(" <- icall sqrt,"), "expected sqrt icall:\n{bc}"); + }); +} diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index 286e1fa..4c4cc18 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // files that was distributed with this source code. +mod builtins; pub(crate) mod ir; mod vm; diff --git a/interpreter/src/vm.rs b/interpreter/src/vm.rs index 2d3accd..72d55a4 100644 --- a/interpreter/src/vm.rs +++ b/interpreter/src/vm.rs @@ -42,7 +42,7 @@ pub struct Interpreter<'a> { program_counter: IxWidth, code_end: IxWidth, registers: Registers<'a>, - symbols: SymbolTable<'a>, + pub(crate) symbols: SymbolTable<'a>, consts: Consts<'a>, _compat: ExecMode, frames: StdVec, @@ -562,7 +562,16 @@ impl<'a> Interpreter<'a> { } self.write_reg(dest, val); } - Instruction::IntrinsicCall { dest: _, start: _, end: _, name: _ } => todo!(), + Instruction::IntrinsicCall { dest, start, end, fun } => { + let offset = self.reg_offset(); + let args = self.registers.get_range(start..end, offset); + match self.call_builtin(fun, args) { + Ok(val) => self.write_reg(dest, val), + Err(err) => { + return Err(err.into_interpreter_error(self.get_span(metadata))); + } + } + } Instruction::OutputCall { start, end, cmd, redir } => { return Ok(Signal::Suspend(self.print_req(start, end, cmd, redir))); } @@ -775,7 +784,7 @@ impl<'a> Registers<'a> { fn write(&mut self, dest: Reg, offset: IxWidth, src: impl Into>) { self.0[dest.0 as usize + offset as usize] = src.into(); } - fn get_range(&mut self, regs: Range, offset: IxWidth) -> &[Value<'a>] { + fn get_range(&self, regs: Range, offset: IxWidth) -> &[Value<'a>] { let start = Self::index_of(regs.start, offset); let end = Self::index_of(regs.end, offset); &self.0[start..end] @@ -842,7 +851,7 @@ impl Arg { stack_space: &mut MaybeUninit>, ) { match ty { - ArgTy::Reg | ArgTy::Cnt => {} + ArgTy::Reg | ArgTy::Cnt | ArgTy::ImmF => {} ArgTy::Rec => todo!(), ArgTy::Imm => { stack_space.write(Value::Int(unsafe { self.imm } as _)); @@ -869,7 +878,7 @@ impl Arg { ArgTy::Reg => intrp.read_reg(unsafe { self.reg }), ArgTy::Rec => todo!(), ArgTy::Imm => unsafe { stack_space.assume_init_ref() }, - ArgTy::Cnt => &intrp.consts.0[unsafe { self.sym.0 } as usize], + ArgTy::Cnt | ArgTy::ImmF => &intrp.consts.0[unsafe { self.sym.0 } as usize], ArgTy::UsVal => intrp.symbols.raw_user_lookup(unsafe { self.sym }), _ => todo!(), } diff --git a/parser/src/lex.rs b/parser/src/lex.rs index 7559d9c..245225b 100644 --- a/parser/src/lex.rs +++ b/parser/src/lex.rs @@ -235,6 +235,7 @@ impl TokenExt for Token<'_> { fn is_expr_start(&self) -> bool { self.is_atom() || self.is_prefix_op() + || self.maps_to_builtin().is_some() || matches!( self, Token::IndirectCall(_) | Token::Getline | Token::OpenParent diff --git a/parser/src/pratt.rs b/parser/src/pratt.rs index a32cf40..e49a969 100644 --- a/parser/src/pratt.rs +++ b/parser/src/pratt.rs @@ -387,6 +387,12 @@ impl<'a, 'b> Pratt<'a, 'b> { leaf_span, )) } + } else if let Some(builtin) = next.maps_to_builtin() { + self.parser.parse_function_call( + lex, + |args| ExprNode::BuiltinCall(builtin, args), + lex.span(), + ) } else if let Token::IndirectCall(name) = next { // BUG(gawk): it accepts special variables iff qualified, // even if it is with the `awk` namespace. diff --git a/parser/src/tests.rs b/parser/src/tests.rs index 2b4a3b7..0776719 100644 --- a/parser/src/tests.rs +++ b/parser/src/tests.rs @@ -820,6 +820,18 @@ fn test_parser_unary_and_divide() { }); } +#[test] +fn test_parser_builtin_in_expression_context() { + let source = r#" + BEGIN { print int(3.7); x = length("ab") } + "#; + test_parser!(source => { + begin: [ + r#"(body (Print (Int 3.7)) (Assignment awk::x (Length "ab")))"# + ], + }); +} + #[test] fn test_parser_proper_assignments() { let source = r" diff --git a/tests/by-util/test_awk.rs b/tests/by-util/test_awk.rs index eea9e71..1e8e759 100644 --- a/tests/by-util/test_awk.rs +++ b/tests/by-util/test_awk.rs @@ -358,3 +358,74 @@ fn user_functions_fib() { .succeeds() .stdout_only("0\n1\n1\n2\n3\n5\n8\n13\n21\n34\n"); } + +#[test] +fn builtin_numeric_and_string_functions() { + ucmd() + .arg( + r#"BEGIN { + print int(3.7) + print int(-3.7) + print sqrt(4) + print length("abc") + print length() + print toupper("ab") + print tolower("AB") + print index("foobar", "bar") + print substr("abcdef", 2, 3) + print substr("abcdef", 2) + print and(7, 3) + print or(1, 2, 4) + print xor(7, 3) + print compl(0) + print lshift(1, 3) + print rshift(8, 2) + print strtonum("0x10") + print strtonum("010") + print typeof(1) + print typeof("") + }"#, + ) + .succeeds() + .stdout_only( + "\ +3 +-3 +2 +3 +0 +AB +ab +4 +bcd +bcdef +3 +7 +4 +9007199254740991 +8 +2 +16 +8 +number +string +", + ); +} + +#[test] +fn builtin_math_trig_basics() { + ucmd() + .arg("BEGIN { print exp(0); print log(1); print sin(0); print cos(0); print atan2(0, 1) }") + .succeeds() + .stdout_only("1\n0\n0\n1\n0\n"); +} + +#[ignore = "FIXME: array arguments currently hit scalar_context before dispatch"] +#[test] +fn builtin_isarray_with_array_variable() { + ucmd() + .arg("BEGIN { a[1] = 1; print isarray(a); print length(a) }") + .succeeds() + .stdout_only("1\n1\n"); +}