// parser.v — recursive-descent parser for the VuurRaaf language.
//
// Grammar (informal):
//   program  := import* (struct | enum | const | fn)*
//   import   := 'import' STRING | 'import' IDENT
//   struct   := 'struct' IDENT '{' [IDENT (',' IDENT)*] '}'
//   enum     := 'enum' IDENT '{' [IDENT (',' IDENT)*] '}'
//   const    := 'const' IDENT '=' expr
//   fn       := 'fn' [ '(' IDENT IDENT ')' ] IDENT '(' [IDENT (',' IDENT)*] ')' block
//   block    := '{' stmt* '}'
//   stmt     := 'let' IDENT '=' expr
//             | IDENT '=' expr
//             | postfix '=' expr            (a[i] = v, a.b = v)
//             | 'if' cond block ['else' ('if' ... | block)]
//             | 'match' expr '{' (expr block | 'else' block)* '}'
//             | 'while' cond block
//             | 'for' IDENT 'in' range block        (range := expr '..' expr | expr '...' expr)
//             | 'for' IDENT 'in' expr block          (iterate an array)
//             | 'break' | 'continue'
//             | 'return' [expr]
//             | 'assert' expr
//             | expr
//   cond     := ['('] expr [')']   // parens optional
//   expr     := or ('or' or)*
//   or       := and ('and' and)*
//   and      := eq (('=='|'!=') eq)*
//   eq       := rel (('<'|'<='|'>'|'>=') rel)*
//   rel      := add (('+'|'-') add)*
//   add      := mul (('*'|'/'|'%') mul)*
//   mul      := ('not'|'-') mul | primary
//   primary  := INT | STR | 'true' | 'false' | IDENT ['(' args ')'] | '(' expr ')'
//             | '{' [IDENT ':' expr (',' IDENT ':' expr)*] '}'   (anonymous struct literal)
//             | IDENT '{' IDENT ':' expr ... '}'                  (typed struct literal)
//   postfix  := primary ('.' IDENT ['(' args ')'] | '[' expr ']')*
//             ('.' IDENT '(' ... ')' is a method call; everything else field access)
module compiler

pub fn parse(toks []Tok) !Program {
	mut p := Parser{ toks: toks }
	return p.parse_program()
}

struct Parser {
mut:
	toks []Tok
	pos  int
}

fn (mut p Parser) cur() Tok {
	if p.pos < p.toks.len {
		return p.toks[p.pos]
	}
	return p.toks[p.toks.len - 1]
}

fn (mut p Parser) advance() Tok {
	t := p.cur()
	if p.pos < p.toks.len - 1 {
		p.pos++
	}
	return t
}

fn (mut p Parser) expect(k TokKind, what string) !Tok {
	t := p.cur()
	if t.kind != k {
		return error('expected ${what}, got "${t.lit}" at line ${t.line}, col ${t.col}')
	}
	return p.advance()
}

// parse_cond parses a condition, accepting either `if cond {` or `if (cond) {`.
fn (mut p Parser) parse_cond() !Expr {
	if p.cur().kind == .lparen {
		p.advance()
		e := p.parse_expr()!
		p.expect(.rparen, "')'")!
		return e
	}
	return p.parse_expr()!
}

fn (mut p Parser) parse_program() !Program {
	mut prog := Program{}
	// imports come first
	for p.cur().kind == .kw_import {
		prog.imports << p.parse_import()!
	}
	// then top-level declarations
	for p.cur().kind != .eof {
		match p.cur().kind {
			.kw_struct { prog.structs << p.parse_struct_decl()! }
			.kw_enum { prog.enums << p.parse_enum_decl()! }
			.kw_const { prog.consts << p.parse_const_decl()! }
			.kw_interface { prog.interfaces << p.parse_interface_decl()! }
			.kw_fn { prog.fns << p.parse_fn()! }
			else { return error('unexpected token "${p.cur().lit}" at line ${p.cur().line}, col ${p.cur().col}') }
		}
	}
	if prog.fns.len == 0 {
		return error('no functions found in source')
	}
	return prog
}

// parse_struct_decl parses `struct Name { a, b, c }`.
fn (mut p Parser) parse_struct_decl() !StructDecl {
	t := p.expect(.kw_struct, "'struct'")!
	name := p.expect(.ident, 'struct name')!
	p.expect(.lbrace, "'{'")!
	mut fields := []string{}
	if p.cur().kind != .rbrace {
		for {
			fields << p.expect(.ident, 'field name')!.lit
			if p.cur().kind == .comma {
				p.advance()
				continue
			}
			break
		}
	}
	p.expect(.rbrace, "'}'")!
	return StructDecl{ name: name.lit, fields: fields, line: t.line }
}

// parse_import parses `import "path/to/file.vr"` (flat file merge) or a
// bare module name like `import os` (namespaced stdlib/vendor module).
fn (mut p Parser) parse_import() !ImportDecl {
	t := p.expect(.kw_import, "'import'")!
	if p.cur().kind == .str_lit {
		path := p.advance().lit
		return ImportDecl{ path: path, line: t.line }
	}
	name := p.expect(.ident, 'module name')!.lit
	return ImportDecl{ name: name, path: name, line: t.line }
}

// parse_enum_decl parses `enum Name { variant1 variant2 ... }`.
// Variants are separated by commas or newlines.
fn (mut p Parser) parse_enum_decl() !EnumDecl {
	t := p.expect(.kw_enum, "'enum'")!
	name := p.expect(.ident, 'enum name')!
	p.expect(.lbrace, "'{'")!
	mut variants := []string{}
	if p.cur().kind != .rbrace {
		for {
			variants << p.expect(.ident, 'variant name')!.lit
			if p.cur().kind == .comma {
				p.advance()
				continue
			}
			if p.cur().kind != .rbrace {
				// expect another variant (newline-separated)
				continue
			}
			break
		}
	}
	p.expect(.rbrace, "'}'")!
	return EnumDecl{ name: name.lit, variants: variants, line: t.line }
}

// parse_const_decl parses `const NAME = expr`.
fn (mut p Parser) parse_const_decl() !ConstDecl {
	t := p.expect(.kw_const, "'const'")!
	name := p.expect(.ident, 'constant name')!
	p.expect(.assign, "'='")!
	value := p.parse_expr()!
	return ConstDecl{ name: name.lit, value: value, line: t.line }
}

// parse_interface_decl parses `interface Name { method1() method2() ... }`.
// Methods may optionally be followed by `()` (any parameter list is ignored)
// and are separated by commas or newlines.
fn (mut p Parser) parse_interface_decl() !InterfaceDecl {
	t := p.expect(.kw_interface, "'interface'")!
	name := p.expect(.ident, 'interface name')!
	p.expect(.lbrace, "'{'")!
	mut methods := []InterfaceMethod{}
	if p.cur().kind != .rbrace {
		for {
			mname := p.expect(.ident, 'method name')!
			if p.cur().kind == .lparen {
				p.advance()
				for p.cur().kind != .rparen {
					if p.cur().kind == .eof {
						return error('unexpected end of file in interface method (line ${mname.line})')
					}
					p.advance()
				}
				p.expect(.rparen, "')'")!
			}
			methods << InterfaceMethod{ name: mname.lit, line: mname.line }
			if p.cur().kind == .comma {
				p.advance()
				continue
			}
			if p.cur().kind != .rbrace {
				continue
			}
			break
		}
	}
	p.expect(.rbrace, "'}'")!
	return InterfaceDecl{ name: name.lit, methods: methods, line: t.line }
}

// parse_fn parses `fn name(params) { }` or a method `fn (p Type) name(params) { }`.
fn (mut p Parser) parse_fn() !FnDecl {
	fn_tok := p.expect(.kw_fn, "'fn'")!
	mut recv_name := ''
	mut recv_type := ''
	if p.cur().kind == .lparen {
		// method: fn (p Type) name(...)
		p.advance()
		recv_name = p.expect(.ident, 'receiver name')!.lit
		recv_type = p.expect(.ident, 'receiver type')!.lit
		p.expect(.rparen, "')'")!
	}
	name := p.expect(.ident, 'function name')!
	// generic type parameters: fn first[T, U](arr) { ... } — captured so the
	// type checker can validate call sites; the VM is dynamically typed, so
	// they erase to a single function at runtime
	mut type_params := []string{}
	if p.cur().kind == .lbracket {
		p.advance()
		for p.cur().kind != .rbracket {
			tp := p.expect(.ident, 'type parameter')!.lit
			if tp in type_params {
				return error('duplicate type parameter "${tp}" in function ${name.lit} (line ${name.line}, col ${name.col})')
			}
			type_params << tp
			if p.cur().kind == .comma {
				p.advance()
			}
		}
		p.expect(.rbracket, "']'")!
	}
	params, defaults, has_defs, variadic := p.parse_params()!
	body := p.parse_block()!
	return FnDecl{
		name: name.lit
		type_params: type_params
		recv_name: recv_name
		recv_type: recv_type
		params: params
		defaults: defaults
		has_defs: has_defs
		variadic: variadic
		body: body
		line: fn_tok.line
	}
}

// parse_params parses `(a, b, c = expr, rest...)` — the parameter list of a
// function. Returns the names, the default-value expressions (parallel array,
// empty Expr{} when no default), whether each has a default, and whether the
// last parameter is variadic.
fn (mut p Parser) parse_params() !([]string, []Expr, []bool, bool) {
	p.expect(.lparen, "'('")!
	mut params := []string{}
	mut defaults := []Expr{}
	mut has_defs := []bool{}
	mut variadic := false
	if p.cur().kind != .rparen {
		for {
			name_tok := p.expect(.ident, 'parameter name')!
			name := name_tok.lit
			if p.cur().kind == .dotdotdot {
				// variadic parameter: `rest...` (must be last)
				p.advance()
				params << name
				defaults << Expr{}
				has_defs << false
				variadic = true
				if p.cur().kind == .comma {
					return error('a variadic parameter must be last (line ${name_tok.line})')
				}
				break
			}
			mut def := Expr{}
			mut has_def := false
			if p.cur().kind == .assign {
				p.advance()
				def = p.parse_expr()!
				has_def = true
			}
			params << name
			defaults << def
			has_defs << has_def
			if p.cur().kind == .comma {
				p.advance()
				continue
			}
			break
		}
	}
	p.expect(.rparen, "')'")!
	return params, defaults, has_defs, variadic
}

fn (mut p Parser) parse_block() ![]Stmt {
	p.expect(.lbrace, "'{'")!
	mut stmts := []Stmt{}
	for p.cur().kind != .rbrace {
		if p.cur().kind == .eof {
			return error('unexpected end of file inside block (missing "}")')
		}
		stmts << p.parse_stmt()!
	}
	p.expect(.rbrace, "'}'")!
	return stmts
}

fn (mut p Parser) parse_stmt() !Stmt {
	t := p.cur()
	match t.kind {
		.kw_let {
			p.advance()
			// destructuring let: `let { a, b } = expr` or `let [a, b] = expr`
			if p.cur().kind == .lbrace || p.cur().kind == .lbracket {
				is_field := p.cur().kind == .lbrace
				p.advance()
				mut names := []string{}
				mut closing := TokKind.rbracket
				if is_field {
					closing = .rbrace
				}
				if p.cur().kind != closing {
					for {
						names << p.expect(.ident, 'binding name')!.lit
						if p.cur().kind == .comma {
							p.advance()
							continue
						}
						break
					}
				}
				p.expect(closing, "']' or '}'")!
				p.expect(.assign, "'='")!
				e := p.parse_expr()!
				return Stmt{ kind: .destruct_stmt, expr: e, destruct_targets: names, destruct_field: is_field, line: t.line }
			}
			name := p.expect(.ident, 'variable name')!
			p.expect(.assign, "'='")!
			e := p.parse_expr()!
			return Stmt{ kind: .let_stmt, target: name.lit, expr: e, line: t.line }
		}		.kw_if {
			return p.parse_if(t)!
		}
		.kw_match {
			p.advance()
			subject := p.parse_expr()!
			p.expect(.lbrace, "'{'")!
			mut arms := []MatchArm{}
			mut has_else := false
			mut els_body := []Stmt{}
			for p.cur().kind != .rbrace {
				if p.cur().kind == .eof {
					return error('unexpected end of file inside match (missing "}")')
				}
				if p.cur().kind == .kw_else {
					p.advance()
					els_body = p.parse_block()!
					has_else = true
					continue
				}
				val := p.parse_expr()!
				body := p.parse_block()!
				arms << MatchArm{ val: val, body: body }
			}
			p.expect(.rbrace, "'}'")!
			return Stmt{ kind: .match_stmt, expr: subject, arms: arms, has_else: has_else, els_body: els_body, line: t.line }
		}
		.kw_while {
			p.advance()
			cond := p.parse_cond()!
			body := p.parse_block()!
			return Stmt{ kind: .while_stmt, cond: cond, body: body, line: t.line }
		}
		.kw_for {
			p.advance()
			name := p.expect(.ident, 'loop variable')!
			mut idx_name := ''
			mut val_name := name.lit
			if p.cur().kind == .comma {
				// for i, v in arr { ... }
				p.advance()
				idx_name = name.lit
				val_name = p.expect(.ident, 'loop value variable')!.lit
			}
			p.expect(.kw_in, "'in'")!
			first := p.parse_expr()!
			if p.cur().kind == .dotdot || p.cur().kind == .dotdotdot {
				inclusive := p.cur().kind == .dotdotdot
				p.advance()
				end := p.parse_expr()!
				body := p.parse_block()!
				return Stmt{ kind: .for_range_stmt, target: val_name, expr: first, cond: end, inclusive: inclusive, body: body, line: t.line }
			}
			body := p.parse_block()!
			return Stmt{ kind: .for_in_stmt, target: val_name, idx_target: idx_name, expr: first, body: body, line: t.line }
		}
		.kw_return {
			p.advance()
			mut e := Expr{}
			has_val := p.cur().kind != .rbrace
			if has_val {
				e = p.parse_expr()!
			}
			return Stmt{ kind: .ret_stmt, expr: e, has_val: has_val, line: t.line }
		}
		.kw_break {
			p.advance()
			return Stmt{ kind: .break_stmt, line: t.line }
		}
		.kw_continue {
			p.advance()
			return Stmt{ kind: .continue_stmt, line: t.line }
		}		.kw_assert {
			p.advance()
			mut e := Expr{}
			if p.cur().kind == .lparen {
				p.advance()
				e = p.parse_expr()!
				p.expect(.rparen, "')'")!
			} else {
				e = p.parse_expr()!
			}
			return Stmt{ kind: .assert_stmt, expr: e, line: t.line }
		}
		.kw_try {
			p.advance()
			return p.parse_try_stmt(t)!
		}
		.kw_throw {
			p.advance()
			e := p.parse_expr()!
			return Stmt{ kind: .throw_stmt, expr: e, line: t.line }
		}
		.ident {
			p.advance()
			// generic type args in statement position: first[int](...)
			mut type_args := []string{}
			if p.cur().kind == .lbracket && p.looks_like_generic_args() {
				type_args = p.parse_type_args()!
			}
			mut e := Expr{}
			if p.cur().kind == .lparen {
				// a call statement: foo(args), optionally chained foo().x
				e = p.parse_call(t, type_args)!
				e = p.parse_postfix_tail(e)!
			} else if type_args.len > 0 {
				return error('generic type arguments on a non-call "${t.lit}" (line ${t.line}, col ${t.col})')
			} else {
				e = p.parse_postfix_tail(Expr{ kind: .ident, name: t.lit, line: t.line })!
			}
		if p.cur().kind == .assign {
			// assignment to an ident, an index, or a field
			p.advance()
			rhs := p.parse_expr()!
			match e.kind {
				.ident {
					return Stmt{ kind: .assign_stmt, target: e.name, expr: rhs, line: t.line }
				}
				.index {
					return Stmt{ kind: .index_assign, base: *e.left, idx: *e.right, expr: rhs, line: t.line }
				}
				.field {
					return Stmt{ kind: .field_assign, base: *e.left, target: e.name, expr: rhs, line: t.line }
				}
				else {
					return error('cannot assign to this expression (line ${t.line})')
				}
			}
		}
		// compound assignment: x += expr, a[i] += expr, a.b += expr
		if p.cur().kind == .plus_eq || p.cur().kind == .minus_eq || p.cur().kind == .star_eq || p.cur().kind == .slash_eq {
			op_tok := p.advance()
			rhs := p.parse_expr()!
			bin_op := match op_tok.kind {
				.plus_eq { TokKind.plus }
				.minus_eq { TokKind.minus }
				.star_eq { TokKind.star }
				.slash_eq { TokKind.slash }
				else { return error('unexpected compound operator (line ${t.line})') }
			}
			// desugar: LHS op= RHS  →  LHS = LHS op RHS
			match e.kind {
				.ident {
					full_rhs := bin_node(bin_op, e, rhs, op_tok.line)
					return Stmt{ kind: .assign_stmt, target: e.name, expr: full_rhs, line: t.line }
				}
				.index {
					full_rhs := bin_node(bin_op, e, rhs, op_tok.line)
					return Stmt{ kind: .index_assign, base: *e.left, idx: *e.right, expr: full_rhs, line: t.line }
				}
				.field {
					full_rhs := bin_node(bin_op, e, rhs, op_tok.line)
					return Stmt{ kind: .field_assign, base: *e.left, target: e.name, expr: full_rhs, line: t.line }
				}
				else {
					return error('cannot use compound assignment on this expression (line ${t.line})')
				}
			}
		}
		return Stmt{ kind: .expr_stmt, expr: e, line: t.line }
		}
		.kw_print, .kw_println {
			p.advance()
			e := p.parse_call(t, [])!
			return Stmt{ kind: .expr_stmt, expr: e, line: t.line }
		}
		else {
			return error('unexpected token "${t.lit}" at line ${t.line}, col ${t.col}')
		}
	}
}

// parse_if parses `if cond block ['else' ('if' ... | block)]`. An `else if`
// chain is represented by putting the nested if-statement in the else list,
// so codegen needs no special casing.
fn (mut p Parser) parse_if(t Tok) !Stmt {
	p.advance()
	cond := p.parse_cond()!
	body := p.parse_block()!
	mut els := []Stmt{}
	if p.cur().kind == .kw_else {
		p.advance()
		if p.cur().kind == .kw_if {
			els << p.parse_if(p.cur())!
		} else {
			els = p.parse_block()!
		}
	}
	return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line }
}

// parse_try_stmt parses `try { body } catch ident { body }`.
fn (mut p Parser) parse_try_stmt(t Tok) !Stmt {
	body := p.parse_block()!
	p.expect(.kw_catch, "'catch'")!
	catch_var := p.expect(.ident, 'error variable')!.lit
	catch_body := p.parse_block()!
	return Stmt{
		kind: .try_stmt
		body: body
		target: catch_var
		els: catch_body
		line: t.line
	}
}

// bin_node allocates a binary-operator node. It takes copies of the operands
// so that `&l`/`&r` target fresh heap objects (taking the address of a local
// that is later reassigned would create a self-referential node).
fn bin_node(op TokKind, left Expr, right Expr, line int) Expr {
	mut l := left
	mut r := right
	return Expr{ kind: .binary, op: op, left: &l, right: &r, line: line }
}

fn unary_node(op TokKind, operand Expr, line int) Expr {
	mut o := operand
	return Expr{ kind: .unary, op: op, right: &o, line: line }
}

// index_node builds `base[idx]`.
fn index_node(base Expr, idx Expr, line int) Expr {
	mut b := base
	mut i := idx
	return Expr{ kind: .index, left: &b, right: &i, line: line }
}

// slice_node builds `base[start..end]`.
// end uses -1 as sentinel for "open-ended" (slice to end).
fn slice_node(base Expr, start Expr, end Expr, _inclusive bool, line int) Expr {
	mut b := base
	mut s := start
	mut e := end
	return Expr{ kind: .slice, left: &b, right: &s, extra: &e, line: line }
}

// field_node builds `base.name`.
fn field_node(base Expr, name string, line int) Expr {
	mut b := base
	return Expr{ kind: .field, left: &b, name: name, line: line }
}

// method_node builds `base.name(args)`.
fn method_node(recv Expr, name string, args []Expr, line int) Expr {
	mut r := recv
	return Expr{ kind: .method_call, left: &r, name: name, args: args, line: line }
}

fn (mut p Parser) parse_expr() !Expr {
	return p.parse_or()!
}

fn (mut p Parser) parse_or() !Expr {
	mut e := p.parse_and()!
	for p.cur().kind == .kw_or {
		op := p.advance()
		rhs := p.parse_and()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

fn (mut p Parser) parse_and() !Expr {
	mut e := p.parse_bitor()!
	for p.cur().kind == .kw_and {
		op := p.advance()
		rhs := p.parse_bitor()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

// parse_bitor handles `|` (bitwise OR).
fn (mut p Parser) parse_bitor() !Expr {
	mut e := p.parse_bitxor()!
	for p.cur().kind == .pipe {
		op := p.advance()
		rhs := p.parse_bitxor()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

// parse_bitxor handles `^` (bitwise XOR).
fn (mut p Parser) parse_bitxor() !Expr {
	mut e := p.parse_bitand()!
	for p.cur().kind == .caret {
		op := p.advance()
		rhs := p.parse_bitand()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

// parse_bitand handles `&` (bitwise AND).
fn (mut p Parser) parse_bitand() !Expr {
	mut e := p.parse_eq()!
	for p.cur().kind == .amp {
		op := p.advance()
		rhs := p.parse_eq()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

fn (mut p Parser) parse_eq() !Expr {
	mut e := p.parse_rel()!
	for p.cur().kind == .eq_eq || p.cur().kind == .not_eq {
		op := p.advance()
		rhs := p.parse_rel()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

fn (mut p Parser) parse_rel() !Expr {
	mut e := p.parse_shift()!
	for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge {
		op := p.advance()
		rhs := p.parse_shift()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

// parse_shift handles `<<` and `>>` (bitwise shift).
fn (mut p Parser) parse_shift() !Expr {
	mut e := p.parse_add()!
	for p.cur().kind == .lt_lt || p.cur().kind == .gt_gt {
		op := p.advance()
		rhs := p.parse_add()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

fn (mut p Parser) parse_add() !Expr {
	mut e := p.parse_mul()!
	for p.cur().kind == .plus || p.cur().kind == .minus {
		op := p.advance()
		rhs := p.parse_mul()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

fn (mut p Parser) parse_mul() !Expr {
	mut e := p.parse_unary()!
	for p.cur().kind == .star || p.cur().kind == .slash || p.cur().kind == .percent {
		op := p.advance()
		rhs := p.parse_unary()!
		e = bin_node(op.kind, e, rhs, op.line)
	}
	return e
}

fn (mut p Parser) parse_unary() !Expr {
	t := p.cur()
	if t.kind == .kw_not || t.kind == .minus || t.kind == .tilde {
		p.advance()
		e := p.parse_unary()!
		return unary_node(t.kind, e, t.line)
	}
	return p.parse_postfix()!
}

// parse_postfix handles postfix operators after a primary: indexing
// `a[i]` (chainable `a[i][j]`) and field access `a.b` (chainable `a.b.c`),
// in any mix: `a[i].b`, `a.b[i]`, ...
fn (mut p Parser) parse_postfix() !Expr {
	mut e := p.parse_primary()!
	return p.parse_postfix_tail(e)!
}

// parse_postfix_tail continues a postfix chain from an already-parsed base.
// It takes the base by value (Expr only holds pointers to heap-allocated
// child nodes) and returns the extended chain. A `.name(` is a method call
// (the receiver is the expression the dot was applied to); `.name` without
// parens is plain field access.
fn (mut p Parser) parse_postfix_tail(e Expr) !Expr {
	mut cur := e
	for {
		if p.cur().kind == .lbracket {
			p.advance()
			idx := p.parse_expr()!
			if p.cur().kind == .dotdot || p.cur().kind == .dotdotdot {
				// arr[start..end] or arr[start..] slicing
				p.advance()
				if p.cur().kind == .rbracket {
					// arr[start..] — slice to end
					p.advance()
					cur = slice_node(cur, idx, Expr{ kind: .int_lit, int_v: -1, line: cur.line }, false, cur.line)
				} else {
					end := p.parse_expr()!
					p.expect(.rbracket, "']'")!
					cur = slice_node(cur, idx, end, false, cur.line)
				}
			} else {
				p.expect(.rbracket, "']'")!
				cur = index_node(cur, idx, cur.line)
			}
			continue
		}
		if p.cur().kind == .dot {
			p.advance()
			name := p.expect(.ident, 'field name')!
			f := field_node(cur, name.lit, cur.line)
			if p.cur().kind == .lparen {
				args := p.parse_args()!
				cur = method_node(*f.left, f.name, args, f.line)
			} else {
				cur = f
			}
			continue
		}
		break
	}
	return cur
}

fn (mut p Parser) parse_primary() !Expr {
	t := p.cur()
	match t.kind {
		.int_lit {
			p.advance()
			return Expr{ kind: .int_lit, int_v: t.lit.i64(), line: t.line }
		}
		.float_lit {
			p.advance()
			return Expr{ kind: .float_lit, float_v: t.lit.f64(), line: t.line }
		}
		.str_lit {
			p.advance()
			return Expr{ kind: .str_lit, str_v: t.lit, line: t.line }
		}
		.str_interp {
			p.advance()
			return p.parse_str_interp(t)!
		}
		.kw_true {
			p.advance()
			return Expr{ kind: .bool_lit, int_v: 1, line: t.line }
		}
		.kw_false {
			p.advance()
			return Expr{ kind: .bool_lit, int_v: 0, line: t.line }
		}
		.kw_none {
			p.advance()
			return Expr{ kind: .none_lit, line: t.line }
		}
		.lparen {
			p.advance()
			e := p.parse_expr()!
			p.expect(.rparen, "')'")!
			return e
		}
		.lbracket {
			p.advance()
			mut elems := []Expr{}
			if p.cur().kind != .rbracket {
				for {
					elems << p.parse_expr()!
					if p.cur().kind == .comma {
						p.advance()
						continue
					}
					break
				}
			}
			p.expect(.rbracket, "']'")!
			return Expr{ kind: .array_lit, elems: elems, line: t.line }
		}
		.lbrace {
			// anonymous struct literal: { name: expr, ... }
			fields := p.parse_struct_fields()!
			return Expr{ kind: .struct_lit, name: '', fields: fields, line: t.line }
		}
		.ident {
			p.advance()
			if p.cur().kind == .lbrace && p.looks_like_struct_lit() {
				// typed struct literal: Name{ name: expr, ... } — only when the
				// brace clearly opens a field list (`{ ident :`), so `if x {` and
				// match arms like `x { ... }` still parse as blocks
				fields := p.parse_struct_fields()!
				return Expr{ kind: .struct_lit, name: t.lit, fields: fields, line: t.line }
			}
			return p.parse_call_or_ident(t)!
		}
		.kw_print, .kw_println {
			p.advance()
			return p.parse_call(t, [])!
		}
		.kw_fn {
			// anonymous function expression: fn(params) { body }
			p.advance()
			return p.parse_anon_fn(t)!
		}
		else {
			return error('unexpected token "${t.lit}" at line ${t.line}, col ${t.col}')
		}
	}
}

fn (mut p Parser) parse_call_or_ident(t Tok) !Expr {
	// generic type args: first[int](...) — captured for the type checker;
	// only treated as type args when [ is followed by idents then ] then (
	mut type_args := []string{}
	if p.cur().kind == .lbracket && p.looks_like_generic_args() {
		type_args = p.parse_type_args()!
	}
	if p.cur().kind == .lparen {
		return p.parse_call(t, type_args)!
	}
	if type_args.len > 0 {
		return error('generic type arguments on a non-call "${t.lit}" (line ${t.line}, col ${t.col})')
	}
	return Expr{ kind: .ident, name: t.lit, line: t.line }
}

// parse_type_args parses `[int, string]` into a list of type names.
fn (mut p Parser) parse_type_args() ![]string {
	p.advance() // consume '['
	mut args := []string{}
	for p.cur().kind != .rbracket {
		args << p.expect(.ident, 'type argument')!.lit
		if p.cur().kind == .comma {
			p.advance()
		}
	}
	p.expect(.rbracket, "']'")!
	return args
}

fn (mut p Parser) parse_call(name Tok, type_args []string) !Expr {
	args := p.parse_args()!
	return Expr{ kind: .call, name: name.lit, type_args: type_args, args: args, line: name.line }
}

// parse_args parses `(e1, e2, ...)` and returns the argument expressions.
fn (mut p Parser) parse_args() ![]Expr {
	p.expect(.lparen, "'('")!
	mut args := []Expr{}
	if p.cur().kind != .rparen {
		for {
			args << p.parse_expr()!
			if p.cur().kind == .comma {
				p.advance()
				continue
			}
			break
		}
	}
	p.expect(.rparen, "')'")!
	return args
}

// parse_struct_fields parses `{ name: expr, ... }` or `{ "key": expr, ... }` and returns the fields.
fn (mut p Parser) parse_struct_fields() ![]StructField {
	p.expect(.lbrace, "'{'")!
	mut fields := []StructField{}
	if p.cur().kind != .rbrace {
		for {
			// field name can be an identifier or a string literal (for maps)
			mut fname := ''
			if p.cur().kind == .str_lit {
				fname = p.advance().lit
			} else {
				fname = p.expect(.ident, 'field name')!.lit
			}
			p.expect(.colon, "':'")!
			val := p.parse_expr()!
			fields << StructField{ name: fname, val: val }
			if p.cur().kind == .comma {
				p.advance()
				continue
			}
			break
		}
	}
	p.expect(.rbrace, "'}'")!
	return fields
}

// looks_like_struct_lit reports whether the current token (`{`) opens a typed
// struct literal: the tokens after the brace must be `ident :`.
fn (mut p Parser) looks_like_struct_lit() bool {
	if p.pos + 2 >= p.toks.len {
		return false
	}
	// typed struct literal: `{ ident :` or map literal: `{ "key" :`
	return (p.toks[p.pos + 1].kind == .ident || p.toks[p.pos + 1].kind == .str_lit) && p.toks[p.pos + 2].kind == .colon
}

// looks_like_generic_args checks if `[` starts generic type args like `[T]` or `[T, U]`
// rather than array indexing. It peeks ahead to see `ident ... ] (`.
fn (mut p Parser) looks_like_generic_args() bool {
	// current token must be lbracket (the caller already checked this)
	if p.pos + 2 >= p.toks.len {
		return false
	}
	// must start with an ident after the [
	if p.toks[p.pos + 1].kind != .ident {
		return false
	}
	// scan forward: ident, comma, ident, ..., rbracket, then lparen
	mut i := p.pos + 2
	for i < p.toks.len && p.toks[i].kind != .rbracket {
		if p.toks[i].kind != .ident && p.toks[i].kind != .comma {
			return false
		}
		i++
	}
	if i >= p.toks.len {
		return false
	}
	// p.toks[i] should be rbracket
	if i + 1 >= p.toks.len {
		return false
	}
	return p.toks[i + 1].kind == .lparen
}

// parse_str_interp handles string interpolation: "hello ${name} ${age}".
// It splits the raw string into alternating text/expression parts and builds
// a chain of + concatenations so the compiler needs no special handling.
fn (mut p Parser) parse_str_interp(t Tok) !Expr {
	parts := split_str_interp(t.lit)
	// parts alternates: text, expr, text, expr, ..., text
	if parts.len == 1 {
		return Expr{ kind: .str_lit, str_v: parts[0], line: t.line }
	}
	// build the first string part
	mut result := Expr{ kind: .str_lit, str_v: parts[0], line: t.line }
	mut i := 1
	for i < parts.len {
		// parts[i] is an expression — tokenise and parse it
		expr_src := parts[i]
		expr_toks := tokenize(expr_src)!
		mut ep := Parser{ toks: expr_toks }
		expr := ep.parse_expr()!
		result = bin_node(.plus, result, expr, t.line)
		i++
		// parts[i] is the next text fragment
		if i < parts.len {
			if parts[i].len > 0 {
				str_e := Expr{ kind: .str_lit, str_v: parts[i], line: t.line }
				result = bin_node(.plus, result, str_e, t.line)
			}
			i++
		}
	}
	return result
}

// parse_anon_fn parses an anonymous function expression: `fn(params) { body }`.
fn (mut p Parser) parse_anon_fn(t Tok) !Expr {
	params, defaults, has_defs, variadic := p.parse_params()!
	body := p.parse_block()!
	return Expr{
		kind: .anon_fn
		fparams: params
		fdefaults: defaults
		fhas_defs: has_defs
		fvariadic: variadic
		fn_body: body
		line: t.line
	}
}

// split_str_interp splits an interpolated string at ${...} boundaries.
// Returns alternating [text, expr, text, expr, ..., text] fragments.
fn split_str_interp(s string) []string {
	mut parts := []string{}
	mut i := 0
	mut current := ''
	for i < s.len {
		if s[i] == `$` && i + 1 < s.len && s[i + 1] == `{` {
		parts << current
		current = ''
		i += 2
		mut depth := 1
		mut expr := ''
		for i < s.len && depth > 0 {
			if s[i] == `{` {
				depth++
			} else if s[i] == `}` {
				depth--
			}
			if depth > 0 {
				expr += s[i].ascii_str()
			}
			i++
		}
		parts << expr
	} else {
		current += s[i].ascii_str()
		i++
	}
}
	parts << current
	return parts
}
