pub mod ast;
pub mod error;
pub mod lexer;

use std::str::FromStr;

use crate::diag::Diag;
use crate::ir::{BinaryOp, DType, UnaryOp};
use crate::parse::ast::{Expr, FuncDef, Span, Spanned};
use crate::parse::error::{
    parse_int_imaginary, parse_reserved_identifier, parse_syntax, parse_unknown_dtype,
};
use crate::parse::lexer::{
    KEYWORDS, RESERVED_TYPE_NAMES, TokenKind, is_ident_continue, is_ident_start, lexer,
};
use chumsky::error::RichReason;
use chumsky::pratt::{infix, left, none, prefix, right};
use chumsky::prelude::*;
use chumsky::recovery::skip_then_retry_until;
use chumsky::span::SimpleSpan;

fn merge_span(a: Span, b: Span) -> Span {
    Span::new(a.start.min(b.start), a.end.max(b.end))
}

fn simple_span_to_span(span: SimpleSpan<usize>) -> Span {
    Span::new(span.start, span.end)
}

fn span_to_simple(span: Span) -> SimpleSpan<usize> {
    SimpleSpan::from(span.start..span.end)
}

fn parse_error_from_rich(err: Rich<'_, TokenKind, SimpleSpan<usize>>) -> Diag {
    let span = simple_span_to_span(*err.span());
    match err.into_reason() {
        RichReason::Custom(msg) if msg == "invalid integer literal" => parse_syntax(&msg, span),
        RichReason::Custom(msg) => {
            if let Some(name) = msg.strip_prefix("unknown-dtype:") {
                parse_unknown_dtype(name, span)
            } else {
                parse_syntax(&msg, span)
            }
        }
        RichReason::ExpectedFound { .. } => parse_syntax("syntax error", span),
    }
}

fn find_int_imaginary(src: &str) -> Option<(i64, usize, usize)> {
    let bytes = src.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i].is_ascii_digit() {
            let start = i;
            while i < bytes.len() && bytes[i].is_ascii_digit() {
                i += 1;
            }
            if i < bytes.len() && bytes[i] == b'j' && !(start > 0 && bytes[start - 1] == b'.') {
                let prev = start.checked_sub(1).map(|idx| bytes[idx]);
                let prev_prev = start.checked_sub(2).map(|idx| bytes[idx]);
                let in_exp = matches!(prev, Some(b'e' | b'E'))
                    || (matches!(prev, Some(b'+' | b'-'))
                        && matches!(prev_prev, Some(b'e' | b'E')));
                if !in_exp {
                    let value = src[start..i].parse::<i64>().unwrap_or(0);
                    return Some((value, start, i - start + 1));
                }
            }
        } else {
            i += 1;
        }
    }
    None
}

/// Strips line comments (`#` to newline) while preserving length (replaces with spaces).
/// Used so that AST spans in the cleaned source match original byte offsets.
pub(crate) fn strip_line_comments_preserve_len(src: &str) -> String {
    let bytes = src.as_bytes();
    let mut out = Vec::with_capacity(bytes.len());
    let mut i = 0;
    while i < bytes.len() {
        let b = bytes[i];
        if b == b'#' {
            // Treat `#` as the start of a line comment: replace everything until newline with spaces.
            out.push(b' ');
            i += 1;
            while i < bytes.len() && bytes[i] != b'\n' {
                out.push(b' ');
                i += 1;
            }
        } else {
            out.push(b);
            i += 1;
        }
    }
    String::from_utf8(out).expect("source is valid UTF-8")
}

#[allow(clippy::type_complexity)]
fn expr_parser<'src>() -> impl Parser<
    'src,
    chumsky::input::MappedInput<
        'src,
        TokenKind,
        SimpleSpan<usize>,
        &'src [(TokenKind, SimpleSpan<usize>)],
    >,
    Spanned<Expr>,
    extra::Err<Rich<'src, TokenKind, SimpleSpan<usize>>>,
> {
    recursive(|expr| {
        let dtype = select! {
            TokenKind::Ident(name) => name,
            TokenKind::DTypeName(name) => name,
        }
        .map_with(|name, e| (name, e.span()))
        .try_map(|(name, span), _| {
            DType::from_str(&name).map_err(|_| Rich::custom(span, format!("unknown-dtype:{name}")))
        })
        .boxed();

        let literal_ann = just(TokenKind::At)
            .ignore_then(dtype.clone())
            .map_with(|dtype, e| (dtype, e.span()));

        let lit_int = select! { TokenKind::Int(v) => v }
            .map_with(|v, e| (v, e.span()))
            .then(literal_ann.clone().or_not())
            .map(|((v, span), ann)| {
                let span = ann
                    .as_ref()
                    .map(|(_, ann_span)| {
                        merge_span(simple_span_to_span(span), simple_span_to_span(*ann_span))
                    })
                    .unwrap_or_else(|| simple_span_to_span(span));
                Spanned::new(Expr::LitInt(v, ann.map(|(d, _)| d)), span)
            });

        let lit_float = select! { TokenKind::Float(v) => v }
            .map_with(|v, e| (v, e.span()))
            .then(literal_ann.clone().or_not())
            .map(|((v, span), ann)| {
                let span = ann
                    .as_ref()
                    .map(|(_, ann_span)| {
                        merge_span(simple_span_to_span(span), simple_span_to_span(*ann_span))
                    })
                    .unwrap_or_else(|| simple_span_to_span(span));
                Spanned::new(Expr::LitFloat(v, ann.map(|(d, _)| d)), span)
            });

        let lit_complex = select! { TokenKind::Complex(v) => v }
            .map_with(|v, e| (v, e.span()))
            .then(literal_ann.clone().or_not())
            .map(|((v, span), ann)| {
                let span = ann
                    .as_ref()
                    .map(|(_, ann_span)| {
                        merge_span(simple_span_to_span(span), simple_span_to_span(*ann_span))
                    })
                    .unwrap_or_else(|| simple_span_to_span(span));
                Spanned::new(Expr::LitComplex(0.0, v, ann.map(|(d, _)| d)), span)
            });

        let lit_bool = select! {
            TokenKind::True => true,
            TokenKind::False => false,
        }
        .map_with(|v, e| Spanned::new(Expr::LitBool(v), simple_span_to_span(e.span())));

        let const_sym = select! {
            TokenKind::ConstE => Expr::ConstE,
            TokenKind::ConstPi => Expr::ConstPi,
        }
        .map_with(|v, e| Spanned::new(v, simple_span_to_span(e.span())));

        let ident = select! { TokenKind::Ident(name) => name };
        let var =
            ident.map_with(|name, e| Spanned::new(Expr::Var(name), simple_span_to_span(e.span())));

        let comma = just(TokenKind::Comma).recover_with(skip_then_retry_until(
            any().ignored(),
            choice((just(TokenKind::Comma), just(TokenKind::RParen))).ignored(),
        ));
        let rparen = just(TokenKind::RParen).recover_with(skip_then_retry_until(
            any().ignored(),
            just(TokenKind::RParen).ignored(),
        ));

        let call = ident
            .then(
                expr.clone()
                    .separated_by(comma.clone())
                    .collect::<Vec<_>>()
                    .delimited_by(just(TokenKind::LParen), rparen.clone()),
            )
            .map_with(|(name, args), e| {
                Spanned::new(Expr::Call(name, args), simple_span_to_span(e.span()))
            });

        let cast = just(TokenKind::Cast)
            .ignore_then(just(TokenKind::Lt))
            .ignore_then(dtype.clone())
            .then_ignore(just(TokenKind::Gt))
            .then_ignore(just(TokenKind::LParen))
            .then(expr.clone())
            .then_ignore(rparen.clone())
            .map_with(|(dtype, child), e| {
                Spanned::new(
                    Expr::Cast(dtype, Box::new(child)),
                    simple_span_to_span(e.span()),
                )
            });

        let tuple = just(TokenKind::LParen)
            .ignore_then(expr.clone())
            .then_ignore(comma.clone())
            .then(
                expr.clone()
                    .separated_by(comma.clone())
                    .at_least(1)
                    .collect::<Vec<_>>(),
            )
            .then_ignore(rparen.clone())
            .map_with(|(first, rest), e| {
                let mut items = Vec::with_capacity(rest.len() + 1);
                items.push(first);
                items.extend(rest);
                Spanned::new(Expr::Tuple(items), simple_span_to_span(e.span()))
            });

        let group = just(TokenKind::LParen)
            .ignore_then(expr.clone())
            .then_ignore(rparen)
            .map_with(|mut inner, e| {
                inner.span = simple_span_to_span(e.span());
                inner
            });

        let atom_base = choice((
            cast,
            call,
            tuple,
            group,
            lit_complex,
            lit_float,
            lit_int,
            lit_bool,
            const_sym,
            var,
        ));

        // Postfix `@dtype` cast sugar: `expr @ dtype` → `Expr::Cast(dtype, expr)`.
        let postfix_casts = literal_ann.clone().repeated().collect::<Vec<_>>();

        let atom = atom_base.then(postfix_casts).map(|(base, anns)| {
            anns.into_iter().fold(base, |inner, (dtype, ann_span)| {
                let cast_span = merge_span(inner.span, simple_span_to_span(ann_span));
                Spanned::new(Expr::Cast(dtype, Box::new(inner)), cast_span)
            })
        });

        let prefix_op = choice((
            just(TokenKind::Minus).map_with(|_, e| (UnaryOp::Neg, e.span())),
            just(TokenKind::Bang).map_with(|_, e| (UnaryOp::Not, e.span())),
        ));

        let infix_pow = just(TokenKind::Pow).to(BinaryOp::Pow);
        let infix_mul = choice((
            just(TokenKind::Star).to(BinaryOp::Mul),
            just(TokenKind::Slash).to(BinaryOp::Div),
            just(TokenKind::SlashSlash).to(BinaryOp::FloorDiv),
            just(TokenKind::Percent).to(BinaryOp::Rem),
        ));
        let infix_add = choice((
            just(TokenKind::Plus).to(BinaryOp::Add),
            just(TokenKind::Minus).to(BinaryOp::Sub),
        ));
        let infix_cmp = choice((
            just(TokenKind::EqEq).to(BinaryOp::Eq),
            just(TokenKind::Ne).to(BinaryOp::Ne),
            just(TokenKind::Lt).to(BinaryOp::Lt),
            just(TokenKind::Le).to(BinaryOp::Le),
            just(TokenKind::Gt).to(BinaryOp::Gt),
            just(TokenKind::Ge).to(BinaryOp::Ge),
        ));
        let infix_and = just(TokenKind::AndAnd).to(BinaryOp::And);
        let infix_or = just(TokenKind::OrOr).to(BinaryOp::Or);

        let pratt_expr = atom.pratt((
            infix(right(6), infix_pow, |lhs, op, rhs, e| {
                Spanned::new(
                    Expr::Binary(op, Box::new(lhs), Box::new(rhs)),
                    simple_span_to_span(e.span()),
                )
            }),
            prefix(5, prefix_op, |(op, op_span), rhs, e| {
                let span = merge_span(simple_span_to_span(op_span), simple_span_to_span(e.span()));
                Spanned::new(Expr::Unary(op, Box::new(rhs)), span)
            }),
            infix(left(4), infix_mul, |lhs, op, rhs, e| {
                Spanned::new(
                    Expr::Binary(op, Box::new(lhs), Box::new(rhs)),
                    simple_span_to_span(e.span()),
                )
            }),
            infix(left(3), infix_add, |lhs, op, rhs, e| {
                Spanned::new(
                    Expr::Binary(op, Box::new(lhs), Box::new(rhs)),
                    simple_span_to_span(e.span()),
                )
            }),
            infix(none(2), infix_cmp, |lhs, op, rhs, e| {
                Spanned::new(
                    Expr::Binary(op, Box::new(lhs), Box::new(rhs)),
                    simple_span_to_span(e.span()),
                )
            }),
            infix(left(1), infix_and, |lhs, op, rhs, e| {
                Spanned::new(
                    Expr::Binary(op, Box::new(lhs), Box::new(rhs)),
                    simple_span_to_span(e.span()),
                )
            }),
            infix(left(0), infix_or, |lhs, op, rhs, e| {
                Spanned::new(
                    Expr::Binary(op, Box::new(lhs), Box::new(rhs)),
                    simple_span_to_span(e.span()),
                )
            }),
        ));

        let if_expr = just(TokenKind::If)
            .ignore_then(expr.clone())
            .then_ignore(just(TokenKind::Then))
            .then(expr.clone())
            .then_ignore(just(TokenKind::Else))
            .then(expr.clone())
            .map_with(|((cond, then_expr), else_expr), e| {
                Spanned::new(
                    Expr::IfElse(Box::new(cond), Box::new(then_expr), Box::new(else_expr)),
                    simple_span_to_span(e.span()),
                )
            });

        let sync = choice((
            just(TokenKind::Comma),
            just(TokenKind::RParen),
            just(TokenKind::Then),
            just(TokenKind::Else),
        ))
        .ignored();

        choice((if_expr, pratt_expr)).recover_with(skip_then_retry_until(any().ignored(), sync))
    })
}

fn parse_expr_with_offset(src: &str, base: usize) -> Result<Spanned<Expr>, Vec<Diag>> {
    if let Some((value, offset, width)) = find_int_imaginary(src) {
        return Err(vec![parse_int_imaginary(
            value,
            Span::new(base + offset, base + offset + width),
        )]);
    }
    let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
    if !lex_errs.is_empty() {
        let diags = lex_errs
            .into_iter()
            .map(|err| {
                let span = err.span();
                let msg = match err.reason() {
                    RichReason::Custom(msg) => msg.clone(),
                    RichReason::ExpectedFound { .. } => "lex error".to_string(),
                };
                parse_syntax(&msg, Span::new(base + span.start, base + span.end))
            })
            .collect();
        return Err(diags);
    }
    let tokens = tokens.ok_or_else(|| vec![parse_syntax("lex error", Span::new(base, base))])?;

    let tokens: Vec<_> = tokens
        .into_iter()
        .map(|(tok, span)| {
            let offset = SimpleSpan::from((span.start + base)..(span.end + base));
            (tok, offset)
        })
        .collect();

    let end_span = Span::new(base + src.len(), base + src.len());
    let input = tokens.as_slice().split_token_span(span_to_simple(end_span));
    let (expr, parse_errs) = expr_parser()
        .then_ignore(end())
        .parse(input)
        .into_output_errors();
    if !parse_errs.is_empty() {
        let diags = parse_errs.into_iter().map(parse_error_from_rich).collect();
        return Err(diags);
    }
    let expr = expr.ok_or_else(|| vec![parse_syntax("parse error", Span::new(base, base))])?;

    Ok(expr)
}

/// Lex a source slice and return tokens with spans in absolute coordinates (base + offset).
/// Use this for highlight and other consumers that need token-level spans without parsing.
pub fn lex_with_offset(source: &str, base: usize) -> Result<Vec<(TokenKind, Span)>, Vec<Diag>> {
    let (tokens, lex_errs) = lexer().parse(source).into_output_errors();
    if !lex_errs.is_empty() {
        return Err(lex_errs
            .into_iter()
            .map(|err| {
                let span = err.span();
                let msg = match err.reason() {
                    RichReason::Custom(msg) => msg.clone(),
                    RichReason::ExpectedFound { .. } => "lex error".to_string(),
                };
                parse_syntax(&msg, Span::new(base + span.start, base + span.end))
            })
            .collect());
    }
    let tokens = tokens.unwrap_or_default();
    Ok(tokens
        .into_iter()
        .map(|(tok, s)| (tok, Span::new(base + s.start, base + s.end)))
        .collect())
}

fn is_reserved_identifier(s: &str) -> bool {
    KEYWORDS.contains(&s) || RESERVED_TYPE_NAMES.contains(&s)
}

fn parse_ident(src: &str, span: Span) -> Result<String, Diag> {
    if src.is_empty() {
        return Err(parse_syntax("identifier cannot be empty", span));
    }
    let mut chars = src.chars();
    let first = chars.next().unwrap_or('_');
    if !is_ident_start(first) || !chars.all(is_ident_continue) {
        return Err(parse_syntax(&format!("invalid identifier `{src}`"), span));
    }
    if is_reserved_identifier(src) {
        return Err(parse_reserved_identifier(src, span));
    }
    Ok(src.to_string())
}

fn find_assignment(line: &str) -> Option<usize> {
    // Walk bytes looking for a bare `=` that is not part of `==`, `<=`, `>=`, or `!=`.
    // We stay in the ASCII fast-path: all operator characters involved are single-byte.
    let bytes = line.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b'=' {
            let prev = i.checked_sub(1).map(|j| bytes[j]);
            let next = bytes.get(i + 1).copied();
            let is_compound = matches!(prev, Some(b'=' | b'<' | b'>' | b'!')) || next == Some(b'=');
            if !is_compound {
                return Some(i);
            }
        }
    }
    None
}

pub fn parse_dsl_with_errors(src: &str) -> Result<FuncDef, Vec<Diag>> {
    let cleaned = strip_line_comments_preserve_len(src);
    parse_dsl_inner_with_errors(&cleaned)
}

fn parse_dsl_inner_with_errors(src: &str) -> Result<FuncDef, Vec<Diag>> {
    let start = src.find('|').ok_or_else(|| {
        vec![parse_syntax(
            "expected function header `|...|`",
            Span::new(0, 0),
        )]
    })?;
    let after_start = start + 1;
    let end_rel = src[after_start..].find('|').ok_or_else(|| {
        vec![parse_syntax(
            "expected closing `|` for function header",
            Span::new(after_start, after_start),
        )]
    })?;
    let end = after_start + end_rel;

    let params_src = &src[after_start..end];
    let mut params = Vec::new();
    if !params_src.trim().is_empty() {
        let mut cursor = after_start;
        for chunk in params_src.split(',') {
            let trimmed = chunk.trim();
            let leading_ws = chunk.len() - chunk.trim_start().len();
            let chunk_start = cursor + leading_ws;
            let chunk_end = chunk_start + trimmed.len();
            let (name, dtype_str) = trimmed.split_once(':').ok_or_else(|| {
                vec![parse_syntax(
                    "invalid parameter, expected `name: dtype`",
                    Span::new(chunk_start, chunk_end),
                )]
            })?;
            let name_trimmed = name.trim();
            let dtype_trimmed = dtype_str.trim();
            let name_span = Span::new(chunk_start, chunk_start + name_trimmed.len());
            let dtype_span = Span::new(chunk_end - dtype_trimmed.len(), chunk_end);
            let ident = parse_ident(name_trimmed, name_span).map_err(|d| vec![d])?;
            let dtype = DType::from_str(dtype_trimmed)
                .map_err(|_| vec![parse_unknown_dtype(dtype_trimmed, dtype_span)])?;
            params.push((ident, dtype));
            cursor += chunk.len() + 1;
        }
    }

    let body_src = &src[end + 1..];
    let body_base = end + 1;
    let mut lines = Vec::new();
    let mut offset = 0usize;
    for raw_line in body_src.split('\n') {
        let line_len = raw_line.len();
        let trimmed = raw_line.trim();
        if !trimmed.is_empty() {
            let leading_ws = raw_line.len() - raw_line.trim_start().len();
            let start = body_base + offset + leading_ws;
            let end = start + trimmed.len();
            lines.push((trimmed.to_string(), start, end));
        }
        offset += line_len + 1;
    }

    if lines.is_empty() {
        return Err(vec![parse_syntax(
            "missing function body",
            Span::new(body_base, body_base),
        )]);
    }

    let (last_line, last_start, last_end) = lines.last().cloned().expect("non-empty");
    if find_assignment(&last_line).is_some() {
        return Err(vec![parse_syntax(
            "missing result expression after assignments",
            Span::new(last_start, last_end),
        )]);
    }
    let mut body = parse_expr_with_offset(&last_line, last_start)?;

    for (line, line_start, line_end) in lines[..lines.len() - 1].iter().rev() {
        let eq_pos = find_assignment(line).ok_or_else(|| {
            vec![parse_syntax(
                "statement must be assignment `name = expr`",
                Span::new(*line_start, *line_end),
            )]
        })?;
        let (lhs, rhs_with_eq) = line.split_at(eq_pos);
        let rhs = &rhs_with_eq[1..];
        let lhs_trimmed = lhs.trim();
        let rhs_trimmed = rhs.trim();
        if lhs_trimmed.is_empty() || rhs_trimmed.is_empty() {
            return Err(vec![parse_syntax(
                "invalid assignment",
                Span::new(*line_start, *line_end),
            )]);
        }
        let lhs_leading_ws = lhs.len() - lhs.trim_start().len();
        let lhs_start = line_start + lhs_leading_ws;
        let lhs_span = Span::new(lhs_start, lhs_start + lhs_trimmed.len());
        let name = parse_ident(lhs_trimmed, lhs_span).map_err(|d| vec![d])?;

        let rhs_start = line_start + eq_pos + 1 + (rhs.len() - rhs.trim_start().len());
        let value = parse_expr_with_offset(rhs_trimmed, rhs_start)?;

        let let_span = Span::new(*line_start, body.span.end);
        body = Spanned::new(Expr::Let(name, Box::new(value), Box::new(body)), let_span);
    }

    Ok(FuncDef { params, body })
}

pub fn parse_dsl(src: &str) -> Result<FuncDef, Diag> {
    parse_dsl_with_errors(src).map_err(|mut diags| {
        // Preserve the old API by returning the first error.
        diags
            .drain(..)
            .next()
            .unwrap_or_else(|| parse_syntax("parse error", Span::new(0, 0)))
    })
}

#[cfg(test)]
mod tests {
    use super::{find_assignment, parse_dsl};
    use crate::diag::DiagCode;
    use crate::ir::BinaryOp;
    use crate::parse::ast::Expr;

    #[test]
    fn parse_rejects_int_imaginary() {
        let err = parse_dsl("||\n2j").expect_err("should reject 2j");
        assert_eq!(err.code, DiagCode::ParseIntImaginary);
    }

    #[test]
    fn parse_tuple_requires_two_items() {
        let err = parse_dsl("||\n(1,)").expect_err("tuple needs >= 2 items");
        assert_eq!(err.code, DiagCode::ParseSyntax);
    }

    #[test]
    fn find_assignment_rejects_compound_operators() {
        // None of these should be detected as assignment `=`.
        assert_eq!(find_assignment("a <= b"), None, "<=");
        assert_eq!(find_assignment("a >= b"), None, ">=");
        assert_eq!(find_assignment("a != b"), None, "!=");
        assert_eq!(find_assignment("a == b"), None, "==");
    }

    #[test]
    fn find_assignment_detects_bare_equals() {
        assert_eq!(find_assignment("x = 1 + 2"), Some(2));
        assert_eq!(find_assignment("foo = bar"), Some(4));
    }

    #[test]
    fn parse_comparison_body_not_misparsed_as_assignment() {
        // `a <= b` as the result expression must parse fine; if find_assignment
        // wrongly matched the `=` the body-parse would fail.
        let src = "|a: i32, b: i32|\na <= b";
        let func = parse_dsl(src).expect("a <= b should be valid result expression");
        let Expr::Binary(op, _, _) = func.body.node else {
            panic!("expected binary expression");
        };
        assert_eq!(op, BinaryOp::Le);
    }

    #[test]
    fn parse_comparison_chaining_errors_on_second_op() {
        let src = "||\n1 < 2 < 3";
        let err = parse_dsl(src).expect_err("comparison chaining should fail");
        assert_eq!(err.code, DiagCode::ParseSyntax);
    }

    #[test]
    fn parse_unary_binds_weaker_than_pow() {
        let func = parse_dsl("||\n-8**2").expect("parse");
        let expr = func.body.node;
        let Expr::Unary(_, child) = expr else {
            panic!("expected unary at top-level")
        };
        assert!(matches!(child.node, Expr::Binary(BinaryOp::Pow, _, _)));
    }

    #[test]
    fn parse_unknown_dtype_reports_error() {
        let err = parse_dsl("|x: foo|\nx").expect_err("unknown dtype should error");
        assert_eq!(err.code, DiagCode::ParseUnknownDType);
    }

    #[test]
    fn parse_ignores_full_line_comment() {
        let src = "||\n# this is a comment\n1 + 2";
        let func = parse_dsl(src).expect("comments should be ignored");
        let Expr::Binary(_, _, _) = func.body.node else {
            panic!("expected binary expression as body");
        };
    }

    #[test]
    fn parse_ignores_trailing_comment() {
        let src = "||\nx = 1 # assign one\nx";
        let func = parse_dsl(src).expect("trailing comments should be ignored");
        let Expr::Let(_, _, body) = func.body.node else {
            panic!("expected let binding");
        };
        let Expr::Var(name) = &body.node else {
            panic!("expected variable reference");
        };
        assert_eq!(name, "x");
    }

    #[test]
    fn parse_does_not_error_on_int_imaginary_in_comment() {
        let src = "||\n# 2j should be allowed in comments\n1";
        parse_dsl(src).expect("int imaginary in comment should be ignored");
    }

    #[test]
    fn parse_rejects_reserved_type_name_as_param() {
        let err = parse_dsl("|f64: i32|\n1").expect_err("f64 cannot be param name");
        assert_eq!(err.code, DiagCode::ParseReservedIdentifier);
    }

    #[test]
    fn parse_rejects_keyword_as_param() {
        let err = parse_dsl("|if: bool|\ntrue").expect_err("if cannot be param name");
        assert_eq!(err.code, DiagCode::ParseReservedIdentifier);
    }

    #[test]
    fn parse_rejects_reserved_type_name_as_let_lhs() {
        // Result expression must parse first; use "x" so the error comes from let LHS "f64"
        let err = parse_dsl("||\nf64 = 1\nx").expect_err("f64 cannot be let binding name");
        assert_eq!(err.code, DiagCode::ParseReservedIdentifier);
    }

    #[test]
    fn parse_accepts_f64_in_type_position() {
        let func = parse_dsl("|x: f64|\nx").expect("f64 as type is allowed");
        assert_eq!(func.params.len(), 1);
        assert_eq!(func.params[0].0, "x");
        assert_eq!(func.params[0].1.to_string(), "f64");
    }
}
