kaish-kernel 0.8.2

Core kernel for kaish: lexer, parser, interpreter, and runtime
Documentation
//! seq — Print sequences of numbers.

use async_trait::async_trait;
use clap::{CommandFactory, Parser};

use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData, OutputNode};
use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::validator::{IssueCode, ValidationIssue};

/// Seq tool: print a sequence of numbers.
pub struct Seq;

/// clap-derived argv layer for seq. See docs/clap-migration.md.
#[derive(Parser, Debug)]
#[command(name = "seq", about = "Print sequences of numbers")]
struct SeqArgs {
    /// Separator between numbers (-s)
    #[arg(short = 's', long = "separator")]
    separator: Option<String>,

    /// Equalize width by padding with zeros (-w)
    #[arg(short = 'w', long = "width")]
    width: bool,

    #[command(flatten)]
    global: GlobalFlags,

    /// Sequence bounds: `LAST`, `FIRST LAST`, or `FIRST INCREMENT LAST`.
    range: Vec<String>,
}

#[async_trait]
impl Tool for Seq {
    fn name(&self) -> &str {
        "seq"
    }

    fn schema(&self) -> ToolSchema {
        schema_from_clap(
            &SeqArgs::command(),
            "seq",
            "Print sequences of numbers",
            [
                ("Count to 5", "seq 5"),
                ("Range with step", "seq 1 2 10"),
                ("Zero-padded", "seq -w 1 100"),
            ],
        )
    }

    fn validate(&self, args: &ToolArgs) -> Vec<ValidationIssue> {
        let mut issues = validate_against_schema(args, &self.schema());

        // Check for zero increment (infinite loop)
        // seq FIRST INCREMENT LAST has increment at position 1 when 3 args provided
        if args.positional.len() == 3 {
            if let Some(Value::Int(0)) = args.positional.get(1) {
                issues.push(ValidationIssue::error(
                    IssueCode::SeqZeroIncrement,
                    "seq: increment cannot be zero (would cause infinite loop)",
                ));
            } else if let Some(Value::Float(f)) = args.positional.get(1) {
                if *f == 0.0 {
                    issues.push(ValidationIssue::error(
                        IssueCode::SeqZeroIncrement,
                        "seq: increment cannot be zero (would cause infinite loop)",
                    ));
                }
            } else if let Some(Value::String(s)) = args.positional.get(1)
                && let Ok(n) = s.parse::<f64>()
                    && n == 0.0 {
                        issues.push(ValidationIssue::error(
                            IssueCode::SeqZeroIncrement,
                            "seq: increment cannot be zero (would cause infinite loop)",
                        ));
                    }
        }

        issues
    }

    async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
        let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
            return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
        };
        let parsed = match SeqArgs::try_parse_from(
            std::iter::once("seq".to_string()).chain(args.to_argv()),
        ) {
            Ok(p) => p,
            Err(e) => return ExecResult::failure(2, format!("seq: {e}")),
        };
        parsed.global.apply(ctx);

        // Parse the arguments - seq has unusual positional arg handling:
        // seq LAST           -> 1 to LAST
        // seq FIRST LAST     -> FIRST to LAST
        // seq FIRST INC LAST -> FIRST to LAST by INC

        let (first, increment, last) = match (
            args.get_positional(0),
            args.get_positional(1),
            args.get_positional(2),
        ) {
            (Some(v1), None, None) => {
                let last = value_to_f64(v1);
                (1.0, 1.0, last)
            }
            (Some(v1), Some(v2), None) => {
                let first = value_to_f64(v1);
                let last = value_to_f64(v2);
                (first, 1.0, last)
            }
            (Some(v1), Some(v2), Some(v3)) => {
                let first = value_to_f64(v1);
                let increment = value_to_f64(v2);
                let last = value_to_f64(v3);
                (first, increment, last)
            }
            _ => return ExecResult::failure(1, "seq: missing argument"),
        };

        if increment == 0.0 {
            return ExecResult::failure(1, "seq: increment cannot be zero");
        }

        let separator = parsed.separator.clone()
            .or_else(|| args.get_string("separator", usize::MAX))
            .or_else(|| args.get_string("s", usize::MAX))
            .unwrap_or_else(|| "\n".to_string());

        let pad_width = parsed.width;

        // Generate sequence
        let mut numbers = Vec::new();
        let mut current = first;
        let tolerance = increment.abs() * 1e-10;

        if increment > 0.0 {
            while current <= last + tolerance {
                numbers.push(current);
                current += increment;
            }
        } else {
            while current >= last - tolerance {
                numbers.push(current);
                current += increment;
            }
        }

        if numbers.is_empty() {
            // Return empty OutputData
            return ExecResult::with_output(OutputData::new());
        }

        // Format output
        let is_integer = numbers.iter().all(|n| n.fract().abs() < f64::EPSILON);

        let formatted: Vec<String> = if is_integer {
            let max_width = if pad_width {
                numbers
                    .iter()
                    .map(|n| (*n as i64).abs().to_string().len())
                    .max()
                    .unwrap_or(1)
            } else {
                0
            };

            numbers
                .iter()
                .map(|n| {
                    let i = *n as i64;
                    if pad_width {
                        format!("{:0>width$}", i, width = max_width)
                    } else {
                        i.to_string()
                    }
                })
                .collect()
        } else {
            numbers.iter().map(|n| format!("{}", n)).collect()
        };

        // Build OutputNodes from formatted numbers
        let nodes: Vec<OutputNode> = formatted
            .iter()
            .map(|s| OutputNode::new(s.as_str()))
            .collect();

        // Build JSON array of numbers for structured iteration
        let json_array: Vec<serde_json::Value> = numbers
            .iter()
            .map(|n| {
                if is_integer {
                    serde_json::Value::Number((*n as i64).into())
                } else {
                    serde_json::Number::from_f64(*n)
                        .map(serde_json::Value::Number)
                        .unwrap_or(serde_json::Value::Null)
                }
            })
            .collect();

        // Create OutputData and preserve the custom separator in text output
        let output_data = OutputData::nodes(nodes);
        let mut result = ExecResult::with_output_and_text(output_data, formatted.join(&separator) + "\n");
        result.data = Some(Value::Json(serde_json::Value::Array(json_array)));
        result
    }
}

fn value_to_f64(v: &Value) -> f64 {
    match v {
        Value::Int(i) => *i as f64,
        Value::Float(f) => *f,
        Value::String(s) => s.parse().unwrap_or(0.0),
        _ => 0.0,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::vfs::{MemoryFs, VfsRouter};
    use std::sync::Arc;

    fn make_ctx() -> ExecContext {
        let mut vfs = VfsRouter::new();
        vfs.mount("/", MemoryFs::new());
        ExecContext::new(Arc::new(vfs))
    }

    #[tokio::test]
    async fn test_seq_single_arg() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Int(5));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        let text = result.text_out();
        let lines: Vec<&str> = text.lines().collect();
        assert_eq!(lines, vec!["1", "2", "3", "4", "5"]);
    }

    #[tokio::test]
    async fn test_seq_first_last() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Int(3));
        args.positional.push(Value::Int(7));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        let text = result.text_out();
        let lines: Vec<&str> = text.lines().collect();
        assert_eq!(lines, vec!["3", "4", "5", "6", "7"]);
    }

    #[tokio::test]
    async fn test_seq_with_increment() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Int(1));
        args.positional.push(Value::Int(2));
        args.positional.push(Value::Int(10));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        let text = result.text_out();
        let lines: Vec<&str> = text.lines().collect();
        assert_eq!(lines, vec!["1", "3", "5", "7", "9"]);
    }

    #[tokio::test]
    async fn test_seq_negative_increment() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Int(5));
        args.positional.push(Value::Int(-1));
        args.positional.push(Value::Int(1));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        let text = result.text_out();
        let lines: Vec<&str> = text.lines().collect();
        assert_eq!(lines, vec!["5", "4", "3", "2", "1"]);
    }

    #[tokio::test]
    async fn test_seq_custom_separator() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Int(3));
        args.named
            .insert("separator".to_string(), Value::String(", ".into()));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert_eq!(result.text_out().trim(), "1, 2, 3");
    }

    #[tokio::test]
    async fn test_seq_zero_pad() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Int(10));
        args.flags.insert("w".to_string());

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        let text = result.text_out();
        let lines: Vec<&str> = text.lines().collect();
        assert_eq!(lines[0], "01");
        assert_eq!(lines[9], "10");
    }

    #[tokio::test]
    async fn test_seq_float() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Float(1.0));
        args.positional.push(Value::Float(0.5));
        args.positional.push(Value::Float(2.0));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        let text = result.text_out();
        let lines: Vec<&str> = text.lines().collect();
        assert_eq!(lines.len(), 3); // 1.0, 1.5, 2.0
    }

    #[tokio::test]
    async fn test_seq_float_boundary() {
        // Bug C: seq 0.1 0.1 1.0 must produce exactly 10 values
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Float(0.1));
        args.positional.push(Value::Float(0.1));
        args.positional.push(Value::Float(1.0));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(result.ok());
        let text = result.text_out();
        let lines: Vec<&str> = text.lines().collect();
        assert_eq!(lines.len(), 10, "expected 10 values, got {}: {:?}", lines.len(), lines);
    }

    #[tokio::test]
    async fn test_seq_zero_increment() {
        let mut ctx = make_ctx();
        let mut args = ToolArgs::new();
        args.positional.push(Value::Int(1));
        args.positional.push(Value::Int(0));
        args.positional.push(Value::Int(5));

        let result = Seq.execute(args, &mut ctx).await;
        assert!(!result.ok());
        assert!(result.err.contains("zero"));
    }
}