1use crate::ast::{Stmt, Value, VarPath};
20use crate::interpreter::{value_defaults_on_emptiness, value_to_string, PathError, Scope};
21use std::ops::Range;
22
23#[derive(Debug, Clone, PartialEq)]
29pub struct ArithError {
30 pub message: String,
31 pub span: Range<usize>,
32}
33
34impl ArithError {
35 fn new(message: impl Into<String>, span: Range<usize>) -> Self {
36 Self { message: message.into(), span }
37 }
38}
39
40impl std::fmt::Display for ArithError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.write_str(&self.message)
43 }
44}
45
46impl std::error::Error for ArithError {}
47
48const MAX_DEPTH: usize = 256;
49
50pub(crate) fn leading_zero_decimal(text: &str) -> Option<String> {
62 if !crate::lexer::is_leading_zero_numeral(text) {
63 return None;
64 }
65 let sign = if text.starts_with('-') { "-" } else { "" };
66 let digits = text.trim_start_matches('-').trim_start_matches('0');
67 Some(format!("{sign}{}", if digits.is_empty() { "0" } else { digits }))
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
75pub(crate) enum BinOp {
76 Add, Sub, Mul, Div, Rem, Pow, Shl, Shr,
77 Lt, Le, Gt, Ge, Eq, Ne,
78 BitAnd, BitXor, BitOr, And, Or,
79}
80
81impl BinOp {
82 fn symbol(self) -> &'static str {
83 match self {
84 BinOp::Add => "+", BinOp::Sub => "-", BinOp::Mul => "*",
85 BinOp::Div => "/", BinOp::Rem => "%", BinOp::Pow => "**",
86 BinOp::Shl => "<<", BinOp::Shr => ">>",
87 BinOp::Lt => "<", BinOp::Le => "<=", BinOp::Gt => ">", BinOp::Ge => ">=",
88 BinOp::Eq => "==", BinOp::Ne => "!=",
89 BinOp::BitAnd => "&", BinOp::BitXor => "^", BinOp::BitOr => "|",
90 BinOp::And => "&&", BinOp::Or => "||",
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq)]
96pub(crate) enum UnOp {
97 Neg,
98 Not,
99 BitNot,
100}
101
102#[derive(Debug, Clone, PartialEq)]
106pub(crate) enum Expansion {
107 Var(String),
109 BracedPath { root: String, brackets: String },
112 BracedDefault { root: String, brackets: String, default: String },
115 LastExitCode,
117 CurrentPid,
119 CommandSubst(Vec<Stmt>),
121 Nested(Box<ArithExpr>),
123}
124
125#[derive(Debug, Clone, PartialEq)]
126pub(crate) enum ArithExpr {
127 Int(i64),
128 Expansion(Expansion),
129 Subscript { root: String, indices: Vec<ArithExpr> },
132 BasedExpansion { base: u32, expansion: Box<Expansion> },
135 Unary { op: UnOp, operand: Box<ArithExpr> },
136 Binary { op: BinOp, left: Box<ArithExpr>, right: Box<ArithExpr> },
137 Ternary { cond: Box<ArithExpr>, then_branch: Box<ArithExpr>, else_branch: Box<ArithExpr> },
138}
139
140impl ArithExpr {
141 pub(crate) fn contains_command_subst(&self) -> bool {
144 fn expansion_has(e: &Expansion) -> bool {
145 match e {
146 Expansion::CommandSubst(_) => true,
147 Expansion::Nested(inner) => inner.contains_command_subst(),
148 Expansion::BracedDefault { default, .. } => parse(default)
154 .map(|parsed| parsed.contains_command_subst())
155 .unwrap_or(false),
156 Expansion::Var(_)
157 | Expansion::BracedPath { .. }
158 | Expansion::LastExitCode
159 | Expansion::CurrentPid => false,
160 }
161 }
162 match self {
163 ArithExpr::Int(_) => false,
164 ArithExpr::Expansion(e) => expansion_has(e),
165 ArithExpr::Subscript { indices, .. } => {
166 indices.iter().any(ArithExpr::contains_command_subst)
167 }
168 ArithExpr::BasedExpansion { expansion, .. } => expansion_has(expansion),
169 ArithExpr::Unary { operand, .. } => operand.contains_command_subst(),
170 ArithExpr::Binary { left, right, .. } => {
171 left.contains_command_subst() || right.contains_command_subst()
172 }
173 ArithExpr::Ternary { cond, then_branch, else_branch } => {
174 cond.contains_command_subst()
175 || then_branch.contains_command_subst()
176 || else_branch.contains_command_subst()
177 }
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq)]
183enum TokKind {
184 Number(u64),
185 BasedExpansion { base: u32, expansion: Box<Expansion> },
186 Ident(String),
187 Expansion(Expansion),
188 LParen,
189 RParen,
190 LBracket,
191 RBracket,
192 Question,
193 Colon,
194 Op(BinOp),
195 Bang,
196 Tilde,
197}
198
199impl std::fmt::Display for TokKind {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 match self {
202 TokKind::Number(n) => write!(f, "{n}"),
203 TokKind::BasedExpansion { base, .. } => write!(f, "{base}#..."),
204 TokKind::Ident(name) => write!(f, "{name}"),
205 TokKind::Expansion(_) => write!(f, "$..."),
206 TokKind::LParen => write!(f, "("),
207 TokKind::RParen => write!(f, ")"),
208 TokKind::LBracket => write!(f, "["),
209 TokKind::RBracket => write!(f, "]"),
210 TokKind::Question => write!(f, "?"),
211 TokKind::Colon => write!(f, ":"),
212 TokKind::Op(op) => write!(f, "{}", op.symbol()),
213 TokKind::Bang => write!(f, "!"),
214 TokKind::Tilde => write!(f, "~"),
215 }
216 }
217}
218
219#[derive(Debug, Clone, PartialEq)]
220struct Tok {
221 kind: TokKind,
222 span: Range<usize>,
223}
224
225struct Tokenizer<'a> {
230 text: &'a str,
231 chars: Vec<(usize, char)>,
232 pos: usize,
233}
234
235impl<'a> Tokenizer<'a> {
236 fn new(text: &'a str) -> Self {
237 Self { text, chars: text.char_indices().collect(), pos: 0 }
238 }
239
240 fn byte_pos(&self) -> usize {
241 self.byte_at(self.pos)
242 }
243
244 fn byte_at(&self, char_idx: usize) -> usize {
250 self.chars.get(char_idx).map(|(b, _)| *b).unwrap_or(self.text.len())
251 }
252
253 fn peek(&self) -> Option<char> {
254 self.chars.get(self.pos).map(|(_, c)| *c)
255 }
256
257 fn peek_at(&self, n: usize) -> Option<char> {
258 self.chars.get(self.pos + n).map(|(_, c)| *c)
259 }
260
261 fn advance(&mut self) -> Option<char> {
262 let c = self.peek();
263 if c.is_some() {
264 self.pos += 1;
265 }
266 c
267 }
268
269 fn slice(&self, start: usize, end: usize) -> &'a str {
270 let end_byte = self.chars.get(end).map(|(b, _)| *b).unwrap_or(self.text.len());
271 let start_byte = self.chars.get(start).map(|(b, _)| *b).unwrap_or(self.text.len());
272 &self.text[start_byte..end_byte]
273 }
274
275 fn numeral_run_end(&self, from: usize) -> usize {
279 let mut end = from;
280 while self.chars.get(end).is_some_and(|(_, c)| c.is_ascii_alphanumeric() || *c == '_') {
281 end += 1;
282 }
283 end
284 }
285
286 fn skip_ws(&mut self) {
287 while matches!(self.peek(), Some(c) if c.is_whitespace()) {
288 self.pos += 1;
289 }
290 }
291
292 fn tokenize(mut self) -> Result<Vec<Tok>, ArithError> {
293 let mut out = Vec::new();
294 loop {
295 self.skip_ws();
296 let Some(c) = self.peek() else { break };
297 let start_byte = self.byte_pos();
298 let kind = match c {
299 '0'..='9' => self.lex_number(&out)?,
300 '$' => self.lex_dollar()?,
301 c if c.is_ascii_alphabetic() || c == '_' => self.lex_ident(),
302 '(' => { self.advance(); TokKind::LParen }
303 ')' => { self.advance(); TokKind::RParen }
304 '[' => { self.advance(); TokKind::LBracket }
305 ']' => { self.advance(); TokKind::RBracket }
306 '?' => { self.advance(); TokKind::Question }
307 ':' => { self.advance(); TokKind::Colon }
308 '~' => { self.advance(); TokKind::Tilde }
309 '!' => {
310 self.advance();
311 if self.peek() == Some('=') {
312 self.advance();
313 TokKind::Op(BinOp::Ne)
314 } else {
315 TokKind::Bang
316 }
317 }
318 '+' => {
319 self.advance();
320 self.reject_compound_or('+', start_byte, &out)?;
321 TokKind::Op(BinOp::Add)
322 }
323 '-' => {
324 self.advance();
325 self.reject_compound_or('-', start_byte, &out)?;
326 TokKind::Op(BinOp::Sub)
327 }
328 '*' => {
329 self.advance();
330 if self.peek() == Some('*') {
331 self.advance();
332 TokKind::Op(BinOp::Pow)
333 } else {
334 TokKind::Op(BinOp::Mul)
335 }
336 }
337 '/' => { self.advance(); TokKind::Op(BinOp::Div) }
338 '%' => { self.advance(); TokKind::Op(BinOp::Rem) }
339 '<' => {
340 self.advance();
341 if self.peek() == Some('<') {
342 self.advance();
343 if self.peek() == Some('<') {
344 return Err(ArithError::new(
345 "`<<<` is a here-string, not an operator; write `<<` to shift",
346 start_byte..self.byte_at(self.pos + 1),
347 ));
348 }
349 TokKind::Op(BinOp::Shl)
350 } else if self.peek() == Some('=') {
351 self.advance();
352 TokKind::Op(BinOp::Le)
353 } else {
354 TokKind::Op(BinOp::Lt)
355 }
356 }
357 '>' => {
358 self.advance();
359 if self.peek() == Some('>') {
360 self.advance();
361 if self.peek() == Some('>') {
362 return Err(ArithError::new(
363 "`>>>` is not an operator; write `>>`",
364 start_byte..self.byte_at(self.pos + 1),
365 ));
366 }
367 TokKind::Op(BinOp::Shr)
368 } else if self.peek() == Some('=') {
369 self.advance();
370 TokKind::Op(BinOp::Ge)
371 } else {
372 TokKind::Op(BinOp::Gt)
373 }
374 }
375 '=' => {
376 self.advance();
377 if self.peek() == Some('=') {
378 self.advance();
379 TokKind::Op(BinOp::Eq)
380 } else {
381 let end = self.text.len();
382 let rhs = self.text[self.byte_pos()..].trim();
383 if let Some((name, name_start)) = Self::preceding_name(&out) {
384 let source = &self.text[name_start..end];
385 return Err(ArithError::new(
386 format!(
387 "`{source}` assigns inside `$(( ))`; write `{name}={rhs}`, or `==` to compare"
388 ),
389 name_start..end,
390 ));
391 }
392 let source = &self.text[start_byte..end];
393 return Err(ArithError::new(
394 format!(
395 "`{source}` assigns inside `$(( ))`; write `name=rhs`, or `==` to compare"
396 ),
397 start_byte..end,
398 ));
399 }
400 }
401 '&' => {
402 self.advance();
403 if self.peek() == Some('&') {
404 self.advance();
405 TokKind::Op(BinOp::And)
406 } else {
407 TokKind::Op(BinOp::BitAnd)
408 }
409 }
410 '|' => {
411 self.advance();
412 if self.peek() == Some('|') {
413 self.advance();
414 TokKind::Op(BinOp::Or)
415 } else {
416 TokKind::Op(BinOp::BitOr)
417 }
418 }
419 '^' => { self.advance(); TokKind::Op(BinOp::BitXor) }
420 ',' => {
421 return Err(ArithError::new(
422 "`,` is not an operator; one expression per `$(( ))`",
423 start_byte..self.byte_at(self.pos + 1),
424 ));
425 }
426 other => {
427 return Err(ArithError::new(
428 format!("`{other}` cannot start a value"),
429 start_byte..self.byte_at(self.pos + 1),
430 ));
431 }
432 };
433 out.push(Tok { kind, span: start_byte..self.byte_pos() });
434 }
435 Ok(out)
436 }
437
438 fn preceding_name(out: &[Tok]) -> Option<(String, usize)> {
442 match out.last() {
443 Some(Tok { kind: TokKind::Ident(name), span }) => Some((name.clone(), span.start)),
444 _ => None,
445 }
446 }
447
448 fn leading_unary_sign(out: &[Tok]) -> Option<char> {
459 let sign = match out.last() {
460 Some(Tok { kind: TokKind::Op(BinOp::Sub), .. }) => '-',
461 Some(Tok { kind: TokKind::Op(BinOp::Add), .. }) => '+',
462 _ => return None,
463 };
464 let ends_an_expression = out
465 .len()
466 .checked_sub(2)
467 .and_then(|i| out.get(i))
468 .is_some_and(|tok| {
469 matches!(
470 tok.kind,
471 TokKind::Number(_)
472 | TokKind::Ident(_)
473 | TokKind::Expansion(_)
474 | TokKind::BasedExpansion { .. }
475 | TokKind::RParen
476 | TokKind::RBracket
477 )
478 });
479 if ends_an_expression { None } else { Some(sign) }
480 }
481
482 fn consume_following_name(&mut self) -> Option<(String, usize)> {
487 let start = self.pos;
488 if !matches!(self.peek(), Some(c) if c.is_ascii_alphabetic() || c == '_') {
489 return None;
490 }
491 while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric() || c == '_') {
492 self.pos += 1;
493 }
494 Some((self.slice(start, self.pos).to_string(), self.byte_pos()))
495 }
496
497 fn reject_compound_or(&mut self, sym: char, op_start_byte: usize, out: &[Tok]) -> Result<(), ArithError> {
502 let step = if sym == '+' { "+ 1" } else { "- 1" };
503 if self.peek() == Some(sym) {
504 self.advance();
505 let end = self.byte_pos();
506 if let Some((name, name_start)) = Self::preceding_name(out) {
507 let source = &self.text[name_start..end];
508 return Err(ArithError::new(
509 format!("`{source}` assigns inside `$(( ))`; write `{name}=$(({name} {step}))`"),
510 name_start..end,
511 ));
512 }
513 if let Some((name, name_end)) = self.consume_following_name() {
514 let source = &self.text[op_start_byte..name_end];
515 return Err(ArithError::new(
516 format!("`{source}` assigns inside `$(( ))`; write `{name}=$(({name} {step}))`"),
517 op_start_byte..name_end,
518 ));
519 }
520 let source = &self.text[op_start_byte..end];
521 return Err(ArithError::new(
522 format!("`{source}` assigns inside `$(( ))`; write `name=$((name {step}))`"),
523 op_start_byte..end,
524 ));
525 }
526 if self.peek() == Some('=') {
527 self.advance();
528 let end = self.text.len();
529 let rhs = self.text[self.byte_pos()..].trim();
530 if let Some((name, name_start)) = Self::preceding_name(out) {
531 let source = &self.text[name_start..end];
532 return Err(ArithError::new(
533 format!("`{source}` assigns inside `$(( ))`; write `{name}=$(({name} {sym} {rhs}))`"),
534 name_start..end,
535 ));
536 }
537 let source = &self.text[op_start_byte..end];
538 return Err(ArithError::new(
539 format!("`{source}` assigns inside `$(( ))`; write `name=$((name {sym} rhs))`"),
540 op_start_byte..end,
541 ));
542 }
543 Ok(())
544 }
545
546 fn lex_ident(&mut self) -> TokKind {
547 let start = self.pos;
548 while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric() || c == '_') {
549 self.pos += 1;
550 }
551 TokKind::Ident(self.slice(start, self.pos).to_string())
552 }
553
554 fn consume_digits(&mut self, base: u32, lit_start: usize) -> Result<(u64, usize), ArithError> {
558 let digits_start = self.pos;
559 let mut mag: u64 = 0;
560 while let Some(c) = self.peek() {
561 if c == '_' {
562 return Err(ArithError::new(
563 format!("`{}` contains `_`; remove it", self.slice(lit_start, self.numeral_run_end(self.pos))),
564 self.byte_at(lit_start)..self.byte_pos() + c.len_utf8(),
565 ));
566 }
567 if !c.is_ascii_alphanumeric() {
568 break;
569 }
570 let digit_val = match c {
571 '0'..='9' => c as u32 - '0' as u32,
572 'a'..='z' => c as u32 - 'a' as u32 + 10,
573 'A'..='Z' => c as u32 - 'A' as u32 + 10,
574 _ => unreachable!("ascii_alphanumeric"),
575 };
576 if digit_val >= base {
577 self.pos += 1;
578 return Err(ArithError::new(
579 format!(
580 "`{c}` is not a digit in `{}`; use digits valid for base {base}",
581 self.slice(lit_start, self.pos)
582 ),
583 self.byte_at(lit_start)..self.byte_pos(),
584 ));
585 }
586 mag = mag
587 .checked_mul(base as u64)
588 .and_then(|m| m.checked_add(digit_val as u64))
589 .ok_or_else(|| {
590 ArithError::new(
591 format!("`{}` {INTEGER_OUT_OF_RANGE}", self.slice(lit_start, self.numeral_run_end(self.pos))),
592 self.byte_at(lit_start)..self.byte_pos(),
593 )
594 })?;
595 self.pos += 1;
596 }
597 Ok((mag, digits_start))
598 }
599
600 fn consume_decimal_digits(&mut self, lit_start: usize) -> Result<(u64, usize), ArithError> {
606 let digits_start = self.pos;
607 let mut mag: u64 = 0;
608 while let Some(c) = self.peek() {
609 if c == '_' {
610 return Err(ArithError::new(
611 format!("`{}` contains `_`; remove it", self.slice(lit_start, self.numeral_run_end(self.pos))),
612 self.byte_at(lit_start)..self.byte_pos() + c.len_utf8(),
613 ));
614 }
615 if !c.is_ascii_digit() {
616 break;
617 }
618 let digit_val = c as u64 - '0' as u64;
619 mag = mag.checked_mul(10).and_then(|m| m.checked_add(digit_val)).ok_or_else(|| {
620 ArithError::new(
621 format!("`{}` {INTEGER_OUT_OF_RANGE}", self.slice(lit_start, self.numeral_run_end(self.pos))),
622 self.byte_at(lit_start)..self.byte_pos(),
623 )
624 })?;
625 self.pos += 1;
626 }
627 Ok((mag, digits_start))
628 }
629
630 fn lex_number(&mut self, out: &[Tok]) -> Result<TokKind, ArithError> {
631 let start = self.pos;
632
633 if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
635 self.pos += 2;
636 let prefix = self.slice(start, self.pos).to_string();
637 let (mag, digits_start) = self.consume_digits(16, start)?;
638 if digits_start == self.pos {
639 return Err(ArithError::new(
640 format!("`{prefix}` has no digits; add digits after `{prefix}`"),
641 self.byte_at(start)..self.byte_pos(),
642 ));
643 }
644 return Ok(TokKind::Number(mag));
645 }
646
647 if self.peek() == Some('0') && matches!(self.peek_at(1), Some('b' | 'B' | 'o' | 'O')) {
649 let kind_char = self.peek_at(1).unwrap_or('b');
650 self.pos += 2;
651 let digits_start = self.pos;
652 while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric()) {
653 self.pos += 1;
654 }
655 let digits = self.slice(digits_start, self.pos);
656 let full = self.slice(start, self.pos);
657 let (base, word) = if matches!(kind_char, 'b' | 'B') { (2, "binary") } else { (8, "octal") };
658 let sign = Self::leading_unary_sign(out).map(String::from).unwrap_or_default();
663 return Err(ArithError::new(
664 format!("`{full}` is not a kaish base spelling; write `{sign}{base}#{digits}` for {word}"),
665 self.byte_at(start)..self.byte_pos(),
666 ));
667 }
668
669 let (base_mag, digits_start) = self.consume_decimal_digits(start)?;
672 debug_assert!(digits_start == start);
673
674 let looks_like_float = (self.peek() == Some('.')
678 && matches!(self.peek_at(1), Some(c) if c.is_ascii_digit()))
679 || (matches!(self.peek(), Some('e' | 'E'))
680 && (matches!(self.peek_at(1), Some(c) if c.is_ascii_digit())
681 || (matches!(self.peek_at(1), Some('+' | '-'))
682 && matches!(self.peek_at(2), Some(c) if c.is_ascii_digit()))));
683 if looks_like_float {
684 if self.peek() == Some('.') {
685 self.pos += 1;
686 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
687 self.pos += 1;
688 }
689 }
690 if matches!(self.peek(), Some('e' | 'E')) {
691 self.pos += 1;
692 if matches!(self.peek(), Some('+' | '-')) {
693 self.pos += 1;
694 }
695 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
696 self.pos += 1;
697 }
698 }
699 let text = self.slice(start, self.pos);
700 return Err(ArithError::new(
701 format!("`{text}` is not an integer; arithmetic is integer-only"),
702 self.byte_at(start)..self.byte_pos(),
703 ));
704 }
705
706 if self.peek() == Some('#') {
707 let base_text = self.slice(start, self.pos);
708 if base_text.len() > 1 && base_text.starts_with('0') {
709 return Err(ArithError::new(
710 format!("`{base_text}` is not a base spelling; write the base without a leading zero"),
711 self.byte_at(start)..self.byte_pos(),
712 ));
713 }
714 self.advance(); if !(2..=36).contains(&base_mag) {
719 return Err(ArithError::new(
720 format!("base `{base_mag}` is outside 2..=36"),
721 self.byte_at(start)..self.byte_pos(),
722 ));
723 }
724 let base = base_mag as u32;
725 if let Some(sign @ ('+' | '-')) = self.peek() {
726 let sign_start = self.pos;
727 self.pos += 1;
728 while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric()) {
729 self.pos += 1;
730 }
731 let lit = self.slice(start, self.pos);
732 return Err(ArithError::new(
733 format!("`{lit}` puts `{sign}` after `#`; write `{sign}{base}#{}`", self.slice(sign_start + 1, self.pos)),
734 self.byte_at(start)..self.byte_pos(),
735 ));
736 }
737 if self.peek() == Some('$') {
738 let expansion = self.lex_expansion_body()?;
739 return Ok(TokKind::BasedExpansion { base, expansion: Box::new(expansion) });
740 }
741 let prefix = self.slice(start, self.pos).to_string();
742 let (mag, bdigits_start) = self.consume_digits(base, start)?;
743 if bdigits_start == self.pos {
744 return Err(ArithError::new(
745 format!("`{prefix}` has no digits; add digits after `{prefix}`"),
746 self.byte_at(start)..self.byte_pos(),
747 ));
748 }
749 return Ok(TokKind::Number(mag));
750 }
751
752 let text = self.slice(start, self.pos);
753 if crate::lexer::is_leading_zero_numeral(text) {
754 let sign_str = Self::leading_unary_sign(out).map(String::from).unwrap_or_default();
759 let decimal = leading_zero_decimal(text).unwrap_or_else(|| "0".to_string());
760 return Err(ArithError::new(
761 format!(
762 "`{text}` has a leading zero — kaish reads no octal; write `{sign_str}8#{}` for octal or `{sign_str}{decimal}` for decimal",
763 text.trim_start_matches('0')
764 ),
765 self.byte_at(start)..self.byte_pos(),
766 ));
767 }
768 Ok(TokKind::Number(base_mag))
769 }
770
771 fn lex_dollar(&mut self) -> Result<TokKind, ArithError> {
774 Ok(TokKind::Expansion(self.lex_expansion_body()?))
775 }
776
777 fn lex_expansion_body(&mut self) -> Result<Expansion, ArithError> {
780 let dollar_start = self.pos;
781 self.advance(); match self.peek() {
783 Some('?') => { self.advance(); Ok(Expansion::LastExitCode) }
784 Some('$') => { self.advance(); Ok(Expansion::CurrentPid) }
785 Some('(') if self.peek_at(1) == Some('(') => {
786 self.pos += 2; let inner_start = self.pos;
788 let close = self.skip_group(')', true, dollar_start, false)?;
789 let inner_text = self.slice(inner_start, close).to_string();
790 let inner = parse(&inner_text)?;
791 Ok(Expansion::Nested(Box::new(inner)))
792 }
793 Some('(') => {
794 self.advance(); let cmd_start = self.pos;
798 let close = self.skip_group(')', false, dollar_start, true)?;
799 let cmd_text = self.slice(cmd_start, close).to_string();
800 match crate::parser::parse(&cmd_text) {
801 Ok(program) => Ok(Expansion::CommandSubst(program.statements)),
802 Err(_) => Err(ArithError::new(
803 format!("syntax error in command substitution: $({cmd_text})"),
804 self.byte_at(dollar_start)..self.byte_pos(),
805 )),
806 }
807 }
808 Some('{') => {
809 self.advance(); let body_start = self.pos;
811 let close = self.skip_group('}', false, dollar_start, false)?;
812 let body = self.slice(body_start, close).to_string();
813 parse_braced_body(&body, self.byte_at(dollar_start)..self.byte_pos())
814 }
815 Some(c) if c.is_ascii_alphabetic() || c == '_' || c.is_ascii_digit() => {
820 let start = self.pos;
821 while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric() || c == '_') {
822 self.pos += 1;
823 }
824 Ok(Expansion::Var(self.slice(start, self.pos).to_string()))
825 }
826 _ => Err(ArithError::new(
827 format!("`{}` cannot start a value", self.slice(dollar_start, self.pos + 1)),
828 self.byte_at(dollar_start)..self.byte_at(self.pos + 1),
829 )),
830 }
831 }
832
833 fn skip_group(
849 &mut self,
850 close: char,
851 double: bool,
852 group_start: usize,
853 comments: bool,
854 ) -> Result<usize, ArithError> {
855 let open: char = if close == '}' { '{' } else { '(' };
856 let mut depth = 1i32;
857 loop {
858 match self.peek() {
859 None => {
860 let close_str = if double { "))" } else if close == '}' { "}" } else { ")" };
861 return Err(ArithError::new(
862 format!("`{}` has no closing `{close_str}`", self.slice(group_start, self.pos)),
863 self.byte_at(group_start)..self.byte_pos(),
864 ));
865 }
866 Some('\\') => {
867 self.pos += 1;
868 if self.peek().is_some() {
869 self.pos += 1;
870 }
871 }
872 Some('\'') => {
873 self.pos += 1;
874 while matches!(self.peek(), Some(c) if c != '\'') {
875 self.pos += 1;
876 }
877 if self.peek() == Some('\'') {
878 self.pos += 1;
879 }
880 }
882 Some('"') => {
883 self.pos += 1;
884 loop {
885 match self.peek() {
886 None => break,
887 Some('\\') => {
888 self.pos += 1;
889 if self.peek().is_some() {
890 self.pos += 1;
891 }
892 }
893 Some('"') => {
894 self.pos += 1;
895 break;
896 }
897 Some(_) => self.pos += 1,
898 }
899 }
900 }
901 Some('#') => {
902 if comments {
903 let prev = self
907 .pos
908 .checked_sub(1)
909 .and_then(|i| self.chars.get(i))
910 .map(|(_, c)| *c);
911 if prev.is_none() || prev.is_some_and(crate::lexer::opens_a_word) {
912 while matches!(self.peek(), Some(c) if c != '\n' && c != '\r') {
913 self.pos += 1;
914 }
915 } else {
916 self.pos += 1; }
918 } else {
919 self.pos += 1; }
921 }
922 Some('<') if comments && self.peek_at(1) == Some('<') && self.peek_at(2) != Some('<') => {
923 let total_len = self.text.len();
929 crate::lexer::skip_heredoc_body(&self.chars, &mut self.pos, total_len).map_err(
930 |e| ArithError::new(e.token.to_string(), group_start..self.byte_pos()),
931 )?;
932 }
933 Some('$') => {
934 let nested_start = self.pos;
935 match self.peek_at(1) {
936 Some('(') if self.peek_at(2) == Some('(') => {
937 self.pos += 3;
938 self.skip_group(')', true, nested_start, false)?;
939 }
940 Some('(') => {
941 self.pos += 2;
942 self.skip_group(')', false, nested_start, true)?;
943 }
944 Some('{') => {
945 self.pos += 2;
946 self.skip_group('}', false, nested_start, false)?;
947 }
948 _ => self.pos += 1, }
950 }
951 Some('(') if self.peek_at(1) == Some('(') => {
952 let nested_start = self.pos;
957 self.pos += 2;
958 self.skip_group(')', true, nested_start, false)?;
959 }
960 Some('(') => {
961 let nested_start = self.pos;
965 self.pos += 1;
966 self.skip_group(')', false, nested_start, comments)?;
967 }
968 Some(c) if c == open => {
969 depth += 1;
970 self.pos += 1;
971 }
972 Some(c) if c == close => {
973 if double {
974 if self.peek_at(1) == Some(close) {
975 let close_pos = self.pos;
976 self.pos += 2;
977 return Ok(close_pos);
978 }
979 self.pos += 1;
981 } else {
982 depth -= 1;
983 self.pos += 1;
984 if depth == 0 {
985 return Ok(self.pos - 1);
986 }
987 }
988 }
989 Some(_) => {
990 self.pos += 1;
991 }
992 }
993 }
994 }
995}
996
997use crate::lexer::INTEGER_OUT_OF_RANGE;
999
1000fn split_name_and_brackets(text: &str) -> Option<(String, String)> {
1001 let bracket_start = text.find('[').unwrap_or(text.len());
1002 let name = &text[..bracket_start];
1003 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1004 || name.chars().next().is_some_and(|c| c.is_ascii_digit())
1005 {
1006 return None;
1007 }
1008 Some((name.to_string(), text[bracket_start..].to_string()))
1009}
1010
1011fn parse_braced_body(body: &str, span: Range<usize>) -> Result<Expansion, ArithError> {
1012 if body == "?" {
1013 return Ok(Expansion::LastExitCode);
1014 }
1015 if body == "$" {
1016 return Ok(Expansion::CurrentPid);
1017 }
1018 let bytes = body.as_bytes();
1019 let mut depth = 0i32;
1020 let mut default_at = None;
1021 let mut i = 0;
1022 while i < bytes.len() {
1023 match bytes[i] {
1024 b'[' => depth += 1,
1025 b']' => depth -= 1,
1026 b':' if depth == 0 && bytes.get(i + 1) == Some(&b'-') => {
1027 default_at = Some(i);
1028 break;
1029 }
1030 _ => {}
1031 }
1032 i += 1;
1033 }
1034 if let Some(idx) = default_at {
1035 let root_and_sub = &body[..idx];
1036 let default = body[idx + 2..].to_string();
1037 let Some((root, brackets)) = split_name_and_brackets(root_and_sub) else {
1038 return Err(ArithError::new(format!("`{{{body}}}` is not valid inside `$(( ))`"), span));
1039 };
1040 return Ok(Expansion::BracedDefault { root, brackets, default });
1041 }
1042 let Some((root, brackets)) = split_name_and_brackets(body) else {
1043 return Err(ArithError::new(format!("`{{{body}}}` is not valid inside `$(( ))`"), span));
1044 };
1045 if brackets.is_empty() {
1046 Ok(Expansion::Var(root))
1047 } else {
1048 Ok(Expansion::BracedPath { root, brackets })
1049 }
1050}
1051
1052fn tokenize(text: &str) -> Result<Vec<Tok>, ArithError> {
1053 Tokenizer::new(text).tokenize()
1054}
1055
1056struct Parser {
1063 toks: Vec<Tok>,
1064 pos: usize,
1065 depth: usize,
1066 end: usize,
1067 text: String,
1070}
1071
1072impl Parser {
1073 fn new(toks: Vec<Tok>, end: usize, text: &str) -> Self {
1074 Self { toks, pos: 0, depth: 0, end, text: text.to_string() }
1075 }
1076
1077 fn missing_right_operand(&self, op: &str, op_span: &Range<usize>) -> ArithError {
1083 ArithError::new(
1084 format!(
1085 "`{op}` has no right operand in `{}`; add an integer expression after `{op}`",
1086 self.text
1087 ),
1088 op_span.clone(),
1089 )
1090 }
1091
1092 fn missing_operand(&self, op: &str, op_span: &Range<usize>) -> ArithError {
1095 ArithError::new(
1096 format!("`{op}` has no operand; add an integer expression after `{op}`"),
1097 op_span.clone(),
1098 )
1099 }
1100
1101 fn peek(&self) -> Option<&TokKind> {
1102 self.toks.get(self.pos).map(|t| &t.kind)
1103 }
1104
1105 fn peek_span(&self) -> Range<usize> {
1106 self.toks.get(self.pos).map(|t| t.span.clone()).unwrap_or(self.end..self.end)
1107 }
1108
1109 fn advance(&mut self) -> Option<Tok> {
1110 let t = self.toks.get(self.pos).cloned();
1111 if t.is_some() {
1112 self.pos += 1;
1113 }
1114 t
1115 }
1116
1117 fn enter(&mut self) -> Result<(), ArithError> {
1118 self.depth += 1;
1119 if self.depth > MAX_DEPTH {
1120 return Err(ArithError::new("more than 256 nested arithmetic forms", self.peek_span()));
1121 }
1122 Ok(())
1123 }
1124
1125 fn leave(&mut self) {
1126 self.depth -= 1;
1127 }
1128
1129 fn left_assoc(
1130 &mut self,
1131 ops: &[BinOp],
1132 mut next: impl FnMut(&mut Self) -> Result<ArithExpr, ArithError>,
1133 ) -> Result<ArithExpr, ArithError> {
1134 let mut left = next(self)?;
1135 loop {
1136 let matched = ops.iter().copied().find(|op| self.peek() == Some(&TokKind::Op(*op)));
1137 let Some(op) = matched else { break };
1138 let op_span = self.peek_span();
1139 self.pos += 1;
1140 if self.pos >= self.toks.len() {
1141 return Err(self.missing_right_operand(op.symbol(), &op_span));
1142 }
1143 let right = next(self)?;
1144 left = ArithExpr::Binary { op, left: Box::new(left), right: Box::new(right) };
1145 }
1146 Ok(left)
1147 }
1148
1149 fn parse_conditional(&mut self) -> Result<ArithExpr, ArithError> {
1150 self.enter()?;
1151 let cond = self.parse_logical_or()?;
1152 let result = if self.peek() == Some(&TokKind::Question) {
1153 self.pos += 1;
1154 let then_branch = self.parse_conditional()?;
1155 match self.peek() {
1156 Some(&TokKind::Colon) => self.pos += 1,
1157 _ => {
1158 return Err(ArithError::new("`?` has no matching `:`", self.peek_span()));
1159 }
1160 }
1161 let else_branch = self.parse_conditional()?;
1162 ArithExpr::Ternary {
1163 cond: Box::new(cond),
1164 then_branch: Box::new(then_branch),
1165 else_branch: Box::new(else_branch),
1166 }
1167 } else {
1168 cond
1169 };
1170 self.leave();
1171 Ok(result)
1172 }
1173
1174 fn parse_logical_or(&mut self) -> Result<ArithExpr, ArithError> {
1175 self.left_assoc(&[BinOp::Or], Self::parse_logical_and)
1176 }
1177
1178 fn parse_logical_and(&mut self) -> Result<ArithExpr, ArithError> {
1179 self.left_assoc(&[BinOp::And], Self::parse_bitor)
1180 }
1181
1182 fn parse_bitor(&mut self) -> Result<ArithExpr, ArithError> {
1183 self.left_assoc(&[BinOp::BitOr], Self::parse_bitxor)
1184 }
1185
1186 fn parse_bitxor(&mut self) -> Result<ArithExpr, ArithError> {
1187 self.left_assoc(&[BinOp::BitXor], Self::parse_bitand)
1188 }
1189
1190 fn parse_bitand(&mut self) -> Result<ArithExpr, ArithError> {
1191 self.left_assoc(&[BinOp::BitAnd], Self::parse_equality)
1192 }
1193
1194 fn parse_equality(&mut self) -> Result<ArithExpr, ArithError> {
1195 self.left_assoc(&[BinOp::Eq, BinOp::Ne], Self::parse_relational)
1196 }
1197
1198 fn parse_relational(&mut self) -> Result<ArithExpr, ArithError> {
1199 self.left_assoc(&[BinOp::Le, BinOp::Ge, BinOp::Lt, BinOp::Gt], Self::parse_shift)
1200 }
1201
1202 fn parse_shift(&mut self) -> Result<ArithExpr, ArithError> {
1203 self.left_assoc(&[BinOp::Shl, BinOp::Shr], Self::parse_additive)
1204 }
1205
1206 fn parse_additive(&mut self) -> Result<ArithExpr, ArithError> {
1207 self.left_assoc(&[BinOp::Add, BinOp::Sub], Self::parse_multiplicative)
1208 }
1209
1210 fn parse_multiplicative(&mut self) -> Result<ArithExpr, ArithError> {
1211 self.left_assoc(&[BinOp::Mul, BinOp::Div, BinOp::Rem], Self::parse_power)
1212 }
1213
1214 fn parse_power(&mut self) -> Result<ArithExpr, ArithError> {
1215 let base = self.parse_unary()?;
1216 if self.peek() == Some(&TokKind::Op(BinOp::Pow)) {
1217 let op_span = self.peek_span();
1218 self.pos += 1;
1219 if self.pos >= self.toks.len() {
1220 return Err(self.missing_right_operand("**", &op_span));
1221 }
1222 self.enter()?;
1223 let exp = self.parse_power()?;
1224 self.leave();
1225 Ok(ArithExpr::Binary { op: BinOp::Pow, left: Box::new(base), right: Box::new(exp) })
1226 } else {
1227 Ok(base)
1228 }
1229 }
1230
1231 const MIN_MAGNITUDE: u64 = 9_223_372_036_854_775_808;
1235
1236 fn parse_unary(&mut self) -> Result<ArithExpr, ArithError> {
1237 self.enter()?;
1238 let result = match self.peek() {
1239 Some(&TokKind::Op(BinOp::Sub)) => {
1240 let op_span = self.peek_span();
1241 self.pos += 1;
1242 if self.pos >= self.toks.len() {
1243 self.leave();
1244 return Err(self.missing_operand("-", &op_span));
1245 }
1246 if let Some(&TokKind::Number(mag)) = self.peek() {
1247 if mag == Self::MIN_MAGNITUDE {
1248 self.pos += 1;
1249 self.leave();
1250 return Ok(ArithExpr::Int(i64::MIN));
1251 }
1252 }
1253 let operand = self.parse_unary()?;
1254 ArithExpr::Unary { op: UnOp::Neg, operand: Box::new(operand) }
1255 }
1256 Some(&TokKind::Op(BinOp::Add)) => {
1257 let op_span = self.peek_span();
1258 self.pos += 1;
1259 if self.pos >= self.toks.len() {
1260 self.leave();
1261 return Err(self.missing_operand("+", &op_span));
1262 }
1263 self.parse_unary()?
1264 }
1265 Some(&TokKind::Bang) => {
1266 let op_span = self.peek_span();
1267 self.pos += 1;
1268 if self.pos >= self.toks.len() {
1269 self.leave();
1270 return Err(self.missing_operand("!", &op_span));
1271 }
1272 let operand = self.parse_unary()?;
1273 ArithExpr::Unary { op: UnOp::Not, operand: Box::new(operand) }
1274 }
1275 Some(&TokKind::Tilde) => {
1276 let op_span = self.peek_span();
1277 self.pos += 1;
1278 if self.pos >= self.toks.len() {
1279 self.leave();
1280 return Err(self.missing_operand("~", &op_span));
1281 }
1282 let operand = self.parse_unary()?;
1283 ArithExpr::Unary { op: UnOp::BitNot, operand: Box::new(operand) }
1284 }
1285 _ => self.parse_primary()?,
1286 };
1287 self.leave();
1288 Ok(result)
1289 }
1290
1291 fn parse_primary(&mut self) -> Result<ArithExpr, ArithError> {
1292 let span = self.peek_span();
1293 match self.advance().map(|t| t.kind) {
1294 Some(TokKind::Number(mag)) => int_from_magnitude(mag, false, span),
1295 Some(TokKind::BasedExpansion { base, expansion }) => {
1296 Ok(ArithExpr::BasedExpansion { base, expansion })
1297 }
1298 Some(TokKind::Expansion(e)) => Ok(ArithExpr::Expansion(e)),
1299 Some(TokKind::Ident(name)) => {
1300 if self.peek() == Some(&TokKind::LBracket) {
1301 let mut indices = Vec::new();
1302 while self.peek() == Some(&TokKind::LBracket) {
1303 self.pos += 1;
1304 self.enter()?;
1305 let index = self.parse_conditional()?;
1306 self.leave();
1307 match self.peek() {
1308 Some(&TokKind::RBracket) => self.pos += 1,
1309 _ => {
1310 return Err(ArithError::new(
1311 "`[` has no matching `]`",
1312 self.peek_span(),
1313 ));
1314 }
1315 }
1316 indices.push(index);
1317 }
1318 Ok(ArithExpr::Subscript { root: name, indices })
1319 } else {
1320 Ok(ArithExpr::Expansion(Expansion::Var(name)))
1321 }
1322 }
1323 Some(TokKind::LParen) => {
1324 self.enter()?;
1325 if self.peek() == Some(&TokKind::RParen) {
1326 self.leave();
1327 self.pos += 1;
1328 return Err(ArithError::new("`()` has no expression", span));
1329 }
1330 let inner = self.parse_conditional()?;
1331 self.leave();
1332 match self.peek() {
1333 Some(&TokKind::RParen) => {
1334 self.pos += 1;
1335 Ok(inner)
1336 }
1337 _ => Err(ArithError::new("`(` has no closing `)`", span)),
1338 }
1339 }
1340 Some(TokKind::RParen) => Err(ArithError::new("`)` has no matching `(`", span)),
1341 Some(other) => Err(ArithError::new(format!("`{other}` cannot start a value"), span)),
1342 None => Err(ArithError::new("`$(( ))` has no expression; write a number or an expression", span)),
1343 }
1344 }
1345}
1346
1347fn int_from_magnitude(mag: u64, negative: bool, span: Range<usize>) -> Result<ArithExpr, ArithError> {
1348 let max = if negative { Parser::MIN_MAGNITUDE } else { i64::MAX as u64 };
1349 if mag > max {
1350 return Err(ArithError::new(format!("`{mag}` {INTEGER_OUT_OF_RANGE}"), span));
1351 }
1352 if negative && mag == Parser::MIN_MAGNITUDE {
1353 return Ok(ArithExpr::Int(i64::MIN));
1354 }
1355 Ok(ArithExpr::Int(if negative { -(mag as i64) } else { mag as i64 }))
1356}
1357
1358pub(crate) fn parse(text: &str) -> Result<ArithExpr, ArithError> {
1359 let toks = tokenize(text)?;
1360 if toks.is_empty() {
1361 return Err(ArithError::new(
1362 "`$(( ))` has no expression; write a number or an expression",
1363 0..text.len(),
1364 ));
1365 }
1366 let end = text.len();
1367 let mut parser = Parser::new(toks, end, text);
1368 let expr = parser.parse_conditional()?;
1369 if let Some(extra) = parser.peek() {
1370 if extra == &TokKind::RParen {
1371 return Err(ArithError::new(
1372 format!("`)` has no matching `(` in `{text}`"),
1373 parser.peek_span(),
1374 ));
1375 }
1376 return Err(ArithError::new(format!("`{extra}` is not valid inside `$(( ))`"), parser.peek_span()));
1377 }
1378 Ok(expr)
1379}
1380
1381fn shift_count_error(count: i64) -> ArithError {
1386 ArithError::new(format!("shift count `{count}` is outside 0..=63"), 0..0)
1387}
1388
1389fn overflow(l: i64, op: BinOp, r: i64) -> ArithError {
1390 ArithError::new(format!("`{l} {} {r}` does not fit in a 64-bit integer", op.symbol()), 0..0)
1391}
1392
1393pub(crate) fn apply_binary(op: BinOp, l: i64, r: i64) -> Result<i64, ArithError> {
1394 match op {
1395 BinOp::Add => l.checked_add(r).ok_or_else(|| overflow(l, op, r)),
1396 BinOp::Sub => l.checked_sub(r).ok_or_else(|| overflow(l, op, r)),
1397 BinOp::Mul => l.checked_mul(r).ok_or_else(|| overflow(l, op, r)),
1398 BinOp::Div => {
1399 if r == 0 {
1400 return Err(ArithError::new(format!("`{l} / 0` divides by zero"), 0..0));
1401 }
1402 l.checked_div(r).ok_or_else(|| overflow(l, op, r))
1403 }
1404 BinOp::Rem => {
1405 if r == 0 {
1406 return Err(ArithError::new(format!("`{l} % 0` divides by zero"), 0..0));
1407 }
1408 if r == -1 {
1410 return Ok(0);
1411 }
1412 l.checked_rem(r).ok_or_else(|| overflow(l, op, r))
1413 }
1414 BinOp::Pow => {
1415 if r < 0 {
1416 return Err(ArithError::new(format!("exponent `{r}` is negative; use 0 or greater"), 0..0));
1417 }
1418 match l {
1419 0 => Ok(if r == 0 { 1 } else { 0 }),
1420 1 => Ok(1),
1421 -1 => Ok(if r % 2 == 0 { 1 } else { -1 }),
1422 _ => {
1423 if r > u32::MAX as i64 {
1424 return Err(overflow(l, op, r));
1425 }
1426 l.checked_pow(r as u32).ok_or_else(|| overflow(l, op, r))
1427 }
1428 }
1429 }
1430 BinOp::Shl => {
1431 if !(0..=63).contains(&r) {
1432 return Err(shift_count_error(r));
1433 }
1434 let factor: i128 = 1i128 << r;
1435 let result = (l as i128) * factor;
1436 i64::try_from(result).map_err(|_| overflow(l, op, r))
1437 }
1438 BinOp::Shr => {
1439 if !(0..=63).contains(&r) {
1440 return Err(shift_count_error(r));
1441 }
1442 Ok(l >> r)
1443 }
1444 BinOp::Lt => Ok((l < r) as i64),
1445 BinOp::Le => Ok((l <= r) as i64),
1446 BinOp::Gt => Ok((l > r) as i64),
1447 BinOp::Ge => Ok((l >= r) as i64),
1448 BinOp::Eq => Ok((l == r) as i64),
1449 BinOp::Ne => Ok((l != r) as i64),
1450 BinOp::BitAnd => Ok(l & r),
1451 BinOp::BitXor => Ok(l ^ r),
1452 BinOp::BitOr => Ok(l | r),
1453 BinOp::And | BinOp::Or => unreachable!("short-circuit ops are handled by the tree walk"),
1454 }
1455}
1456
1457pub(crate) fn apply_unary(op: UnOp, v: i64) -> Result<i64, ArithError> {
1458 match op {
1459 UnOp::Neg => v
1460 .checked_neg()
1461 .ok_or_else(|| ArithError::new(format!("`-{v}` does not fit in a 64-bit integer"), 0..0)),
1462 UnOp::Not => Ok(if v == 0 { 1 } else { 0 }),
1463 UnOp::BitNot => Ok(!v),
1464 }
1465}
1466
1467fn truthy(v: i64) -> bool {
1468 v != 0
1469}
1470
1471fn expression_like(s: &str) -> bool {
1476 let bytes = s.as_bytes();
1477 bytes.iter().enumerate().any(|(i, &b)| match b {
1478 b'+' | b'-' => i > 0,
1479 b'*' | b'/' | b'%' | b'<' | b'>' | b'=' | b'&' | b'|' | b'^' | b'!' | b'~' | b'?' | b':' | b'(' | b')' => true,
1480 _ => false,
1481 })
1482}
1483
1484enum Numeral {
1489 Ok(i64),
1490 Empty,
1491 ExpressionLike,
1492 LeadingZero { neg: bool, digits: String },
1496 NotANumber,
1497 NotANumberWithFix(String),
1502 OutOfRange,
1503}
1504
1505fn read_numeral(text: &str) -> Numeral {
1506 let trimmed = text.trim();
1507 if trimmed.is_empty() {
1508 return Numeral::Empty;
1509 }
1510 if expression_like(trimmed) {
1511 return Numeral::ExpressionLike;
1512 }
1513 let (neg, digits) = match trimmed.strip_prefix('-') {
1514 Some(rest) => (true, rest),
1515 None => (false, trimmed.strip_prefix('+').unwrap_or(trimmed)),
1516 };
1517 if digits.is_empty() {
1518 return Numeral::NotANumber;
1519 }
1520 if crate::lexer::is_leading_zero_numeral(digits) {
1521 return Numeral::LeadingZero { neg, digits: digits.to_string() };
1522 }
1523 match tokenize(digits) {
1524 Ok(toks) if toks.len() == 1 => match &toks[0].kind {
1525 TokKind::Number(mag) => match int_from_magnitude(*mag, neg, 0..0) {
1526 Ok(ArithExpr::Int(n)) => Numeral::Ok(n),
1527 Ok(_) => unreachable!("int_from_magnitude only returns Int"),
1528 Err(_) => Numeral::OutOfRange,
1529 },
1530 _ => Numeral::NotANumber,
1531 },
1532 Ok(_) => Numeral::NotANumber,
1533 Err(e) => Numeral::NotANumberWithFix(e.message),
1534 }
1535}
1536
1537fn parse_numeric_string(s: &str, name: &str) -> Result<i64, ArithError> {
1538 match read_numeral(s) {
1539 Numeral::Ok(n) => Ok(n),
1540 Numeral::Empty | Numeral::ExpressionLike => Err(ArithError::new(
1541 format!(
1542 "`{name}` holds `{s}`; a variable is a value, not an expression — write it inside `$(( ))`"
1543 ),
1544 0..0,
1545 )),
1546 Numeral::LeadingZero { neg: false, .. } => Err(ArithError::new(
1553 format!(
1554 "`{name}` holds `{s}` (leading zero) — kaish reads no octal; write `10#${name}` for decimal or `8#${name}` for octal"
1555 ),
1556 0..0,
1557 )),
1558 Numeral::LeadingZero { neg: true, digits } => Err(ArithError::new(
1559 format!(
1560 "`{name}` holds `{s}` (leading zero) — kaish reads no octal; write `-10#{digits}` for decimal or `-8#{digits}` for octal"
1561 ),
1562 0..0,
1563 )),
1564 Numeral::NotANumber => {
1565 Err(ArithError::new(format!("`{name}` holds `{s}`, which is not a number"), 0..0))
1566 }
1567 Numeral::NotANumberWithFix(fix) => {
1568 Err(ArithError::new(format!("`{name}` holds `{s}`; {fix}"), 0..0))
1569 }
1570 Numeral::OutOfRange => {
1571 Err(ArithError::new(format!("`{name}` holds `{s}`, outside the 64-bit range"), 0..0))
1572 }
1573 }
1574}
1575
1576pub(crate) fn parse_command_output(text: &str, cmd: &str) -> Result<i64, ArithError> {
1579 match read_numeral(text) {
1580 Numeral::Ok(n) => Ok(n),
1581 Numeral::Empty => Err(ArithError::new(
1582 format!("`{cmd}` printed nothing; the command must print one integer"),
1583 0..0,
1584 )),
1585 Numeral::ExpressionLike
1586 | Numeral::NotANumber
1587 | Numeral::NotANumberWithFix(_)
1588 | Numeral::LeadingZero { .. }
1589 | Numeral::OutOfRange => Err(ArithError::new(
1590 format!("`{cmd}` printed `{text}`; the command must print one integer"),
1591 0..0,
1592 )),
1593 }
1594}
1595
1596pub(crate) fn value_to_arith(value: &Value, name: &str) -> Result<i64, ArithError> {
1597 match value {
1598 Value::Int(n) => Ok(*n),
1599 Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
1600 Value::Float(f) => {
1601 if !f.is_finite() || f.fract() != 0.0 {
1602 Err(ArithError::new(format!("`{name}` holds `{f}`; arithmetic is integer-only"), 0..0))
1603 } else if *f < i64::MIN as f64 || *f >= 9_223_372_036_854_775_808.0 {
1612 Err(ArithError::new(format!("`{name}` holds `{f}`, outside the 64-bit range"), 0..0))
1613 } else {
1614 Ok(*f as i64)
1615 }
1616 }
1617 Value::String(s) => parse_numeric_string(s, name),
1618 Value::Null => Err(ArithError::new(format!("`{name}` is null; set it to an integer"), 0..0)),
1619 Value::Json(serde_json::Value::Array(_)) => {
1620 Err(ArithError::new(format!("`{name}` is a list; index a number field"), 0..0))
1621 }
1622 Value::Json(serde_json::Value::Object(_)) => {
1623 Err(ArithError::new(format!("`{name}` is a record; index a number field"), 0..0))
1624 }
1625 Value::Json(_) => Err(ArithError::new(
1626 format!("`{name}` holds `{}`, which is not a number", value_to_string(value)),
1627 0..0,
1628 )),
1629 Value::Bytes(b) => {
1630 Err(ArithError::new(format!("`{name}` holds {} bytes; decode them first", b.len()), 0..0))
1631 }
1632 }
1633}
1634
1635pub(crate) fn unset_error(name: &str) -> ArithError {
1636 let message = match name {
1637 "RANDOM" => "`$RANDOM` has no value in kaish; write `$(random --max 100)`".to_string(),
1638 "SECONDS" => {
1639 "`$SECONDS` has no value in kaish; write `start=$(date +%s)` and `$(( $(date +%s) - start ))`"
1640 .to_string()
1641 }
1642 _ => format!("`{name}` is unset; set it before `$(( ))` or write `${{{name}:-0}}`"),
1643 };
1644 ArithError::new(message, 0..0)
1645}
1646
1647pub(crate) fn resolve_var_sync(scope: &Scope, name: &str) -> Result<i64, ArithError> {
1648 if let Ok(index) = name.parse::<usize>() {
1651 return match scope.get_positional(index) {
1652 Some(s) => parse_numeric_string(s, name),
1653 None => Err(unset_error(name)),
1654 };
1655 }
1656 match scope.get(name) {
1657 Some(value) => value_to_arith(value, name),
1658 None => Err(unset_error(name)),
1659 }
1660}
1661
1662pub(crate) fn braced_path_value(scope: &Scope, root: &str, brackets: &str) -> Result<Value, ArithError> {
1663 let raw = format!("${{{root}{brackets}}}");
1664 let path: VarPath = crate::parser::parse_varpath(&raw);
1665 scope.resolve_path(&path).map_err(|e| match e {
1666 crate::interpreter::PathError::UndefinedRoot(_) => unset_error(root),
1667 crate::interpreter::PathError::Absence(msg) | crate::interpreter::PathError::Shape(msg) => {
1668 ArithError::new(msg, 0..0)
1669 }
1670 })
1671}
1672
1673pub(crate) fn braced_default_operand(
1685 scope: &Scope,
1686 root: &str,
1687 brackets: &str,
1688) -> Result<Option<Value>, ArithError> {
1689 let resolved: Result<Value, PathError> = if brackets.is_empty() {
1690 scope
1691 .get(root)
1692 .cloned()
1693 .ok_or_else(|| PathError::UndefinedRoot(root.to_string()))
1694 } else {
1695 let raw = format!("${{{root}{brackets}}}");
1696 let path: VarPath = crate::parser::parse_varpath(&raw);
1697 scope.resolve_path(&path)
1698 };
1699 match resolved {
1700 Ok(v) if value_defaults_on_emptiness(&v) => Ok(None),
1701 Ok(v) => Ok(Some(v)),
1702 Err(PathError::UndefinedRoot(_)) | Err(PathError::Absence(_)) => Ok(None),
1703 Err(PathError::Shape(msg)) => Err(ArithError::new(msg, 0..0)),
1704 }
1705}
1706
1707fn subscript_path(root: &str, indices: &[i64]) -> VarPath {
1708 let mut raw = format!("${{{root}");
1709 for idx in indices {
1710 raw.push('[');
1711 raw.push_str(&idx.to_string());
1712 raw.push(']');
1713 }
1714 raw.push('}');
1715 crate::parser::parse_varpath(&raw)
1716}
1717
1718pub(crate) fn resolve_subscript_sync(scope: &Scope, root: &str, indices: &[i64]) -> Result<i64, ArithError> {
1719 let path = subscript_path(root, indices);
1720 let value = scope.resolve_path(&path).map_err(|e| match e {
1721 crate::interpreter::PathError::UndefinedRoot(_) => unset_error(root),
1722 crate::interpreter::PathError::Absence(msg) | crate::interpreter::PathError::Shape(msg) => {
1723 ArithError::new(msg, 0..0)
1724 }
1725 })?;
1726 value_to_arith(&value, root)
1727}
1728
1729pub(crate) fn expansion_label(e: &Expansion) -> (String, &'static str) {
1734 match e {
1735 Expansion::Var(name) => (name.clone(), "holds"),
1736 Expansion::BracedPath { root, .. } | Expansion::BracedDefault { root, .. } => {
1737 (root.clone(), "holds")
1738 }
1739 Expansion::LastExitCode => ("$?".to_string(), "holds"),
1740 Expansion::CurrentPid => ("$$".to_string(), "holds"),
1741 Expansion::CommandSubst(_) => ("$(...)".to_string(), "printed"),
1742 Expansion::Nested(_) => ("$((...))".to_string(), "holds"),
1743 }
1744}
1745
1746pub(crate) fn based_value(base: u32, text: &str, label: &str, verb: &str, negative: bool) -> Result<i64, ArithError> {
1766 let trimmed = text.trim();
1767 if let Some(stripped) = trimmed.strip_prefix('-').or_else(|| trimmed.strip_prefix('+')) {
1768 let sign = &trimmed[..1];
1769 return Err(ArithError::new(
1770 format!(
1771 "`{label}` {verb} `{text}`; the digits after `#` take no sign — write `{sign}{base}#{stripped}`"
1772 ),
1773 0..0,
1774 ));
1775 }
1776 let digits = trimmed;
1777 if digits.is_empty() {
1778 return Err(ArithError::new(format!("`{text}` has no digits"), 0..0));
1779 }
1780 let mut mag: u64 = 0;
1781 for c in digits.chars() {
1782 if !c.is_ascii_alphanumeric() {
1783 return Err(ArithError::new(format!("`{c}` is not a digit in `{text}`; use digits valid for base {base}"), 0..0));
1784 }
1785 let digit_val = match c {
1786 '0'..='9' => c as u32 - '0' as u32,
1787 'a'..='z' => c as u32 - 'a' as u32 + 10,
1788 'A'..='Z' => c as u32 - 'A' as u32 + 10,
1789 _ => unreachable!(),
1790 };
1791 if digit_val >= base {
1792 return Err(ArithError::new(format!("`{c}` is not a digit in `{text}`; use digits valid for base {base}"), 0..0));
1793 }
1794 mag = mag
1795 .checked_mul(base as u64)
1796 .and_then(|m| m.checked_add(digit_val as u64))
1797 .ok_or_else(|| ArithError::new(format!("`{text}` {INTEGER_OUT_OF_RANGE}"), 0..0))?;
1798 }
1799 match int_from_magnitude(mag, negative, 0..0)? {
1800 ArithExpr::Int(n) => Ok(n),
1801 _ => unreachable!(),
1802 }
1803}
1804
1805fn needs_async(what: &str) -> ArithError {
1817 ArithError::new(
1818 format!("`{what}` must be resolved by the async evaluator before sync evaluation"),
1819 0..0,
1820 )
1821}
1822
1823fn resolve_expansion_sync(e: &Expansion, scope: &Scope) -> Result<i64, ArithError> {
1824 match e {
1825 Expansion::Var(name) => resolve_var_sync(scope, name),
1826 Expansion::BracedPath { root, brackets } => {
1827 let v = braced_path_value(scope, root, brackets)?;
1828 value_to_arith(&v, root)
1829 }
1830 Expansion::BracedDefault { root, brackets, default } => {
1831 match braced_default_operand(scope, root, brackets)? {
1832 None => {
1833 let default_expr = parse(default)?;
1834 eval_sync(&default_expr, scope)
1835 }
1836 Some(v) => value_to_arith(&v, root),
1837 }
1838 }
1839 Expansion::LastExitCode => Ok(scope.last_result().code),
1840 Expansion::CurrentPid => Ok(scope.pid() as i64),
1841 Expansion::CommandSubst(_) => Err(needs_async("$(...)")),
1842 Expansion::Nested(inner) => eval_sync(inner, scope),
1843 }
1844}
1845
1846pub(crate) fn expansion_text_sync(e: &Expansion, scope: &Scope) -> Result<String, ArithError> {
1851 match e {
1852 Expansion::Var(name) => {
1853 if let Ok(index) = name.parse::<usize>() {
1854 return match scope.get_positional(index) {
1855 Some(s) => Ok(s.to_string()),
1856 None => Err(unset_error(name)),
1857 };
1858 }
1859 match scope.get(name) {
1860 Some(v) => Ok(value_to_string(v)),
1861 None => Err(unset_error(name)),
1862 }
1863 }
1864 Expansion::BracedPath { root, brackets } => {
1865 braced_path_value(scope, root, brackets).map(|v| value_to_string(&v))
1866 }
1867 Expansion::BracedDefault { root, brackets, default } => {
1868 match braced_default_operand(scope, root, brackets)? {
1869 None => match parse(default)? {
1876 ArithExpr::Expansion(e) => expansion_text_sync(&e, scope),
1877 default_expr => Ok(eval_sync(&default_expr, scope)?.to_string()),
1878 },
1879 Some(v) => Ok(value_to_string(&v)),
1880 }
1881 }
1882 Expansion::LastExitCode => Ok(scope.last_result().code.to_string()),
1883 Expansion::CurrentPid => Ok(scope.pid().to_string()),
1884 Expansion::CommandSubst(_) => Err(needs_async("$(...)")),
1885 Expansion::Nested(inner) => Ok(eval_sync(inner, scope)?.to_string()),
1886 }
1887}
1888
1889fn resolve_based_sync(base: u32, e: &Expansion, scope: &Scope, negative: bool) -> Result<i64, ArithError> {
1890 let text = expansion_text_sync(e, scope)?;
1891 let (label, verb) = expansion_label(e);
1892 based_value(base, &text, &label, verb, negative)
1893}
1894
1895pub(crate) fn eval_sync(expr: &ArithExpr, scope: &Scope) -> Result<i64, ArithError> {
1896 match expr {
1897 ArithExpr::Int(n) => Ok(*n),
1898 ArithExpr::Expansion(e) => resolve_expansion_sync(e, scope),
1899 ArithExpr::Subscript { root, indices } => {
1900 let mut idx_vals = Vec::with_capacity(indices.len());
1901 for idx in indices {
1902 idx_vals.push(eval_sync(idx, scope)?);
1903 }
1904 resolve_subscript_sync(scope, root, &idx_vals)
1905 }
1906 ArithExpr::BasedExpansion { base, expansion } => resolve_based_sync(*base, expansion, scope, false),
1907 ArithExpr::Unary { op: UnOp::Neg, operand } if matches!(operand.as_ref(), ArithExpr::BasedExpansion { .. }) => {
1911 let ArithExpr::BasedExpansion { base, expansion } = operand.as_ref() else {
1912 unreachable!("guarded by the match arm's pattern")
1913 };
1914 resolve_based_sync(*base, expansion, scope, true)
1915 }
1916 ArithExpr::Unary { op, operand } => apply_unary(*op, eval_sync(operand, scope)?),
1917 ArithExpr::Binary { op: BinOp::And, left, right } => {
1918 let l = eval_sync(left, scope)?;
1919 if !truthy(l) { Ok(0) } else { Ok(if truthy(eval_sync(right, scope)?) { 1 } else { 0 }) }
1920 }
1921 ArithExpr::Binary { op: BinOp::Or, left, right } => {
1922 let l = eval_sync(left, scope)?;
1923 if truthy(l) { Ok(1) } else { Ok(if truthy(eval_sync(right, scope)?) { 1 } else { 0 }) }
1924 }
1925 ArithExpr::Binary { op, left, right } => {
1926 apply_binary(*op, eval_sync(left, scope)?, eval_sync(right, scope)?)
1927 }
1928 ArithExpr::Ternary { cond, then_branch, else_branch } => {
1929 if truthy(eval_sync(cond, scope)?) {
1930 eval_sync(then_branch, scope)
1931 } else {
1932 eval_sync(else_branch, scope)
1933 }
1934 }
1935 }
1936}
1937
1938pub fn eval_arithmetic(text: &str, scope: &Scope) -> Result<i64, ArithError> {
1943 let expr = parse(text)?;
1944 eval_sync(&expr, scope)
1945}
1946
1947#[cfg(test)]
1948mod tests {
1949 use super::*;
1950
1951 fn eval(expr: &str) -> i64 {
1952 let scope = Scope::new();
1953 eval_arithmetic(expr, &scope).unwrap_or_else(|e| panic!("eval {expr:?} failed: {e}"))
1954 }
1955
1956 fn err(expr: &str) -> String {
1957 let scope = Scope::new();
1958 eval_arithmetic(expr, &scope).expect_err("expected an error").message
1959 }
1960
1961 fn eval_with(expr: &str, setup: impl FnOnce(&mut Scope)) -> i64 {
1962 let mut scope = Scope::new();
1963 setup(&mut scope);
1964 eval_arithmetic(expr, &scope).unwrap_or_else(|e| panic!("eval {expr:?} failed: {e}"))
1965 }
1966
1967 fn err_with(expr: &str, setup: impl FnOnce(&mut Scope)) -> String {
1968 let mut scope = Scope::new();
1969 setup(&mut scope);
1970 eval_arithmetic(expr, &scope).expect_err("expected an error").message
1971 }
1972
1973 #[test]
1975 fn decimal() {
1976 assert_eq!(eval("42"), 42);
1977 assert_eq!(eval("0"), 0);
1978 }
1979
1980 #[test]
1981 fn hex() {
1982 assert_eq!(eval("0xff"), 255);
1983 assert_eq!(eval("0XFF"), 255);
1984 }
1985
1986 #[test]
1987 fn based() {
1988 assert_eq!(eval("16#ff"), 255);
1989 assert_eq!(eval("8#17"), 15);
1990 assert_eq!(eval("2#1011"), 11);
1991 assert_eq!(eval("36#z"), 35);
1992 }
1993
1994 #[test]
1995 fn negative_hex_and_based() {
1996 assert_eq!(eval("-0xff"), -255);
1997 assert_eq!(eval("- 16#ff"), -255);
1998 }
1999
2000 #[test]
2001 fn sign_after_hash_is_an_error() {
2002 let msg = err("16#-ff");
2003 assert!(msg.contains("puts") && msg.contains('#'), "{msg}");
2004 }
2005
2006 #[test]
2007 fn leading_zero_is_an_error() {
2008 let msg = err("010");
2009 assert!(msg.contains("leading zero"), "{msg}");
2010 assert!(msg.contains("8#10"), "{msg}");
2011 assert!(msg.contains('9') || msg.contains("10"), "{msg}");
2012 }
2013
2014 #[test]
2015 fn zero_alone_is_fine() {
2016 assert_eq!(eval("0 + 1"), 1);
2017 }
2018
2019 #[test]
2020 fn zero_b_and_zero_o_are_not_kaish_spellings() {
2021 let msg = err("0b101");
2022 assert!(msg.contains("2#101"), "{msg}");
2023 let msg = err("0o17");
2024 assert!(msg.contains("8#17"), "{msg}");
2025 }
2026
2027 #[test]
2039 fn negative_leading_zero_names_a_signed_fix() {
2040 let msg = err("-007");
2041 assert!(msg.contains("-8#7"), "{msg}");
2042 assert!(msg.contains("-7"), "{msg}");
2043 assert_eq!(eval("-8#7"), -7);
2044 assert_eq!(eval("-7"), -7);
2045 }
2046
2047 #[test]
2048 fn positive_sign_leading_zero_names_a_signed_fix() {
2049 let msg = err("+007");
2050 assert!(msg.contains("+8#7"), "{msg}");
2051 assert!(msg.contains("+7"), "{msg}");
2052 assert_eq!(eval("+8#7"), 7);
2053 assert_eq!(eval("+7"), 7);
2054 }
2055
2056 #[test]
2057 fn unsigned_leading_zero_fix_is_unchanged() {
2058 let msg = err("007");
2059 assert!(msg.contains("`8#7`"), "{msg}");
2060 assert!(!msg.contains("-8#7") && !msg.contains("+8#7"), "{msg}");
2061 }
2062
2063 #[test]
2064 fn binary_minus_before_leading_zero_keeps_the_unsigned_fix() {
2065 let msg = err("5 - 007");
2069 assert!(msg.contains("`8#7`"), "{msg}");
2070 assert!(!msg.contains("-8#7"), "{msg}");
2071 }
2072
2073 #[test]
2074 fn negative_binary_base_spelling_names_a_signed_fix() {
2075 let msg = err("-0b101");
2076 assert!(msg.contains("-2#101"), "{msg}");
2077 assert_eq!(eval("-2#101"), -5);
2078 }
2079
2080 #[test]
2081 fn negative_octal_o_spelling_names_a_signed_fix() {
2082 let msg = err("-0o17");
2083 assert!(msg.contains("-8#17"), "{msg}");
2084 assert_eq!(eval("-8#17"), -15);
2085 }
2086
2087 #[test]
2088 fn based_expansion_holding_a_signed_leading_zero_string_names_a_working_literal() {
2089 let msg = err_with("x", |s| s.set("x", Value::String("-007".to_string())));
2094 assert!(msg.contains("-10#007"), "{msg}");
2095 assert!(msg.contains("-8#007"), "{msg}");
2096 assert_eq!(eval("-10#007"), -7);
2097 assert_eq!(eval("-8#007"), -7);
2098 }
2099
2100 #[test]
2101 fn based_expansion_holding_an_unsigned_leading_zero_string_is_unchanged() {
2102 let msg = err_with("x", |s| s.set("x", Value::String("007".to_string())));
2103 assert!(msg.contains("10#$x"), "{msg}");
2104 assert!(msg.contains("8#$x"), "{msg}");
2105 }
2106
2107 #[test]
2108 fn base_out_of_range() {
2109 let msg = err("1#5");
2110 assert!(msg.contains("outside 2..=36"), "{msg}");
2111 let msg = err("37#5");
2112 assert!(msg.contains("outside 2..=36"), "{msg}");
2113 }
2114
2115 #[test]
2116 fn bad_digit_for_base() {
2117 let msg = err("2#5");
2118 assert!(msg.contains("not a digit"), "{msg}");
2119 }
2120
2121 #[test]
2127 fn base_out_of_range_survives_u32_truncation() {
2128 for (expr, true_base) in [
2129 ("4294967298#10", "4294967298"),
2130 ("4294967299#10", "4294967299"),
2131 ("4294967330#10", "4294967330"),
2132 ("8589934594#10", "8589934594"),
2133 ] {
2134 let msg = err(expr);
2135 assert!(msg.contains("outside 2..=36"), "{expr}: {msg}");
2136 assert!(msg.contains(true_base), "{expr}: {msg}");
2137 }
2138 }
2139
2140 #[test]
2141 fn based_expansion_out_of_range_survives_u32_truncation() {
2142 let msg = err_with("4294967298#$d", |s| s.set("d", Value::String("10".to_string())));
2143 assert!(msg.contains("outside 2..=36"), "{msg}");
2144 assert!(msg.contains("4294967298"), "{msg}");
2145 }
2146
2147 #[test]
2148 fn string_variable_based_literal_base_overflow_does_not_compute() {
2149 let mut scope = Scope::new();
2150 scope.set("x", Value::String("4294967298#10".to_string()));
2151 assert!(
2152 eval_arithmetic("x", &scope).is_err(),
2153 "a u32-truncated out-of-range base must refuse, not silently compute a value"
2154 );
2155 }
2156
2157 #[test]
2158 fn no_digits_after_prefix() {
2159 let msg = err("0x");
2160 assert!(msg.contains("no digits"), "{msg}");
2161 let msg = err("16#");
2162 assert!(msg.contains("no digits"), "{msg}");
2163 }
2164
2165 #[test]
2166 fn underscore_in_literal_quotes_the_whole_literal() {
2167 assert!(err("1_000").contains("`1_000`"), "{}", err("1_000"));
2169 assert!(err("12_345_6").contains("`12_345_6`"), "{}", err("12_345_6"));
2170 assert!(err("16#f_f").contains("`16#f_f`"), "{}", err("16#f_f"));
2171 }
2172
2173 #[test]
2174 fn out_of_range_literal() {
2175 let msg = err("9223372036854775808 + 1");
2176 assert!(msg.contains("does not fit"), "{msg}");
2177 }
2178
2179 #[test]
2180 fn min_literal_only_as_direct_unary_operand() {
2181 assert_eq!(eval("-9223372036854775808"), i64::MIN);
2182 let msg = err("9223372036854775808");
2183 assert!(msg.contains("does not fit"), "{msg}");
2184 let msg = err("- -9223372036854775808");
2185 assert!(msg.contains("does not fit"), "{msg}");
2186 }
2187
2188 #[test]
2190 fn basic_ops() {
2191 assert_eq!(eval("5 + 3 * 2"), 11);
2192 assert_eq!(eval("10 / 3"), 3);
2193 assert_eq!(eval("-7 % 3"), -1);
2194 assert_eq!(eval("2 ** 10"), 1024);
2195 }
2196
2197 #[test]
2198 fn precedence_examples() {
2199 assert_eq!(eval("1 << 2 + 1"), 8);
2200 assert_eq!(eval("5 & 3 == 3"), 1);
2201 assert_eq!(eval("2 ** 3 ** 2"), 512);
2202 assert_eq!(eval("-2 ** 2"), 4);
2203 assert_eq!(eval("1 ? 2 : 3 ? 4 : 5"), 2);
2204 }
2205
2206 #[test]
2207 fn comparisons_return_one_or_zero() {
2208 assert_eq!(eval("5 > 3"), 1);
2209 assert_eq!(eval("3 > 5"), 0);
2210 }
2211
2212 #[test]
2213 fn bitwise() {
2214 assert_eq!(eval("6 & 3"), 2);
2215 assert_eq!(eval("6 | 1"), 7);
2216 assert_eq!(eval("6 ^ 3"), 5);
2217 assert_eq!(eval("~0"), -1);
2218 }
2219
2220 #[test]
2221 fn shifts() {
2222 assert_eq!(eval("1 << 4"), 16);
2223 assert_eq!(eval("-8 >> 1"), -4);
2224 }
2225
2226 #[test]
2227 fn shift_count_out_of_range() {
2228 let msg = err("1 << 64");
2229 assert!(msg.contains("outside 0..=63"), "{msg}");
2230 let msg = err("1 << -1");
2231 assert!(msg.contains("outside 0..=63"), "{msg}");
2232 }
2233
2234 #[test]
2235 fn short_circuit_and_or() {
2236 assert_eq!(eval("0 && 1"), 0);
2237 assert_eq!(eval("1 && 1"), 1);
2238 assert_eq!(eval("1 || 0"), 1);
2239 assert_eq!(eval("0 || 0"), 0);
2240 }
2241
2242 #[test]
2243 fn ternary_selects_unnormalized_value() {
2244 assert_eq!(eval("1 ? 42 : 7"), 42);
2245 assert_eq!(eval("0 ? 42 : 7"), 7);
2246 }
2247
2248 #[test]
2250 fn overflow_each_op() {
2251 assert!(err("9223372036854775807 + 1").contains("does not fit"));
2252 assert!(err("-9223372036854775808 - 1").contains("does not fit"));
2253 assert!(err("9223372036854775807 * 2").contains("does not fit"));
2254 assert!(err("-9223372036854775808 / -1").contains("does not fit"));
2255 assert!(err("2 ** 63").contains("does not fit"));
2256 assert!(err("1 << 63").contains("does not fit"));
2257 }
2258
2259 #[test]
2260 fn division_and_modulo_by_zero() {
2261 assert!(err("10 / 0").contains("divides by zero"));
2262 assert!(err("10 % 0").contains("divides by zero"));
2263 }
2264
2265 #[test]
2266 fn division_truncates_toward_zero() {
2267 assert_eq!(eval("7 / 2"), 3);
2268 assert_eq!(eval("-7 / 2"), -3);
2269 }
2270
2271 #[test]
2272 fn negative_exponent() {
2273 assert!(err("2 ** -1").contains("negative"));
2274 }
2275
2276 #[test]
2278 fn bare_and_dollar_variable() {
2279 assert_eq!(eval_with("count + 1", |s| s.set("count", Value::Int(4))), 5);
2280 assert_eq!(eval_with("$count + 1", |s| s.set("count", Value::Int(4))), 5);
2281 }
2282
2283 #[test]
2284 fn unset_variable_is_an_error() {
2285 let msg = err("missing + 1");
2286 assert!(msg.contains("unset"), "{msg}");
2287 assert!(msg.contains(":-0"), "{msg}");
2288 }
2289
2290 #[test]
2291 fn random_and_seconds_name_their_fix() {
2292 let msg = err("RANDOM % 10");
2293 assert!(msg.contains("random --max"), "{msg}");
2294 let msg = err("SECONDS");
2295 assert!(msg.contains("date +%s"), "{msg}");
2296 }
2297
2298 #[test]
2299 fn null_variable_is_an_error() {
2300 let msg = err_with("x", |s| s.set("x", Value::Null));
2301 assert!(msg.contains("null"), "{msg}");
2302 }
2303
2304 #[test]
2305 fn float_variable_errors() {
2306 let msg = err_with("x", |s| s.set("x", Value::Float(2.7)));
2307 assert!(msg.contains("integer-only"), "{msg}");
2308 }
2309
2310 #[test]
2311 fn integral_float_coerces() {
2312 assert_eq!(eval_with("x + 1", |s| s.set("x", Value::Float(100.0))), 101);
2313 }
2314
2315 #[test]
2316 fn float_at_2_63_is_out_of_range() {
2317 let msg = err_with("x", |s| s.set("x", Value::Float(9223372036854775808.0)));
2321 assert!(msg.contains("64-bit"), "{msg}");
2322 }
2323
2324 #[test]
2325 fn float_at_min_still_converts() {
2326 assert_eq!(eval_with("x", |s| s.set("x", Value::Float(-9223372036854775808.0))), i64::MIN);
2327 }
2328
2329 #[test]
2330 fn negative_zero_float_converts_to_zero() {
2331 assert_eq!(eval_with("x", |s| s.set("x", Value::Float(-0.0))), 0);
2332 }
2333
2334 #[test]
2335 fn string_value_is_parsed() {
2336 assert_eq!(eval_with("x", |s| s.set("x", Value::String("0xff".to_string()))), 255);
2337 assert_eq!(eval_with("mask & 16#0f", |s| s.set("mask", Value::String("0xff".to_string()))), 15);
2338 }
2339
2340 #[test]
2341 fn string_with_leading_zero_names_the_fix() {
2342 let msg = err_with("x", |s| s.set("x", Value::String("08".to_string())));
2343 assert!(msg.contains("10#$x") || msg.contains("leading zero"), "{msg}");
2344 }
2345
2346 #[test]
2347 fn string_expression_names_the_fix() {
2348 let msg = err_with("x", |s| s.set("x", Value::String("1 + 2".to_string())));
2349 assert!(msg.contains("not an expression"), "{msg}");
2350 }
2351
2352 #[test]
2353 fn string_non_numeric_is_an_error() {
2354 let msg = err_with("x", |s| s.set("x", Value::String("abc".to_string())));
2355 assert!(msg.contains("not a number"), "{msg}");
2356 }
2357
2358 #[test]
2363 fn string_binary_spelling_names_the_fix() {
2364 let msg = err_with("x", |s| s.set("x", Value::String("0b101".to_string())));
2365 assert!(msg.contains("`x`") && msg.contains("0b101"), "{msg}");
2366 assert!(msg.contains("2#101"), "{msg}");
2367 }
2368
2369 #[test]
2370 fn string_underscore_digit_group_names_the_fix() {
2371 let msg = err_with("x", |s| s.set("x", Value::String("1_000".to_string())));
2372 assert!(msg.contains("`x`") && msg.contains("1_000"), "{msg}");
2373 assert!(msg.contains("remove it"), "{msg}");
2374 }
2375
2376 #[test]
2377 fn string_float_spelling_names_the_fix() {
2378 let msg = err_with("x", |s| s.set("x", Value::String("1e3".to_string())));
2379 assert!(msg.contains("`x`") && msg.contains("1e3"), "{msg}");
2380 assert!(msg.contains("integer-only"), "{msg}");
2381 }
2382
2383 #[test]
2384 fn list_record_and_bytes_error() {
2385 let msg = err_with("x", |s| s.set("x", Value::Json(serde_json::json!([1, 2]))));
2386 assert!(msg.contains("list"), "{msg}");
2387 let msg = err_with("x", |s| s.set("x", Value::Json(serde_json::json!({"a": 1}))));
2388 assert!(msg.contains("record"), "{msg}");
2389 let msg = err_with("x", |s| s.set("x", Value::Bytes(vec![0xff, 0xfe, 0x00, 0x01])));
2390 assert!(msg.contains("bytes"), "{msg}");
2391 }
2392
2393 #[test]
2394 fn last_exit_code_and_pid() {
2395 let mut scope = Scope::new();
2396 scope.set_last_result(crate::interpreter::ExecResult::success("x").with_code(3));
2397 assert_eq!(eval_arithmetic("$?", &scope).unwrap(), 3);
2398 assert_eq!(eval_arithmetic("$$", &scope).unwrap(), scope.pid() as i64);
2399 }
2400
2401 #[test]
2403 fn bare_subscript_is_a_variable_expression() {
2404 let r = eval_with("xs[i]", |s| {
2405 s.set("xs", Value::Json(serde_json::json!([10, 20, 30])));
2406 s.set("i", Value::Int(1));
2407 });
2408 assert_eq!(r, 20);
2409 }
2410
2411 #[test]
2412 fn bare_subscript_literal_and_expression_index() {
2413 let r = eval_with("xs[0] + 1", |s| s.set("xs", Value::Json(serde_json::json!([10, 20, 30]))));
2414 assert_eq!(r, 11);
2415 let r = eval_with("xs[i + 1]", |s| {
2416 s.set("xs", Value::Json(serde_json::json!([10, 20, 30])));
2417 s.set("i", Value::Int(0));
2418 });
2419 assert_eq!(r, 20);
2420 }
2421
2422 #[test]
2423 fn braced_path_reads_a_literal_key() {
2424 let r = eval_with("${c[port]}", |s| s.set("c", Value::Json(serde_json::json!({"port": 8080}))));
2425 assert_eq!(r, 8080);
2426 }
2427
2428 #[test]
2430 fn default_used_when_unset() {
2431 assert_eq!(eval("${limit:-0} + 1"), 1);
2432 }
2433
2434 #[test]
2435 fn default_not_used_when_set() {
2436 assert_eq!(eval_with("${limit:-0} + 1", |s| s.set("limit", Value::Int(9))), 10);
2437 }
2438
2439 #[test]
2441 fn nested_arithmetic() {
2442 assert_eq!(eval("$(( 1 + 2 )) * 4"), 12);
2443 }
2444
2445 #[test]
2446 fn newline_inside_is_whitespace() {
2447 assert_eq!(eval("1 +\n2"), 3);
2448 }
2449
2450 #[test]
2452 fn empty_is_an_error() {
2453 let msg = err("");
2454 assert!(msg.contains("no expression"), "{msg}");
2455 }
2456
2457 #[test]
2458 fn empty_group_is_an_error() {
2459 let msg = err("()");
2460 assert!(msg.contains("no expression"), "{msg}");
2461 }
2462
2463 #[test]
2464 fn missing_close_paren() {
2465 let msg = err("(1 + 2");
2466 assert!(msg.contains("closing"), "{msg}");
2467 }
2468
2469 #[test]
2470 fn extra_close_paren() {
2471 let msg = err("1 + 2)");
2472 assert!(msg.contains("matching"), "{msg}");
2473 }
2474
2475 #[test]
2476 fn ternary_without_colon() {
2477 let msg = err("1 ? 2");
2478 assert!(msg.contains(':'), "{msg}");
2479 }
2480
2481 #[test]
2482 fn not_operators_are_diagnosed() {
2483 assert!(err("1 <<< 2").contains("here-string"));
2484 assert!(err("1 >>> 2").contains(">>"));
2485 assert!(err("x = 5").contains("assigns"));
2486 assert!(err("x += 1").contains("assigns"));
2487 assert!(err("x++").contains("assigns"));
2488 assert!(err("1, 2").contains("one expression"));
2489 }
2490
2491 #[test]
2492 fn assignment_errors_name_the_real_tokens_not_a_placeholder() {
2493 assert_eq!(err("x++"), "`x++` assigns inside `$(( ))`; write `x=$((x + 1))`");
2494 assert_eq!(err("++x"), "`++x` assigns inside `$(( ))`; write `x=$((x + 1))`");
2495 assert_eq!(err("x--"), "`x--` assigns inside `$(( ))`; write `x=$((x - 1))`");
2496 assert_eq!(err("--x"), "`--x` assigns inside `$(( ))`; write `x=$((x - 1))`");
2497 assert_eq!(err("x += 2"), "`x += 2` assigns inside `$(( ))`; write `x=$((x + 2))`");
2498 assert_eq!(err("x -= 3"), "`x -= 3` assigns inside `$(( ))`; write `x=$((x - 3))`");
2499 assert_eq!(err("x = 2"), "`x = 2` assigns inside `$(( ))`; write `x=2`, or `==` to compare");
2500 }
2501
2502 #[test]
2503 fn missing_operand_names_the_source_consumed_so_far() {
2504 assert_eq!(
2505 err("1 + "),
2506 "`+` has no right operand in `1 + `; add an integer expression after `+`"
2507 );
2508 assert_eq!(err(" + "), "`+` has no operand; add an integer expression after `+`");
2509 }
2510
2511 #[test]
2512 fn a_leading_zero_base_is_refused() {
2513 assert_eq!(err("08#17"), "`08` is not a base spelling; write the base without a leading zero");
2514 assert_eq!(err("010#5"), "`010` is not a base spelling; write the base without a leading zero");
2515 }
2516
2517 #[test]
2518 fn based_expansion_digits_take_no_sign() {
2519 let msg = err_with("16#$d", |s| s.set("d", Value::String("-ff".to_string())));
2520 assert_eq!(msg, "`d` holds `-ff`; the digits after `#` take no sign — write `-16#ff`");
2521 }
2522
2523 #[test]
2524 fn depth_cap() {
2525 let mut src = String::new();
2526 for _ in 0..300 {
2527 src.push('(');
2528 }
2529 src.push('1');
2530 for _ in 0..300 {
2531 src.push(')');
2532 }
2533 let msg = err(&src);
2534 assert!(msg.contains("256"), "{msg}");
2535 }
2536
2537 fn parse_err(text: &str) -> ArithError {
2548 parse(text).expect_err("expected a tokenizer/parse error")
2549 }
2550
2551 fn assert_span_slices_to(source: &str, err: &ArithError, expected: &str) {
2554 let slice = source.get(err.span.clone());
2555 assert!(
2556 slice.is_some(),
2557 "span {:?} does not slice {source:?} (message: {})",
2558 err.span,
2559 err.message
2560 );
2561 assert_eq!(slice.expect("checked above"), expected, "source {source:?}, message {:?}", err.message);
2562 }
2563
2564 #[test]
2568 fn control_ascii_before_leading_zero_span_is_byte_correct() {
2569 let source = "$(echo x) + 008";
2570 let err = parse_err(source);
2571 assert!(err.message.contains("leading zero"), "{}", err.message);
2572 assert_span_slices_to(source, &err, "008");
2573 }
2574
2575 #[test]
2576 fn ideographic_space_before_leading_zero_span_is_byte_correct() {
2577 let source = "\u{3000}008";
2581 let err = parse_err(source);
2582 assert!(err.message.contains("leading zero"), "{}", err.message);
2583 assert_span_slices_to(source, &err, "008");
2584 }
2585
2586 #[test]
2587 fn full_width_digit_before_leading_zero_span_is_byte_correct() {
2588 let source = "$(echo \u{ff10}) + 008";
2592 let err = parse_err(source);
2593 assert!(err.message.contains("leading zero"), "{}", err.message);
2594 assert_span_slices_to(source, &err, "008");
2595 }
2596
2597 #[test]
2598 fn accented_letter_before_leading_zero_span_is_byte_correct() {
2599 let source = "$(echo caf\u{e9}) + 008";
2601 let err = parse_err(source);
2602 assert!(err.message.contains("leading zero"), "{}", err.message);
2603 assert_span_slices_to(source, &err, "008");
2604 }
2605
2606 #[test]
2607 fn accented_letter_before_unterminated_brace_group_span_is_byte_correct() {
2608 let source = "\u{3000}${caf\u{e9}";
2609 let err = parse_err(source);
2610 assert!(err.message.contains("no closing"), "{}", err.message);
2611 assert_span_slices_to(source, &err, "${caf\u{e9}");
2612 }
2613}
2614