local tokens = require("../../tokens")
local nodes = require("../../ast/nodes")
local precedenceModule = require("../precedence")

export type Token = tokens.Token
export type Expr = nodes.Expr
export type CmpOp = nodes.CmpOp

export type BinaryDeps = {
	precedence: typeof(precedenceModule),
	parsePrimary: (state: any) -> Expr,
	parseLambda: (state: any) -> Expr,
}

local function lookahead(state: any, offset: number): Token?
	local idx = state.pos + offset
	if idx >= 1 and idx <= #state.tokens then
		return state.tokens[idx]
	end
	return nil
end

local function getCompareOp(tokenType: string, nextTok: Token?): CmpOp?
	if tokenType == "EQEQUAL" then
		return "Eq" :: CmpOp
	end
	if tokenType == "NOTEQUAL" then
		return "NotEq" :: CmpOp
	end
	if tokenType == "LESS" then
		return "Lt" :: CmpOp
	end
	if tokenType == "LESSEQUAL" then
		return "LtE" :: CmpOp
	end
	if tokenType == "GREATER" then
		return "Gt" :: CmpOp
	end
	if tokenType == "GREATEREQUAL" then
		return "GtE" :: CmpOp
	end
	if tokenType == "IS" then
		if nextTok and nextTok.type == "NOT" then
			return "IsNot" :: CmpOp
		end
		return "Is" :: CmpOp
	end
	if tokenType == "IN" then
		return "In" :: CmpOp
	end
	if tokenType == "NOT" and nextTok and nextTok.type == "IN" then
		return "NotIn" :: CmpOp
	end
	return nil
end

local function createBinaryModule(deps: BinaryDeps)
	local precedence = deps.precedence
	local PRECEDENCE = precedence.LEVELS
	local BINOP_MAP = precedence.BINOP_MAP
	local parsePrimary = deps.parsePrimary
	local parseLambda = deps.parseLambda
	local parseBinaryExpression: (state: any, minPrec: number) -> Expr

	local function isYieldTerminator(tokenType: string): boolean
		return tokenType == "COMMA"
			or tokenType == "COLON"
			or tokenType == "RPAR"
			or tokenType == "RSQB"
			or tokenType == "RBRACE"
			or tokenType == "NEWLINE"
			or tokenType == "ENDMARKER"
			or tokenType == "DEDENT"
			or tokenType == "SEMICOLON"
	end

	local function parseYieldExpression(state: any): Expr
		local yieldToken: Token = state.current :: Token
		state:advance()
		if state.current and state.current.type == "FROM" then
			state:advance()
			local valueExpr = parseBinaryExpression(state, precedence.LAMBDA)
			return {
				type = "YieldFrom",
				value = valueExpr,
				lineno = yieldToken.line,
				col_offset = yieldToken.column,
			}
		end
		local valueExpr: Expr? = nil
		if state.current and not isYieldTerminator(state.current.type) then
			valueExpr = parseBinaryExpression(state, precedence.LAMBDA)
		end
		return {
			type = "Yield",
			value = valueExpr,
			lineno = yieldToken.line,
			col_offset = yieldToken.column,
		}
	end

	local function parseUnary(state: any): Expr
		local current: Token? = state.current
		if not current then
			return parsePrimary(state)
		end
		if current.type == "YIELD" then
			return parseYieldExpression(state)
		end
		if current.type == "MINUS" then
			state:advance()
			local operand = parseUnary(state)
			return {
				type = "UnaryOp",
				op = "USub",
				operand = operand,
				lineno = current.line,
				col_offset = current.column,
			}
		end
		if current.type == "PLUS" then
			state:advance()
			local operand = parseUnary(state)
			return {
				type = "UnaryOp",
				op = "UAdd",
				operand = operand,
				lineno = current.line,
				col_offset = current.column,
			}
		end
		if current.type == "TILDE" then
			state:advance()
			local operand = parseUnary(state)
			return {
				type = "UnaryOp",
				op = "Invert",
				operand = operand,
				lineno = current.line,
				col_offset = current.column,
			}
		end
		if current.type == "NOT" then
			state:advance()
			local operand = parseUnary(state)
			return {
				type = "UnaryOp",
				op = "Not",
				operand = operand,
				lineno = current.line,
				col_offset = current.column,
			}
		end
		return parsePrimary(state)
	end

	local function parsePrefix(state: any, _minPrec: number): Expr
		local current: Token? = state.current
		if current and current.type == "LAMBDA" then
			return parseLambda(state)
		end
		return parseUnary(state)
	end

	parseBinaryExpression = function(state: any, minPrec: number): Expr
		local left = parsePrefix(state, minPrec)

		local function foldComparison(base: Expr, firstTok: Token, firstOp: CmpOp): Expr
			local ops = { firstOp }
			local comparators: { Expr } = {}
			local rhs = parseBinaryExpression(state, PRECEDENCE[firstTok.type] + 1)
			table.insert(comparators, rhs)
			while true do
				local t: Token? = state.current
				if not t then
					break
				end
				local nextTok = lookahead(state, 1)
				local op2 = t and getCompareOp(t.type, nextTok)
				if not op2 then
					break
				end
				if t.type == "IS" and nextTok and nextTok.type == "NOT" then
					state:advance()
					state:advance()
				elseif t.type == "NOT" and nextTok and nextTok.type == "IN" then
					state:advance()
					state:advance()
				else
					state:advance()
				end
				local r2 = parseBinaryExpression(state, PRECEDENCE[t.type] + 1)
				table.insert(ops, op2)
				table.insert(comparators, r2)
			end
			return {
				type = "Compare",
				left = base,
				ops = ops,
				comparators = comparators,
				lineno = firstTok.line,
				col_offset = firstTok.column,
			}
		end

		while true do
			local currentTok: Token? = state.current
			if not currentTok then
				break
			end
			local prec = PRECEDENCE[currentTok.type]
			if not prec or prec < minPrec then
				break
			end
			if currentTok.type == "AND" or currentTok.type == "OR" then
				local boolType = (currentTok.type == "AND") and "And" or "Or"
				local operatorTokenType = currentTok.type
				local values = { left }
				repeat
					state:advance()
					local rhs = parseBinaryExpression(state, PRECEDENCE[operatorTokenType] + 1)
					table.insert(values, rhs)
					currentTok = state.current
				until not currentTok or currentTok.type ~= operatorTokenType
				left = {
					type = "BoolOp",
					op = boolType,
					values = values,
					lineno = values[1].lineno,
					col_offset = values[1].col_offset,
				}
			else
				local nextTok = lookahead(state, 1)
				local cmpOp: CmpOp? = getCompareOp(currentTok.type, nextTok)
				if cmpOp then
					local cmp = (cmpOp) :: CmpOp
					local opToken: Token = currentTok
					if currentTok.type == "IS" and nextTok and nextTok.type == "NOT" then
						state:advance()
						state:advance()
					elseif currentTok.type == "NOT" and nextTok and nextTok.type == "IN" then
						state:advance()
						state:advance()
					else
						state:advance()
					end
					left = foldComparison(left, opToken, cmp :: CmpOp)
				else
					local op = BINOP_MAP[currentTok.type]
					if not op then
						break
					end
					state:advance()
					local right = parseBinaryExpression(state, prec + 1)
					left = {
						type = "BinOp",
						left = left,
						op = op,
						right = right,
						lineno = currentTok.line,
						col_offset = currentTok.column,
					}
				end
			end
		end

		return left
	end

	return {
		parseBinaryExpression = parseBinaryExpression,
		parseUnary = parseUnary,
	}
end

return {
	new = createBinaryModule,
}


local tokens = require("../../tokens")
local nodes = require("../../ast/nodes")

export type Token = tokens.Token
export type Expr = nodes.Expr

export type FStringDeps = {
	lexer: any,
	wrapCall: (funcName: string, argList: { Expr }, baseNode: Expr, token: Token) -> Expr,
	foldConcat: (parts: { Expr }, token: Token) -> Expr,
	parseExpression: (state: any) -> Expr,
}

local function makeSubState(tokenList: { Token }, parentState: any)
	local sub: any = {
		tokens = tokenList,
		pos = 1,
		current = tokenList[1],
	}

	function sub:advance()
		if self.pos < #self.tokens then
			self.pos += 1
			self.current = self.tokens[self.pos]
		else
			self.current = nil
		end
	end

	function sub:lookahead(offset: number): Token?
		local idx = self.pos + offset
		if idx >= 1 and idx <= #self.tokens then
			return self.tokens[idx]
		end
		return nil
	end

	function sub:consume(tokenType: string, message: string?): Token
		local cur = self.current
		if not cur then
			parentState:raise(message or ("Expected " .. tokenType .. " in f-string expression"))
		end
		if cur.type ~= tokenType then
			parentState:raise(message or ("Expected " .. tokenType .. " in f-string expression, got " .. cur.type))
		end
		self:advance()
		return cur :: any
	end

	function sub:raise(msg: string)
		parentState:raise(msg)
	end

	return sub
end

local function createEmbeddedExpressionParser(deps: FStringDeps)
	local lexer = deps.lexer
	local parseExpression = deps.parseExpression

	local function parseEmbeddedExpression(exprText: string, state: any, token: Token): Expr
		local exprTokens = lexer.tokenize(exprText)
		local subState = makeSubState(exprTokens, state)
		local exprNode = parseExpression(subState)
		while subState.current and (subState.current.type == "NEWLINE" or subState.current.type == "ENDMARKER") do
			subState:advance()
		end
		if subState.current then
			state:raise("Unexpected token after expression in f-string")
		end
		return exprNode
	end

	return parseEmbeddedExpression
end

local function buildFormattedValue(
	exprNode: Expr,
	conversion: string?,
	formatSpec: string?,
	token: Token,
	state: any,
	deps: FStringDeps
): Expr
	local converted = exprNode
	if conversion == "s" then
		converted = deps.wrapCall("str", { converted }, exprNode, token)
	elseif conversion == "r" then
		converted = deps.wrapCall("repr", { converted }, exprNode, token)
	elseif conversion == "a" then
		converted = deps.wrapCall("ascii", { converted }, exprNode, token)
	elseif conversion ~= nil then
		state:raise("Unsupported conversion specifier in f-string: !" .. tostring(conversion))
	end

	if formatSpec and formatSpec ~= "" then
		local specConst: Expr = {
			type = "Constant",
			value = formatSpec,
			kind = nil,
			lineno = token.line,
			col_offset = token.column,
		}
		converted = deps.wrapCall("format", { converted, specConst }, converted, token)
	elseif conversion == nil then
		converted = deps.wrapCall("str", { converted }, exprNode, token)
	end

	return converted
end

local function createFormattedSectionParser(deps: FStringDeps)
	local parseEmbeddedExpression = createEmbeddedExpressionParser(deps)

	local function parseFormattedSection(content: string, startIndex: number, state: any, token: Token): (Expr, number)
		local len = #content
		local i = startIndex + 1
		local exprStart = i
		local braceDepth = 0
		local parenDepth = 0
		local bracketDepth = 0
		local inStringQuote: string? = nil
		local stringTriple = false
		local conversion: string? = nil
		local formatStart: number? = nil
		local exprEndIndex: number? = nil

		while i <= len do
			if inStringQuote then
				if stringTriple then
					if i + 2 <= len and content:sub(i, i + 2) == string.rep(inStringQuote, 3) then
						inStringQuote = nil
						stringTriple = false
						i += 3
					else
						i += 1
					end
				else
					local ch = content:sub(i, i)
					if ch == "\\" then
						i += 2
					elseif ch == inStringQuote then
						inStringQuote = nil
						i += 1
					else
						i += 1
					end
				end
			else
				local ch = content:sub(i, i)
				if ch == "'" or ch == '"' then
					local triple = i + 2 <= len and content:sub(i, i + 2) == ch .. ch .. ch
					inStringQuote = ch
					stringTriple = triple
					i += if triple then 3 else 1
				elseif ch == "(" then
					parenDepth += 1
					i += 1
				elseif ch == ")" then
					if parenDepth > 0 then
						parenDepth -= 1
					end
					i += 1
				elseif ch == "[" then
					bracketDepth += 1
					i += 1
				elseif ch == "]" then
					if bracketDepth > 0 then
						bracketDepth -= 1
					end
					i += 1
				elseif ch == "{" then
					braceDepth += 1
					i += 1
				elseif ch == "}" then
					if braceDepth == 0 then
						if not exprEndIndex then
							exprEndIndex = i - 1
						end
						break
					else
						braceDepth -= 1
						i += 1
					end
				elseif
					ch == "!"
					and braceDepth == 0
					and parenDepth == 0
					and bracketDepth == 0
					and conversion == nil
					and formatStart == nil
				then
					local convChar = content:sub(i + 1, i + 1)
					if convChar == "" then
						state:raise("Conversion specifier in f-string is missing")
					end
					if convChar ~= "s" and convChar ~= "r" and convChar ~= "a" then
						state:raise("Unsupported conversion specifier in f-string: !" .. convChar)
					end
					conversion = convChar
					exprEndIndex = exprEndIndex or (i - 1)
					i += 2
				elseif
					ch == ":"
					and braceDepth == 0
					and parenDepth == 0
					and bracketDepth == 0
					and formatStart == nil
				then
					exprEndIndex = exprEndIndex or (i - 1)
					formatStart = i + 1
					i += 1
				elseif
					ch == "="
					and braceDepth == 0
					and parenDepth == 0
					and bracketDepth == 0
					and conversion == nil
					and formatStart == nil
				then
					state:raise("Debug f-string expressions (expr=) are not supported yet")
				else
					i += 1
				end
			end
		end

		if i > len then
			state:raise("F-string expression is missing closing '}'")
		end

		local exprStop = exprEndIndex or (i - 1)
		if exprStop < exprStart then
			state:raise("Empty expression in f-string")
		end

		local exprSlice = string.sub(content, exprStart, exprStop)
		local exprText = (string.match(exprSlice, "^%s*(.-)%s*$") or "")
		if exprText == "" then
			state:raise("Empty expression in f-string")
		end

		local exprNode = parseEmbeddedExpression(exprText, state, token)

		local formatSpecText: string? = nil
		if formatStart then
			local rawSpec = string.sub(content, formatStart, i - 1)
			local trimmed = string.match(rawSpec, "^%s*(.-)%s*$") or ""
			if trimmed ~= "" then
				local check = trimmed:gsub("{{", ""):gsub("}}", "")
				if string.find(check, "{", 1, true) or string.find(check, "}", 1, true) then
					state:raise("Nested expressions in format specifiers are not supported yet")
				end
				formatSpecText = trimmed:gsub("{{", "{"):gsub("}}", "}")
			end
		end

		local formatted = buildFormattedValue(exprNode, conversion, formatSpecText, token, state, deps)
		return formatted, i + 1
	end

	return parseFormattedSection
end

local function createFStringParser(deps: FStringDeps)
	local parseFormattedSection = createFormattedSectionParser(deps)

	local function parseFStringLiteral(token: Token, state: any): Expr
		local content = (token :: any).stringContent or ""
		local values: { Expr } = {}
		local literalBuffer: { string } = {}

		local function flushLiteral()
			if #literalBuffer > 0 then
				table.insert(values, {
					type = "Constant",
					value = table.concat(literalBuffer),
					kind = nil,
					lineno = token.line,
					col_offset = token.column,
				})
				literalBuffer = {}
			end
		end

		local i = 1
		local len = #content
		while i <= len do
			local ch = string.sub(content, i, i)
			if ch == "{" then
				local nextCh = i < len and string.sub(content, i + 1, i + 1) or nil
				if nextCh == "{" then
					table.insert(literalBuffer, "{")
					i += 2
				else
					flushLiteral()
					local expr, nextIndex = parseFormattedSection(content, i, state, token)
					table.insert(values, expr)
					i = nextIndex
				end
			elseif ch == "}" then
				local nextCh = i < len and string.sub(content, i + 1, i + 1) or nil
				if nextCh == "}" then
					table.insert(literalBuffer, "}")
					i += 2
				else
					state:raise("Single '}' is not allowed in f-string")
				end
			else
				table.insert(literalBuffer, ch)
				i += 1
			end
		end

		flushLiteral()

		return deps.foldConcat(values, token)
	end

	return {
		parseFStringLiteral = parseFStringLiteral,
	}
end

return {
	new = createFStringParser,
}


-- This module handles expression parsing

local tokens = require("../tokens")
local nodes = require("../ast/nodes")
local precedence = require("./precedence")

local postfix = require("./postfix")
local lexer = require("../lexer")

local utils = require("@self/utils")

local fstringFactory = require("@self/fstring")
local lambdaFactory = require("@self/lambda")
local primaryFactory = require("@self/primary")

local binaryFactory = require("@self/binary")

export type Token = tokens.Token

export type TokenType = tokens.TokenType
export type Expr = nodes.Expr

local expressions = {}

local parseBinaryExpression: (state: any, minPrec: number) -> Expr

local function parseExpression(state: any): Expr
	return parseBinaryExpression(state, precedence.LAMBDA)
end

local fstringModule = fstringFactory.new({
	lexer = lexer,
	wrapCall = utils.wrapCall,

	foldConcat = utils.foldConcat,
	parseExpression = parseExpression,
})

local primaryModule = primaryFactory.new({
	postfix = postfix,
	prefixContains = utils.prefixContains,

	makeStringConstant = utils.makeStringConstant,
	foldConcat = utils.foldConcat,
	parseExpression = parseExpression,

	parseFStringLiteral = fstringModule.parseFStringLiteral,
})

local parsePrimary = primaryModule.parsePrimary

local lambdaModule = lambdaFactory.new({
	precedence = precedence,
	postfix = postfix,

	parseBinaryExpression = function(state: any, minPrec: number)
		return parseBinaryExpression(state, minPrec)
	end,

	parseExpression = parseExpression,
	newArguments = utils.newArguments,
})

local parseLambda = lambdaModule.parseLambda

local binaryModule = binaryFactory.new({
	precedence = precedence,

	parsePrimary = parsePrimary,
	parseLambda = parseLambda,
})

parseBinaryExpression = binaryModule.parseBinaryExpression

function expressions.parse(state: any): Expr
	return parseExpression(state)
end

function expressions.parsePrimaryForTarget(state: any)
	return primaryModule.parsePrimaryForTarget(state)
end

return expressions


local tokens = require("../../tokens")
local nodes = require("../../ast/nodes")

export type Token = tokens.Token
export type Expr = nodes.Expr
export type Arguments = nodes.Arguments

local MAX_LAMBDA_NESTING = 64

export type LambdaDeps = {
	precedence: any,
	postfix: any,
	parseBinaryExpression: (state: any, minPrec: number) -> Expr,
	parseExpression: (state: any) -> Expr,
	newArguments: () -> Arguments,
}

local function enterLambda(state: any)
	local depth = (state.lambdaDepth or 0) + 1
	if depth > MAX_LAMBDA_NESTING then
		state:raise("Lambda expression nesting too deep")
	end
	state.lambdaDepth = depth
end

local function leaveLambda(state: any)
	local depth = (state.lambdaDepth or 1) - 1
	if depth <= 0 then
		state.lambdaDepth = nil
	else
		state.lambdaDepth = depth
	end
end

local function createLambdaModule(deps: LambdaDeps)
	local precedence = deps.precedence
	local parseBinaryExpression = deps.parseBinaryExpression
	local parseExpression = deps.parseExpression
	local postfix = deps.postfix
	local newArguments = deps.newArguments

	local function parseLambdaParameters(state: any): Arguments
		local args = newArguments()

		while state.current and state.current.type ~= "COLON" do
			if state.current.type ~= "NAME" then
				state:raise("Expected parameter name in lambda")
			end
			local paramToken: Token = state.current
			state:advance()
			local param: nodes.Arg = {
				arg = paramToken.value,
				annotation = nil,
				lineno = paramToken.line,
				col_offset = paramToken.column,
				end_lineno = paramToken.endLine,
				end_col_offset = paramToken.endColumn,
			}
			table.insert(args.args, param)

			if state.current and state.current.type == "EQUAL" then
				state:advance()
				local defaultExpr = parseBinaryExpression(state, precedence.LAMBDA)
				table.insert(args.defaults, defaultExpr)
			end

			if not state.current or state.current.type ~= "COMMA" then
				break
			end

			local lookaheadToken = state:lookahead(1)
			state:advance()
			if not lookaheadToken or lookaheadToken.type == "COLON" then
				break
			end
		end

		return args
	end

	local function parseLambda(state: any): Expr
		local lambdaToken: Token = state.current :: Token
		enterLambda(state)
		state:advance()

		local args = newArguments()
		if state.current and state.current.type ~= "COLON" then
			args = parseLambdaParameters(state)
		end

		state:consume("COLON", "Expected ':' after lambda parameters")
		local bodyExpr = parseBinaryExpression(state, precedence.LAMBDA)

		local lambdaNode: Expr = {
			type = "Lambda",
			args = args,
			body = bodyExpr,
			lineno = lambdaToken.line,
			col_offset = lambdaToken.column,
			end_lineno = bodyExpr.end_lineno,
			end_col_offset = bodyExpr.end_col_offset,
		}

		local result = postfix.parse(state, lambdaNode, parseExpression)
		leaveLambda(state)
		return result
	end

	return {
		parseLambda = parseLambda,
	}
end

return {
	new = createLambdaModule,
}


local tokens = require("../../tokens")
local nodes = require("../../ast/nodes")
local postfix = require("../postfix")

export type Token = tokens.Token
export type Expr = nodes.Expr

export type PrimaryDeps = {
	postfix: typeof(postfix),
	prefixContains: (prefix: string?, flag: string) -> boolean,
	makeStringConstant: (token: Token) -> Expr,
	foldConcat: (parts: { Expr }, token: Token) -> Expr,
	parseExpression: (state: any) -> Expr,
	parseFStringLiteral: (token: Token, state: any) -> Expr,
}

local function createPrimaryModule(deps: PrimaryDeps)
	local postfixModule = deps.postfix
	local prefixContains = deps.prefixContains
	local makeStringConstant = deps.makeStringConstant
	local foldConcat = deps.foldConcat
	local parseExpression = deps.parseExpression
	local parseFStringLiteral = deps.parseFStringLiteral

	local parsePrimaryForTarget: (state: any) -> Expr?

	local function parsePrimary(state: any): Expr
		local currentOpt: Token? = state.current
		if not currentOpt then
			state:raise("Unexpected end of input in expression")
		end
		local current: Token = currentOpt :: Token
		if current.type == "NUMBER" then
			state:advance()
			local value = current.value
			local numValue: any
			if string.find(value, "^0b") then
				numValue = tonumber(string.sub(value, 3), 2)
			elseif string.find(value, "^0o") then
				numValue = tonumber(string.sub(value, 3), 8)
			elseif string.find(value, "^0x") then
				numValue = tonumber(string.sub(value, 3), 16)
			elseif string.find(value, "%.") or string.find(value, "[eE]") then
				numValue = tonumber(value)
			else
				numValue = tonumber(value)
			end
			local node: Expr = {
				type = "Constant",
				value = numValue,
				kind = nil,
				lineno = current.line,
				col_offset = current.column,
			}
			return postfixModule.parse(state, node, parseExpression)
		end
		if current.type == "STRING" then
			local parts: { Expr } = {}
			while state.current and state.current.type == "STRING" do
				local tok: Token = state.current :: Token
				state:advance()
				local prefix = (tok :: any).stringPrefix
				if prefixContains(prefix, "f") then
					if prefixContains(prefix, "b") then
						state:raise("Cannot combine 'f' and 'b' prefixes in string literal")
					end
					table.insert(parts, parseFStringLiteral(tok, state))
				else
					table.insert(parts, makeStringConstant(tok))
				end
			end
			local node = foldConcat(parts, current)
			return postfixModule.parse(state, node, parseExpression)
		end
		if current.type == "TRUE" then
			state:advance()
			local node: Expr = {
				type = "Constant",
				value = true,
				kind = nil,
				lineno = current.line,
				col_offset = current.column,
			}
			return postfixModule.parse(state, node, parseExpression)
		end
		if current.type == "FALSE" then
			state:advance()
			local node: Expr = {
				type = "Constant",
				value = false,
				kind = nil,
				lineno = current.line,
				col_offset = current.column,
			}
			return postfixModule.parse(state, node, parseExpression)
		end
		if current.type == "NONE" then
			state:advance()
			local node: Expr = {
				type = "Constant",
				value = nil,
				kind = nil,
				lineno = current.line,
				col_offset = current.column,
			}
			return postfixModule.parse(state, node, parseExpression)
		end
		if current.type == "NAME" then
			state:advance()
			local node: Expr = {
				type = "Name",
				id = current.value,
				ctx = "Load",
				lineno = current.line,
				col_offset = current.column,
			}
			return postfixModule.parse(state, node, parseExpression)
		end
		if current.type == "LSQB" then
			state:advance()
			if state.current and state.current.type == "RSQB" then
				state:advance()
				local emptyList: Expr = {
					type = "List",
					elts = {},
					ctx = "Load",
					lineno = current.line,
					col_offset = current.column,
				}
				return postfixModule.parse(state, emptyList, parseExpression)
			end

			local function parseListElement(): Expr
				if not state.current then
					state:raise("Expected expression in list literal")
				end
				if state.current.type == "STAR" then
					state:advance()
					local starredValue = parseExpression(state)
					return {
						type = "Starred",
						value = starredValue,
						ctx = "Load",
						lineno = starredValue.lineno,
						col_offset = starredValue.col_offset,
					}
				end
				return parseExpression(state)
			end

			local function setComprehensionTargetContext(expr: Expr)
				local t = expr.type
				if t == "Name" or t == "Attribute" or t == "Subscript" then
					(expr :: any).ctx = "Store"
				elseif t == "Tuple" or t == "List" then
					(expr :: any).ctx = "Store"
					for _, elt in ipairs((expr :: any).elts) do
						setComprehensionTargetContext(elt)
					end
				elseif t == "Starred" then
					(expr :: any).ctx = "Store"
					setComprehensionTargetContext((expr :: any).value)
				else
					state:raise("Invalid comprehension target")
				end
			end

			local function parseComprehensionGenerators(): { nodes.Comprehension }
				local gens: { nodes.Comprehension } = {}
				while state.current and state.current.type == "FOR" do
					state:advance()
					local target = parsePrimaryForTarget(state)
					if not target then
						state:raise("Invalid comprehension target")
					end
					setComprehensionTargetContext(target :: Expr)
					state:consume("IN", "Expected 'in' in comprehension")
					local iterExpr = parseExpression(state)
					local filters: { Expr } = {}
					while state.current and state.current.type == "IF" do
						state:advance()
						local ifExpr = parseExpression(state)
						table.insert(filters, ifExpr)
					end
					table.insert(gens, {
						target = target :: Expr,
						iter = iterExpr,
						ifs = filters,
						is_async = false,
					})
				end
				if #gens == 0 then
					state:raise("Expected comprehension generator")
				end
				return gens
			end

			local firstElement = parseListElement()

			if state.current and state.current.type == "FOR" then
				if firstElement.type == "Starred" then
					state:raise("Starred expression not allowed in list comprehension")
				end
				local generators = parseComprehensionGenerators()
				state:consume("RSQB", "Expected ']' to close list comprehension")
				local listComp: Expr = {
					type = "ListComp",
					elt = firstElement,
					generators = generators,
					lineno = current.line,
					col_offset = current.column,
				}
				return postfixModule.parse(state, listComp, parseExpression)
			end

			local elts: { Expr } = { firstElement }
			while state.current and state.current.type ~= "RSQB" do
				if state.current.type == "COMMA" then
					state:advance()
					if state.current and state.current.type == "RSQB" then
						break
					end
					local nextElt = parseListElement()
					table.insert(elts, nextElt)
				else
					break
				end
			end
			state:consume("RSQB", "Expected ']' to close list literal")
			local listNode: Expr = {
				type = "List",
				elts = elts,
				ctx = "Load",
				lineno = current.line,
				col_offset = current.column,
			}
			return postfixModule.parse(state, listNode, parseExpression)
		end
		if current.type == "LBRACE" then
			state:advance()
			local keys: { Expr? } = {}
			local values: { Expr } = {}
			local elts: { Expr } = {}
			local isDict = false
			local hasElements = false
			while state.current and state.current.type ~= "RBRACE" do
				hasElements = true
				if state.current.type == "DOUBLESTAR" then
					state:advance()
					local unpackExpr = parseExpression(state)
					local unpackNode: Expr = {
						type = "DictUnpack",
						value = unpackExpr,
						lineno = unpackExpr.lineno,
						col_offset = unpackExpr.col_offset,
					}
					table.insert(keys, unpackNode)
					table.insert(values, unpackExpr)
					isDict = true
				else
					local firstExpr: Expr
					if state.current.type == "STAR" then
						state:advance()
						local starredInner = parseExpression(state)
						firstExpr = {
							type = "Starred",
							value = starredInner,
							ctx = "Load",
							lineno = starredInner.lineno,
							col_offset = starredInner.col_offset,
						}
					else
						firstExpr = parseExpression(state)
					end
					if state.current and state.current.type == "COLON" then
						isDict = true
						state:advance()
						local valueExpr = parseExpression(state)
						table.insert(keys, firstExpr)
						table.insert(values, valueExpr)
					else
						if isDict then
							state:raise("Mixed set and dict elements in literal")
						end
						table.insert(elts, firstExpr)
					end
				end
				if state.current and state.current.type == "COMMA" then
					state:advance()
				else
					break
				end
			end
			state:consume("RBRACE", "Expected '}' to close literal")
			local node: Expr
			if not hasElements then
				node = {
					type = "Dict",
					keys = {},
					values = {},
					lineno = current.line,
					col_offset = current.column,
				}
			elseif isDict then
				node = {
					type = "Dict",
					keys = keys,
					values = values,
					lineno = current.line,
					col_offset = current.column,
				}
			else
				node = {
					type = "Set",
					elts = elts,
					lineno = current.line,
					col_offset = current.column,
				}
			end
			return postfixModule.parse(state, node, parseExpression)
		end
		if current.type == "LPAR" then
			state:advance()
			if state.current and state.current.type == "RPAR" then
				state:advance()
				local tupleNode: Expr = {
					type = "Tuple",
					elts = {},
					ctx = "Load",
					lineno = current.line,
					col_offset = current.column,
				}
				return postfixModule.parse(state, tupleNode, parseExpression)
			end
			local elements: { Expr } = {}
			local first: Expr
			if state.current and state.current.type == "STAR" then
				state:advance()
				local starredInner = parseExpression(state)
				first = {
					type = "Starred",
					value = starredInner,
					ctx = "Load",
					lineno = starredInner.lineno,
					col_offset = starredInner.col_offset,
				}
			else
				first = parseExpression(state)
			end
			if state.current and state.current.type == "COMMA" then
				table.insert(elements, first)
				while state.current and state.current.type == "COMMA" do
					state:advance()
					if state.current and state.current.type == "RPAR" then
						break
					end
					local nextExpr: Expr
					if state.current and state.current.type == "STAR" then
						state:advance()
						local starredInner = parseExpression(state)
						nextExpr = {
							type = "Starred",
							value = starredInner,
							ctx = "Load",
							lineno = starredInner.lineno,
							col_offset = starredInner.col_offset,
						}
					else
						nextExpr = parseExpression(state)
					end
					table.insert(elements, nextExpr)
				end
				state:consume("RPAR", "Expected ')' to close tuple")
				local tupleNode: Expr = {
					type = "Tuple",
					elts = elements,
					ctx = "Load",
					lineno = first.lineno,
					col_offset = first.col_offset,
				}
				return postfixModule.parse(state, tupleNode, parseExpression)
			else
				state:consume("RPAR", "Expected ')' after expression")
				return postfixModule.parse(state, first, parseExpression)
			end
		end

		if current.type == "ERRORTOKEN" then
			state:raise("Syntax error: " .. (current.value or "unknown error"))
		else
			state:raise("Unexpected token in expression: " .. current.type)
		end
		return {
			type = "Constant",
			value = nil,
			kind = nil,
			lineno = current.line,
			col_offset = current.column,
		}
	end

	parsePrimaryForTarget = function(state: any): Expr?
		local tok: Token? = state.current
		if not tok or tok.type ~= "NAME" then
			return nil
		end
		local savedPos = state.pos
		local savedTok = tok
		local node = parsePrimary(state)
		return node
	end

	return {
		parsePrimary = parsePrimary,
		parsePrimaryForTarget = parsePrimaryForTarget,
	}
end

return {
	new = createPrimaryModule,
}


local tokens = require("../../tokens")
local nodes = require("../../ast/nodes")

export type Token = tokens.Token
export type Expr = nodes.Expr
export type Arguments = nodes.Arguments

local function prefixContains(prefix: string?, flag: string): boolean
	if not prefix then
		return false
	end
	return string.find(prefix, flag, 1, true) ~= nil
end

local function makeStringConstant(token: Token): Expr
	local prefix = (token :: any).stringPrefix
	local content = (token :: any).stringContent
	local value: string
	if content ~= nil then
		value = content
	else
		local v = token.value
		if
			#v >= 6
			and (
				(string.sub(v, 1, 3) == '"""' and string.sub(v, -3) == '"""')
				or (string.sub(v, 1, 3) == "'''" and string.sub(v, -3) == "'''")
			)
		then
			value = string.sub(v, 4, -4)
		elseif
			#v >= 2
			and (
				(string.sub(v, 1, 1) == '"' and string.sub(v, -1) == '"')
				or (string.sub(v, 1, 1) == "'" and string.sub(v, -1) == "'")
			)
		then
			value = string.sub(v, 2, -2)
		else
			value = v
		end
	end

	local outKind = prefix
	if outKind and string.find(outKind, "u", 1, true) then
		outKind = string.gsub(outKind, "u", "")
		if outKind == "" then
			outKind = nil
		end
	end
	local isBytes = outKind and string.find(outKind, "b", 1, true) ~= nil
	if isBytes then
		outKind = "bytes"
	end

	return {
		type = "Constant",
		value = value,
		kind = outKind,
		lineno = token.line,
		col_offset = token.column,
	}
end

local function makeNameNode(id: string, baseNode: Expr?, token: Token): Expr
	local lineno = if baseNode then baseNode.lineno else token.line
	local col = if baseNode then baseNode.col_offset else token.column
	return { type = "Name", id = id, ctx = "Load", lineno = lineno, col_offset = col }
end

local function wrapCall(funcName: string, argList: { Expr }, baseNode: Expr, token: Token): Expr
	local funcNode = makeNameNode(funcName, baseNode, token)
	return {
		type = "Call",
		func = funcNode,
		args = argList,
		keywords = {},
		lineno = baseNode.lineno,
		col_offset = baseNode.col_offset,
	}
end

local function foldConcat(parts: { Expr }, token: Token): Expr
	if #parts == 0 then
		return {
			type = "Constant",
			value = "",
			kind = nil,
			lineno = token.line,
			col_offset = token.column,
		}
	end
	local result: Expr = parts[1]
	for i = 2, #parts do
		result = {
			type = "BinOp",
			left = result,
			op = "Add",
			right = parts[i],
			lineno = token.line,
			col_offset = token.column,
		}
	end
	return result
end

local function newArguments(): nodes.Arguments
	return {
		posonlyargs = {},
		args = {},
		vararg = nil,
		kwonlyargs = {},
		kw_defaults = {},
		kwarg = nil,
		defaults = {},
	}
end

return {
	prefixContains = prefixContains,
	makeStringConstant = makeStringConstant,
	makeNameNode = makeNameNode,
	wrapCall = wrapCall,
	foldConcat = foldConcat,
	newArguments = newArguments,
}


local tokens = require("./tokens")
local nodes = require("./ast/nodes")
local errors = require("@self/errors")
local expressions = require("@self/expressions")
local statements = require("@self/statements")

export type Token = tokens.Token
export type Module = nodes.Module
export type Expr = nodes.Expr
export type Stmt = nodes.Stmt

local Parser = {}

-- Parser state object (OO-ish wrapper giving methods for modules)
local State = {}
State.__index = State

export type ParserState = {
	tokens: { Token },
	pos: number,
	current: Token?,
	advance: (self: any) -> (),
	consume: (self: any, tokenType: string, message: string?) -> Token,
	raise: (self: any, msg: string) -> (),
	lookahead: (self: any, offset: number) -> Token?,
}

function State.new(tokenList: { Token })
	local self: ParserState = setmetatable({
		tokens = tokenList,
		pos = 1,
		current = tokenList[1],
	}, State) :: any
	return self
end

function State:advance()
	if self.pos < #self.tokens then
		self.pos += 1
		self.current = self.tokens[self.pos]
	else
		self.current = nil
	end
end

function State:lookahead(offset: number): Token?
	local idx = self.pos :: number + offset
	if idx >= 1 and idx <= #self.tokens then
		return self.tokens[idx]
	end
	return nil
end

function State:consume(tokenType: string, message: string?): Token
	local cur = self.current
	if not cur then
		local lastToken = self.tokens[#self.tokens]
		errors.raise(message or ("Expected " .. tokenType .. " but reached end of input"), lastToken)
	end
	if cur.type ~= tokenType then
		errors.raise(message or ("Expected " .. tokenType .. " but got " .. cur.type), cur)
	end
	self:advance()
	return cur :: any
end

function State:raise(msg: string)
	errors.raise(msg, self.current)
end

local function skipLayout(state: ParserState)
	while
		state.current
		and (
			state.current.type == "NEWLINE"
			or state.current.type == "INDENT"
			or state.current.type == "DEDENT"
			or state.current.type == "COMMENT"
		)
	do
		state:advance()
	end
end

-- Module-level parse entry
function Parser.parse(tokenList: { Token }): Module
	local state = State.new(tokenList)
	local body: { Stmt } = {}
	while state.current and state.current.type ~= "ENDMARKER" do
		skipLayout(state)
		if not state.current or state.current.type == "ENDMARKER" then
			break
		end
		local stmt = statements.parse(state)
		table.insert(body, stmt)
		while state.current and state.current.type == "NEWLINE" do
			state:advance()
		end
	end
	return { type = "Module", body = body, type_ignores = nil, lineno = 1, col_offset = 1 } :: any
end

function Parser.parseExpression(tokenList: { Token }): Expr
	local state = State.new(tokenList)
	return expressions.parse(state)
end

return Parser


local tokens = require("../tokens")
local nodes = require("../ast/nodes")

export type Token = tokens.Token
export type Expr = nodes.Expr

local postfix = {}

-- Forward-declared expression parser injected to avoid cycles
export type ExpressionParser = (state: any) -> Expr

-- Parse function call arguments and keywords
local function parseCallArgs(state: any, parseExpression: ExpressionParser): ({ Expr }, { any })
	local args: { Expr } = {}
	local keywords: { any } = {}
	while state.current and state.current.type ~= "RPAR" do
		if
			state.current.type == "NAME"
			and state.pos < #state.tokens
			and state.tokens[state.pos + 1].type == "EQUAL"
		then
			local nameTok: Token = state.current
			state:advance() -- name
			state:advance() -- '='
			local valueExpr = parseExpression(state)
			table.insert(
				keywords,
				{ arg = nameTok.value, value = valueExpr, lineno = nameTok.line, col_offset = nameTok.column }
			)
		elseif state.current.type == "STAR" then
			state:advance()
			local starredValue = parseExpression(state)
			table.insert(args, {
				type = "Starred",
				value = starredValue,
				ctx = "Load",
				lineno = starredValue.lineno,
				col_offset = starredValue.col_offset,
			})
		elseif state.current.type == "DOUBLESTAR" then
			state:advance()
			local kwValue = parseExpression(state)
			table.insert(
				keywords,
				{ arg = nil, value = kwValue, lineno = kwValue.lineno, col_offset = kwValue.col_offset }
			)
		else
			local expr = parseExpression(state)
			table.insert(args, expr)
		end
		if state.current and state.current.type == "COMMA" then
			state:advance()
			if state.current and state.current.type == "RPAR" then
				break
			end
		else
			break
		end
	end
	return args, keywords
end

function postfix.parse(state: any, expr: Expr, parseExpression: ExpressionParser): Expr
	while true do
		local current: Token? = state.current
		if not current then
			break
		end
		if current.type == "DOT" then
			state:advance()
			local nameTok: Token = state:consume("NAME", "Expected attribute name after '.'")
			expr = {
				type = "Attribute",
				value = expr,
				attr = nameTok.value,
				ctx = "Load",
				lineno = nameTok.line,
				col_offset = nameTok.column,
			}
		elseif current.type == "LSQB" then
			state:advance()
			local indexExpr = parseExpression(state)
			state:consume("RSQB", "Expected ']' after subscript expression")
			expr = {
				type = "Subscript",
				value = expr,
				slice = indexExpr,
				ctx = "Load",
				lineno = indexExpr.lineno,
				col_offset = indexExpr.col_offset,
			}
		elseif current.type == "LPAR" then
			state:advance()
			local args, keywords = parseCallArgs(state, parseExpression)
			state:consume("RPAR", "Expected ')' to close call arguments")
			expr = {
				type = "Call",
				func = expr,
				args = args,
				keywords = keywords,
				lineno = expr.lineno,
				col_offset = expr.col_offset,
			}
		else
			break
		end
	end
	return expr
end

return postfix


local precedence = {}

-- Precedence levels (lower number = lower precedence)
precedence.LEVELS = {
	LAMBDA = 0,
	OR = 1,
	AND = 2,
	NOT = 3, -- unary not
	IN = 4,
	NOT_IN = 4,
	IS = 4,
	IS_NOT = 4,
	LESS = 4,
	LESSEQUAL = 4,
	GREATER = 4,
	GREATEREQUAL = 4,
	EQEQUAL = 4,
	NOTEQUAL = 4,
	VBAR = 5, -- bitwise or
	CIRCUMFLEX = 6, -- xor
	AMPER = 7, -- bitwise and
	LEFTSHIFT = 8,
	RIGHTSHIFT = 8,
	PLUS = 9,
	MINUS = 9,
	STAR = 10,
	SLASH = 10,
	DOUBLESLASH = 10,
	PERCENT = 10,
	AT = 10,
	UNARY = 11, -- unary + - ~ handled specially
	DOUBLESTAR = 12, -- exponentiation (right associative)
}

-- Binary operator token -> AST BinOpType
precedence.BINOP_MAP = {
	PLUS = "Add",
	MINUS = "Sub",
	STAR = "Mult",
	SLASH = "Div",
	DOUBLESLASH = "FloorDiv",
	PERCENT = "Mod",
	DOUBLESTAR = "Pow",
	LEFTSHIFT = "LShift",
	RIGHTSHIFT = "RShift",
	VBAR = "BitOr",
	CIRCUMFLEX = "BitXor",
	AMPER = "BitAnd",
	AT = "MatMult",
}

-- Augmented assignment mapping (token of operator before '=' -> BinOpType)
precedence.AUGASSIGN_MAP = precedence.BINOP_MAP

precedence.LAMBDA = precedence.LEVELS.LAMBDA

return precedence


local nodes = require("../ast/nodes")
local precedence = require("./precedence")
local expressions = require("./expressions")

export type Expr = nodes.Expr
export type Stmt = nodes.Stmt

local statements = {}

-- Helper to recursively parse assignment targets (including nested tuples)
local function parseForTarget(state: any): Expr
	if state.current and state.current.type == "LPAR" then
		-- Parenthesized tuple
		local lparToken = state.current
		state:advance() -- consume LPAR

		local elements: { Expr } = {}
		if state.current and state.current.type ~= "RPAR" then
			-- Parse first element (recursively handles nested tuples)
			table.insert(elements, parseForTarget(state))

			-- Parse remaining elements
			while state.current and state.current.type == "COMMA" do
				state:advance() -- consume COMMA
				if state.current and state.current.type == "RPAR" then
					break -- trailing comma
				end
				table.insert(elements, parseForTarget(state))
			end
		end

		state:consume("RPAR", "Expected ')' to close tuple")

		-- Create tuple node
		return {
			type = "Tuple",
			elts = elements,
			ctx = "Load",
			lineno = lparToken.line,
			col_offset = lparToken.column,
		}
	else
		-- Simple name or name with subscript/attribute
		local targetOpt = expressions.parsePrimaryForTarget(state)
		if not targetOpt then
			state:raise("Invalid for target element")
		end
		return targetOpt :: Expr
	end
end

local function assignmentTarget(expr: Expr): Expr
	if expr.type == "Name" or expr.type == "Attribute" or expr.type == "Subscript" then
		(expr :: any).ctx = "Store"
	elseif expr.type == "Tuple" then
		-- Recursively mark tuple elements as store targets
		(expr :: any).ctx = "Store"
		for _, elt in ipairs((expr :: any).elts) do
			assignmentTarget(elt)
		end
	end
	return expr
end

local function parseModuleAlias(state: any): nodes.alias
	local nameToken = state:consume("NAME", "Expected module name")
	local name = nameToken.value

	while state.current and state.current.type == "DOT" do
		state:advance()
		local part = state:consume("NAME", "Expected name after dot")
		name = name .. "." .. part.value
	end

	local asname: string? = nil
	if state.current and state.current.type == "AS" then
		state:advance()
		local asToken = state:consume("NAME", "Expected alias name")
		asname = asToken.value
	end

	return {
		name = name,
		asname = asname,
		lineno = nameToken.line,
		col_offset = nameToken.column,
	}
end

local function parseImportFromAlias(state: any): nodes.alias
	local nameToken = state:consume("NAME", "Expected name")
	local name = nameToken.value

	local asname: string? = nil
	if state.current and state.current.type == "AS" then
		state:advance()
		local asToken = state:consume("NAME", "Expected alias name")
		asname = asToken.value
	end

	return {
		name = name,
		asname = asname,
		lineno = nameToken.line,
		col_offset = nameToken.column,
	}
end

-- Parse a simple statement (assignment, augassign, expression, return)
function statements.parseSimple(state: any): Stmt
	local current = state.current
	if not current then
		state:raise("Unexpected end of input in statement")
	end
	if current.type == "IMPORT" then
		local importToken = current
		state:advance()
		local names = { parseModuleAlias(state) }
		while state.current and state.current.type == "COMMA" do
			state:advance()
			table.insert(names, parseModuleAlias(state))
		end
		return {
			type = "Import",
			names = names,
			lineno = importToken.line,
			col_offset = importToken.column,
		}
	elseif current.type == "FROM" then
		local fromToken = current
		state:advance()

		local level = 0
		while state.current and (state.current.type == "DOT" or state.current.type == "ELLIPSIS") do
			if state.current.type == "DOT" then
				level = level + 1
				state:advance()
			else -- ELLIPSIS is ... (3 dots)
				level = level + 3
				state:advance()
			end
		end

		local module: string? = nil
		if state.current and state.current.type == "NAME" then
			local nameToken = state.current
			module = nameToken.value
			state:advance()
			while state.current and state.current.type == "DOT" do
				state:advance()
				local part = state:consume("NAME", "Expected name after dot")
				module = module .. "." .. part.value
			end
		end

		if level == 0 and not module then
			state:raise("Expected module name or relative import dots")
		end

		state:consume("IMPORT", "Expected 'import' after 'from ...'")

		local names = {}
		if state.current and state.current.type == "LPAR" then
			state:advance()
			table.insert(names, parseImportFromAlias(state))
			while state.current and state.current.type == "COMMA" do
				state:advance()
				if state.current and state.current.type == "RPAR" then
					break
				end
				table.insert(names, parseImportFromAlias(state))
			end
			state:consume("RPAR", "Expected ')'")
		elseif state.current and state.current.type == "STAR" then
			local starToken = state.current
			state:advance()
			table.insert(names, {
				name = "*",
				asname = nil,
				lineno = starToken.line,
				col_offset = starToken.column,
			})
		else
			table.insert(names, parseImportFromAlias(state))
			while state.current and state.current.type == "COMMA" do
				state:advance()
				table.insert(names, parseImportFromAlias(state))
			end
		end

		return {
			type = "ImportFrom",
			module = module,
			names = names,
			level = level,
			lineno = fromToken.line,
			col_offset = fromToken.column,
		}
	elseif current.type == "RETURN" then
		state:advance()
		local value: Expr? = nil
		if
			state.current
			and state.current.type ~= "NEWLINE"
			and state.current.type ~= "ENDMARKER"
			and state.current.type ~= "DEDENT"
		then
			value = expressions.parse(state)
		end
		return { type = "Return", value = value, lineno = current.line, col_offset = current.column }
	elseif current.type == "BREAK" then
		state:advance()
		return { type = "Break", lineno = current.line, col_offset = current.column }
	elseif current.type == "CONTINUE" then
		state:advance()
		return { type = "Continue", lineno = current.line, col_offset = current.column }
	elseif current.type == "PASS" then
		state:advance()
		return { type = "Pass", lineno = current.line, col_offset = current.column }
	end
	-- Attempt target-only parse for assignment/augassign
	local savePos = state.pos
	local saveCurrent = current
	local targetCandidate = expressions.parsePrimaryForTarget(state)
	if targetCandidate then
		local afterTarget = state.current
		if afterTarget and afterTarget.type == "EQUAL" then
			state:advance()
			local value = expressions.parse(state)
			local target = assignmentTarget(targetCandidate)
			return {
				type = "Assign",
				targets = { target },
				value = value,
				lineno = target.lineno,
				col_offset = target.col_offset,
			}
		elseif afterTarget then
			local opTok = afterTarget
			local nextTok = state:lookahead(1)
			local opMap = precedence.BINOP_MAP
			if nextTok and nextTok.type == "EQUAL" and opMap[opTok.type] then
				state:advance()
				state:advance()
				local value = expressions.parse(state)
				local target = assignmentTarget(targetCandidate)
				return {
					type = "AugAssign",
					target = target,
					op = opMap[opTok.type],
					value = value,
					lineno = target.lineno,
					col_offset = target.col_offset,
				}
			end
		end
		state.pos = savePos
		state.current = saveCurrent
	else
		state.pos = savePos
		state.current = saveCurrent
	end
	local expr = expressions.parse(state)
	return { type = "Expr", value = expr, lineno = expr.lineno, col_offset = expr.col_offset }
end

local function parseSuiteAfterColon(state: any): { Stmt }
	local body: { Stmt } = {}

	local function skipEmptyLines()
		while state.current and (state.current.type == "NEWLINE" or state.current.type == "COMMENT") do
			state:advance()
		end
	end

	if state.current and state.current.type == "NEWLINE" then
		-- Multi-line suite
		state:advance()

		skipEmptyLines()

		if state.current and state.current.type == "INDENT" then
			state:advance()
		end

		skipEmptyLines()

		while
			state.current
			and state.current.type ~= "ENDMARKER"
			and state.current.type ~= "DEDENT"
			and state.current.type ~= "ELIF"
			and state.current.type ~= "ELSE"
		do
			local stmt = statements.parse(state)
			table.insert(body, stmt)
			skipEmptyLines()
		end

		if state.current and state.current.type == "DEDENT" then
			state:advance()
		end
	else
		-- Single-line suite: parse only one statement
		local stmt = statements.parse(state)
		table.insert(body, stmt)
		skipEmptyLines()
	end

	return body
end

function statements.parseCompound(state: any): Stmt
	local current = state.current
	if not current then
		state:raise("Unexpected end of input in compound statement")
	end
	if current.type == "IF" then
		state:advance()
		local test = expressions.parse(state)
		state:consume("COLON", "Expected ':' after if condition")
		local body = parseSuiteAfterColon(state)

		local ifNode = {
			type = "If",
			test = test,
			body = body,
			orelse = {},
			lineno = current.line,
			col_offset = current.column,
		}

		local lastIf = ifNode

		-- Handle chained elif clauses by nesting If nodes in orelse lists
		while state.current and state.current.type == "ELIF" do
			local elifToken = state.current
			state:advance()
			local elifTest = expressions.parse(state)
			state:consume("COLON", "Expected ':' after elif condition")
			local elifBody = parseSuiteAfterColon(state)
			local elifNode = {
				type = "If",
				test = elifTest,
				body = elifBody,
				orelse = {},
				lineno = elifToken.line,
				col_offset = elifToken.column,
			}
			lastIf.orelse = { elifNode }
			lastIf = elifNode
		end

		-- Handle final else clause (binds to last if/elif in chain)
		if state.current and state.current.type == "ELSE" then
			state:advance()
			state:consume("COLON", "Expected ':' after else")
			lastIf.orelse = parseSuiteAfterColon(state)
		end

		return ifNode
	end
	if current.type == "WHILE" then
		state:advance()
		local test = expressions.parse(state)
		state:consume("COLON", "Expected ':' after while condition")
		local body = parseSuiteAfterColon(state)
		return {
			type = "While",
			test = test,
			body = body,
			orelse = {},
			lineno = current.line,
			col_offset = current.column,
		}
	end
	if current.type == "FOR" then
		state:advance()
		-- Parse the target - can be a Name or a Tuple for destructuring
		local target: Expr

		-- Parse the first part of the target
		target = parseForTarget(state)

		-- Check if there's a comma, making it an implicit tuple
		if state.current and state.current.type == "COMMA" then
			-- Implicit tuple without parens: for x, y in items: or for (a, b), c in items:
			local elements = { target }
			while state.current and state.current.type == "COMMA" do
				state:advance()
				-- Check if we hit 'in' (trailing comma case)
				if state.current and state.current.type == "IN" then
					break
				end
				table.insert(elements, parseForTarget(state))
			end
			-- Create tuple node
			target = {
				type = "Tuple",
				elts = elements,
				ctx = "Load",
				lineno = elements[1].lineno,
				col_offset = elements[1].col_offset,
			}
		end

		-- Mark target as Store context before consuming 'in'
		target = assignmentTarget(target)

		state:consume("IN", "Expected 'in' after for target")
		local iter = expressions.parse(state)
		state:consume("COLON", "Expected ':' after for clause")
		local body = parseSuiteAfterColon(state)
		return {
			type = "For",
			target = target,
			iter = iter,
			body = body,
			orelse = {},
			lineno = current.line,
			col_offset = current.column,
		}
	end
	if current.type == "TRY" then
		local tryToken = current
		state:advance()
		state:consume("COLON", "Expected ':' after try")
		local body = parseSuiteAfterColon(state)

		local handlers = {}
		local sawExcept = false
		while state.current and state.current.type == "EXCEPT" do
			local exceptToken = state.current
			state:advance()
			local handlerExpr: Expr? = nil
			if state.current and state.current.type ~= "COLON" then
				handlerExpr = expressions.parse(state)
			end
			sawExcept = true

			local aliasName: string? = nil
			if state.current and state.current.type == "AS" then
				state:advance()
				local nameToken = state:consume("NAME", "Expected name after 'as'")
				aliasName = nameToken.value
			end

			state:consume("COLON", "Expected ':' after except clause")
			local handlerBody = parseSuiteAfterColon(state)

			table.insert(handlers, {
				type = "ExceptHandler",
				lineno = exceptToken.line,
				col_offset = exceptToken.column,
				end_lineno = nil,
				end_col_offset = nil,
				body = handlerBody,
				expr = handlerExpr,
				name = aliasName,
			})
		end

		local finalbody: { Stmt } = {}
		local nextToken = state.current and state.current.type or nil
		if not sawExcept and nextToken ~= "FINALLY" then
			state:raise("Expected at least one except or finally clause for try statement")
		end

		for i, handler in ipairs(handlers) do
			if (handler :: any).expr == nil and i < #handlers then
				state:raise("Bare 'except' must be the final handler")
			end
		end

		local orelse: { Stmt } = {}
		if state.current and state.current.type == "ELSE" then
			state:advance()
			state:consume("COLON", "Expected ':' after else")
			if not sawExcept then
				state:raise("'else' clause is only allowed when at least one except handler is present")
			end
			orelse = parseSuiteAfterColon(state)
		end

		if state.current and state.current.type == "FINALLY" then
			state:advance()
			state:consume("COLON", "Expected ':' after finally")
			finalbody = parseSuiteAfterColon(state)
		end

		if not sawExcept and #finalbody == 0 then
			state:raise("try statement requires except or finally clause")
		end

		return {
			type = "Try",
			body = body,
			handlers = handlers,
			orelse = orelse,
			finalbody = finalbody,
			lineno = tryToken.line,
			col_offset = tryToken.column,
		}
	end
	if current.type == "DEF" then
		state:advance()
		local nameToken = state:consume("NAME", "Expected function name after 'def'")
		local name = nameToken.value
		state:consume("LPAR", "Expected '(' after function name")

		-- Parse function arguments
		local args =
			{ posonlyargs = {}, args = {}, vararg = nil, kwonlyargs = {}, kw_defaults = {}, kwarg = nil, defaults = {} }

		-- Parse parameter list
		while state.current and state.current.type ~= "RPAR" do
			local paramToken = state:consume("NAME", "Expected parameter name")
			local param = {
				arg = paramToken.value,
				annotation = nil, -- No type annotations for now
				lineno = paramToken.line,
				col_offset = paramToken.column,
			}
			table.insert(args.args, param)

			-- Check for default value (e.g., param=value)
			if state.current and state.current.type == "EQUAL" then
				state:advance() -- consume '='
				local defaultExpr = expressions.parse(state)
				table.insert(args.defaults, defaultExpr)
			else
				-- If no default for this parameter, we may need to track it
				-- In Python, once you have a default, all following params must have defaults
				-- For now, only add to defaults array when we actually see '='
			end

			-- Check for comma or end
			if state.current and state.current.type == "COMMA" then
				state:advance() -- consume comma
				-- If next token is RPAR, we have a trailing comma which is fine
				if state.current and state.current.type == "RPAR" then
					break
				end
			elseif state.current and state.current.type == "RPAR" then
				break -- End of parameters
			else
				state:raise("Expected ',' or ')' after parameter")
			end
		end

		state:consume("RPAR", "Expected ')' after function arguments")
		state:consume("COLON", "Expected ':' after function signature")
		local body = parseSuiteAfterColon(state)
		return {
			type = "FunctionDef",
			name = name,
			args = args,
			body = body,
			decorator_list = {},
			returns = nil,
			lineno = current.line,
			col_offset = current.column,
		}
	end
	if current.type == "CLASS" then
		local classToken = current
		state:advance()
		local nameToken = state:consume("NAME", "Expected class name after 'class'")
		local bases: { Expr } = {}
		local keywords: { nodes.Keyword } = {}
		if state.current and state.current.type == "LPAR" then
			state:advance()
			if state.current and state.current.type ~= "RPAR" then
				while true do
					if state.current and state.current.type == "RPAR" then
						break
					end
					local baseExpr = expressions.parse(state)
					table.insert(bases, baseExpr)
					if not state.current or state.current.type ~= "COMMA" then
						break
					end
					state:advance()
				end
			end
			state:consume("RPAR", "Expected ')' after base list")
		end
		state:consume("COLON", "Expected ':' after class definition")
		local body = parseSuiteAfterColon(state)
		return {
			type = "ClassDef",
			name = nameToken.value,
			bases = bases,
			keywords = keywords,
			body = body,
			decorator_list = {},
			lineno = classToken.line,
			col_offset = classToken.column,
		}
	end
	state:raise("Unknown compound statement: " .. current.type)
	error("unreachable")
end

function statements.parse(state: any): Stmt
	local current = state.current
	if not current then
		state:raise("Unexpected end of input in statement")
	end
	if
		current.type == "IF"
		or current.type == "WHILE"
		or current.type == "FOR"
		or current.type == "DEF"
		or current.type == "CLASS"
		or current.type == "TRY"
	then
		return statements.parseCompound(state)
	end
	return statements.parseSimple(state)
end

return statements


local FrameModule = require("../frame")
local Stack = require("../stack")
local Base = require("../../objects/base")

local Blocks = {}

export type Frame = FrameModule.Frame
export type Block = FrameModule.Block

local function truncateToBlock(frame: Frame, block: Block)
	local target = block.stackSize
	local current = FrameModule.stackSize(frame)
	if target == nil or target > current then
		target = current
	end
	FrameModule.truncateStack(frame, target)
end

function Blocks.handleReturn(vm: any, frame: Frame, value: any): boolean
	while true do
		local block = FrameModule.getCurrentBlock(frame)
		if not block then
			if (frame :: any).is_generator then
				local info = (frame :: any).generator_state
				if info then
					info.returnValue = Base.ensurePyObject(value)
					info.finished = true
					info.lastYield = nil
					info.resumeValue = nil
					info.hasResumeValue = false
					info.nextSend = nil
				end
				return false
			end
			vm.return_value = value
			return false
		end
		local popped = FrameModule.popBlock(frame)
		if popped then
			truncateToBlock(frame, popped)
			if popped.type == "finally" then
				assert(popped.handler ~= nil, "finally block missing handler")
				FrameModule.pushFinally(frame, { reason = "return", value = value })
				FrameModule.jump(frame, popped.handler :: number)
				frame.skip_advance = true
				return true
			end
		end
	end
end

function Blocks.handleBreak(frame: Frame, target: number?)
	while true do
		local block = FrameModule.popBlock(frame)
		if not block then
			error("'break' outside loop", 2)
		end
		truncateToBlock(frame, block)
		if block.type == "finally" then
			assert(block.handler ~= nil, "finally block missing handler")
			FrameModule.pushFinally(frame, { reason = "break", target = target })
			FrameModule.jump(frame, block.handler :: number)
			frame.skip_advance = true
			return
		elseif block.type == "loop" then
			local exitTarget = target or block.endPos
			assert(exitTarget ~= nil, "Loop block missing end position")
			FrameModule.jump(frame, exitTarget :: number)
			frame.skip_advance = true
			return
		end
	end
end

function Blocks.handleContinue(frame: Frame, target: number?)
	while true do
		local block = FrameModule.getCurrentBlock(frame)
		if not block then
			error("'continue' not properly in loop", 2)
		end
		if block.type == "finally" then
			local popped = FrameModule.popBlock(frame)
			if popped then
				truncateToBlock(frame, popped)
				assert(popped.handler ~= nil, "finally block missing handler")
				FrameModule.pushFinally(frame, { reason = "continue", target = target })
				FrameModule.jump(frame, popped.handler :: number)
				frame.skip_advance = true
				return
			end
		elseif block.type == "loop" then
			local nextTarget = target or block.start
			if nextTarget == nil then
				error("Loop block missing start label for continue", 2)
			end
			FrameModule.jump(frame, nextTarget :: number)
			frame.skip_advance = true
			return
		else
			local popped = FrameModule.popBlock(frame)
			if popped then
				truncateToBlock(frame, popped)
			end
		end
	end
end

function Blocks.cleanIteratorOnBreak(frame: Frame)
	if Stack.isEmpty(frame.stack) then
		return
	end
	local tos = FrameModule.peek(frame)
	if type(tos) ~= "table" then
		return
	end
	local tt = (tos :: any).__type
	if type(tt) == "string" and string.find(tt, "iterator", 1, true) then
		FrameModule.pop(frame)
	end
end

Blocks.truncateToBlock = truncateToBlock

return Blocks


local FrameModule = require("../frame")
local Functions = require("../../objects/functions")
local Instructions = require("../../bytecode/instructions")
local Base = require("../../objects/base")
local State = require("./state")
local Generator = require("./generator")

local CallFunction = {}

type CallOptions = {
	locals: { [string]: any }?,
	captureLocals: boolean?,
}

export type Frame = FrameModule.Frame
export type VMState = State.VMState
type PyCode = Functions.PyCode
type Instruction = Instructions.Instruction

local function bindPositionalArguments(code: PyCode, locals: { [string]: any }, safeArgs: { any })
	for i = 1, #safeArgs do
		if i <= #code.varnames then
			local varname = code.varnames[i]
			if varname then
				locals[varname] = safeArgs[i]
			end
		end
	end
end

local function applyDefaultValues(
	code: PyCode,
	locals: { [string]: any },
	safeArgs: { any },
	requiredArgs: number,
	defaults: { any }?
)
	if not defaults or #defaults == 0 then
		return
	end
	local defaultsList = defaults :: { any }
	local defaultStart = requiredArgs - #defaultsList + 1
	for i = #safeArgs + 1, requiredArgs do
		local defaultIndex = i - defaultStart + 1
		if defaultIndex > 0 and defaultIndex <= #defaultsList and i <= #code.varnames then
			local varname = code.varnames[i]
			if varname then
				locals[varname] = defaultsList[defaultIndex]
			end
		end
	end
end

local function bindKeywordArguments(
	code: PyCode,
	locals: { [string]: any },
	safeArgs: { any },
	safeKwargs: { [string]: any },
	requiredArgs: number
)
	for name: string, value in pairs(safeKwargs) do
		local paramIndex = nil
		for i, varname in ipairs(code.varnames) do
			if varname == name and i <= requiredArgs then
				paramIndex = i
				break
			end
		end

		if paramIndex then
			if safeArgs[paramIndex] then
				error(
					"TypeError: "
						.. (code.name or "<anonymous>")
						.. "() got multiple values for argument '"
						.. name
						.. "'",
					2
				)
			end
			locals[name] = value
		else
			error(
				"TypeError: "
					.. (code.name or "<anonymous>")
					.. "() got an unexpected keyword argument '"
					.. name
					.. "'",
				2
			)
		end
	end
end

function CallFunction.new(executeInstruction: (
	vm: VMState,
	frame: Frame,
	instruction: Instruction,
	constants: { any }?,
	names: { string }?
) -> boolean)
	local function callInternal(
		vm: VMState,
		func: any,
		args: { any }?,
		kwargs: { [string]: any }?,
		options: CallOptions?
	): (any, { [string]: any }?)
		if not Functions.isFunction(func) then
			error("TypeError: '" .. tostring(func.__type or type(func)) .. "' object is not callable", 2)
		end

		local safeArgs = args or {}
		local safeKwargs = kwargs or {}

		local code = Functions.getFunctionCode(func)
		local funcGlobals = func.__value.globals

		local providedArgs = #safeArgs
		local requiredArgs = code.argcount
		local defaultsList = func.__value.defaults :: { any }?
		local defaultsCount = defaultsList and #defaultsList or 0
		local minArgs = if defaultsCount > 0 then requiredArgs - defaultsCount else requiredArgs

		if providedArgs < minArgs then
			error(
				"TypeError: "
					.. (code.name or "<anonymous>")
					.. "() missing "
					.. (minArgs - providedArgs)
					.. " required positional argument(s)",
				2
			)
		end

		if providedArgs > requiredArgs then
			error(
				"TypeError: "
					.. (code.name or "<anonymous>")
					.. "() takes "
					.. requiredArgs
					.. " positional argument(s) but "
					.. providedArgs
					.. " were given",
				2
			)
		end

		local locals: { [string]: any } = if options and options.locals then options.locals else {}
		bindPositionalArguments(code, locals, safeArgs)
		applyDefaultValues(code, locals, safeArgs, requiredArgs, defaultsList)
		bindKeywordArguments(code, locals, safeArgs, safeKwargs, requiredArgs)

		local funcInfo = func.__value
		if type(funcInfo) == "table" then
			local owner = (funcInfo :: any).owner
			if owner then
				local existing = locals["__class__"]
				if existing == nil then
					locals["__class__"] = Base.wrapType(owner)
				end
			end
		end

		if code.isGenerator then
			return Generator.new({
				code = code,
				globals = funcGlobals,
				builtins = vm.builtins,
				locals = locals,
				executeInstruction = executeInstruction,
			}),
				nil
		end

		local funcFrame = FrameModule.new(code.bytecode, funcGlobals, vm.builtins, locals, code.name)
		funcFrame.function_object = func

		local prevFrame = vm.current_frame
		vm.current_frame = funcFrame
		table.insert(vm.call_stack, funcFrame)

		while FrameModule.hasNext(funcFrame) do
			local instruction = FrameModule.getCurrentInstruction(funcFrame)
			if not instruction then
				break
			end

			local shouldContinue = executeInstruction(vm, funcFrame, instruction, code.constants, code.names)
			if not shouldContinue then
				break
			end

			if not funcFrame.skip_advance then
				FrameModule.advance(funcFrame)
			else
				funcFrame.skip_advance = false
			end
		end

		table.remove(vm.call_stack)
		vm.current_frame = prevFrame

		local result = vm.return_value
		if result == nil then
			result = Base.newNone()
		end

		vm.return_value = nil

		local captured = if options and options.captureLocals then locals else nil
		return result, captured
	end

	return {
		call = function(vm: VMState, func: any, args: { any }?, kwargs: { [string]: any }?)
			local result = callInternal(vm, func, args, kwargs, nil)
			return result
		end,
		callWithLocals = function(
			vm: VMState,
			func: any,
			args: { any }?,
			kwargs: { [string]: any }?,
			locals: { [string]: any }?,
			captureLocals: boolean?
		)
			local opts: CallOptions = {
				locals = locals or {},
				captureLocals = if captureLocals == nil then true else captureLocals,
			}
			local result, captured = callInternal(vm, func, args, kwargs, opts)
			return result, captured or opts.locals
		end,
	}
end

return CallFunction


local FrameModule = require("../frame")
local Stack = require("../stack")
local Base = require("../../objects/base")
local Collections = require("../../objects/collections")
local ObjBuiltins = require("../../objects/builtins")
local ClassBuilder = require("../../objects/class_builder")
local Exceptions = require("../../builtins/exceptions")
local interop = require("../../interop/bridge")

local Helpers = require("./helpers")
local Iteration = require("./iteration")
local Blocks = require("./blocks")

local Dispatcher = {}

local compareOperations = Helpers.compareOperations

type CallHelpers = {
	call: (vm: any, func: any, args: { any }?, kwargs: { [string]: any }?) -> any,
	callWithLocals: (
		vm: any,
		func: any,
		args: { any }?,
		kwargs: { [string]: any }?,
		locals: { [string]: any }?,
		captureLocals: boolean?
	) -> (any, { [string]: any }?),
}

type PyObject = Base.PyObject

local callHelpersRef: CallHelpers? = nil

local function ensureCallHelpers(): CallHelpers
	if not callHelpersRef then
		error("Interpreter dispatch: call helpers not configured", 2)
	end
	return callHelpersRef
end

-- Helper to check if value is already a PyObject (performance optimization)
local function isPyObject(value: any): boolean
	return type(value) == "table" and (value :: any).__type ~= nil
end

-- Helper to ensure value is a PyObject (optimized check first)
local function toPyObject(value: any): any
	return if isPyObject(value) then value else Base.ensurePyObject(value)
end

local function instantiateClass(classObj: PyObject, args: { PyObject }): PyObject
	local info = classObj.__value
	if type(info) ~= "table" then
		error("TypeError: cannot instantiate non-class object", 2)
	end
	local pyType = (info :: any).pyType
	if not pyType then
		error("TypeError: invalid class metadata", 2)
	end
	local instance = Base.newInstance(pyType)
	local okInit, initMethod = pcall(function()
		return Base.getattr(instance, "__init__")
	end)
	if okInit and initMethod then
		Base.call(initMethod, args)
	elseif #args > 0 then
		local name = (info :: any).name or classObj.__type
		error(string.format("TypeError: %s() takes no arguments", tostring(name)), 2)
	end
	return instance
end

function Dispatcher.setCallHelpers(helpers: CallHelpers)
	callHelpersRef = helpers
end

function Dispatcher.dispatchInstruction(
	vm: any,
	frame: FrameModule.Frame,
	instruction: any,
	constants: { any }?,
	names: { string }?
): boolean
	local opcode = instruction.opcode
	local arg = instruction.arg

	FrameModule.updateLineNumber(frame)

	if opcode == "POP_TOP" then
		FrameModule.pop(frame)
	elseif opcode == "ROT_TWO" then
		Stack.rotTwo(frame.stack)
	elseif opcode == "ROT_THREE" then
		Stack.rotThree(frame.stack)
	elseif opcode == "DUP_TOP" then
		Stack.duplicate(frame.stack)
	elseif opcode == "LOAD_CONST" then
		assert(arg ~= nil, "LOAD_CONST requires argument")
		local value = nil
		if constants then
			value = constants[arg + 1]
		end
		if constants and value ~= nil then
			local resolved = value
			if typeof(resolved) == "table" and resolved.__nil then
				resolved = nil
			elseif typeof(resolved) == "table" and (resolved :: any).__bytes_literal then
				resolved = ObjBuiltins.Bytes((resolved :: any).value)
			elseif typeof(resolved) == "boolean" then
				resolved = if resolved then ObjBuiltins.True() else ObjBuiltins.False()
			end
			FrameModule.push(frame, resolved)
		else
			FrameModule.push(frame, arg)
		end
	elseif opcode == "LOAD_NAME" then
		assert(arg ~= nil, "LOAD_NAME requires argument")
		local name: string
		if names and names[arg + 1] then
			name = names[arg + 1]
		else
			name = tostring(arg)
		end
		local value = FrameModule.getVariable(frame, name)
		FrameModule.push(frame, value)
	elseif opcode == "STORE_NAME" then
		assert(arg ~= nil, "STORE_NAME requires argument")
		local name: string
		if names and names[arg + 1] then
			name = names[arg + 1]
		else
			name = tostring(arg)
		end
		local value = FrameModule.pop(frame)
		FrameModule.setVariable(frame, name, value)
	elseif opcode == "LOAD_FAST" then
		assert(arg ~= nil, "LOAD_FAST requires argument")
		local value = FrameModule.getFastLocal(frame, arg)
		FrameModule.push(frame, value)
	elseif opcode == "STORE_FAST" then
		assert(arg ~= nil, "STORE_FAST requires argument")
		local value = FrameModule.pop(frame)
		FrameModule.setFastLocal(frame, arg, value)
	elseif opcode == "LOAD_GLOBAL" then
		local name = tostring(arg)
		local value = FrameModule.getGlobal(frame, name)
		FrameModule.push(frame, value)
	elseif opcode == "STORE_GLOBAL" then
		local name = tostring(arg)
		local value = FrameModule.pop(frame)
		FrameModule.setGlobal(frame, name, value)
	elseif opcode == "LOAD_ATTR" then
		assert(arg ~= nil, "LOAD_ATTR requires argument")
		local obj = FrameModule.pop(frame)
		local name: string
		if names and names[arg + 1] then
			name = names[arg + 1]
		else
			name = tostring(arg)
		end
		local pyObj = toPyObject(obj)
		local value = Base.getattr(pyObj, name)
		FrameModule.push(frame, value)
	elseif opcode == "STORE_ATTR" then
		assert(arg ~= nil, "STORE_ATTR requires argument")
		local value = FrameModule.pop(frame)
		local obj = FrameModule.pop(frame)
		local name: string
		if names and names[arg + 1] then
			name = names[arg + 1]
		else
			name = tostring(arg)
		end
		local pyObj = toPyObject(obj)
		local pyValue = toPyObject(value)
		Base.setattr(pyObj, name, pyValue)
	elseif opcode == "LOAD_SUBSCR" then
		local index = FrameModule.pop(frame)
		local container = FrameModule.pop(frame)
		local pyContainer = toPyObject(container)
		local pyIndex = toPyObject(index)
		local getter = Base.getattr(pyContainer, "__getitem__")
		local result = Base.call(getter, { pyIndex })
		FrameModule.push(frame, result)
	elseif opcode == "STORE_SUBSCR" then
		local value = FrameModule.pop(frame)
		local index = FrameModule.pop(frame)
		local container = FrameModule.pop(frame)
		local pyContainer = toPyObject(container)
		local pyIndex = toPyObject(index)
		local pyValue = toPyObject(value)
		local setter = Base.getattr(pyContainer, "__setitem__")
		Base.call(setter, { pyIndex, pyValue })
	elseif opcode == "BINARY_ADD" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local left = toPyObject(a)
		local right = toPyObject(b)
		FrameModule.push(frame, Base.operate("add", left :: any, right :: any))
	elseif opcode == "BINARY_SUBTRACT" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		local bVal = (type(b) == "table" and b.__value ~= nil) and b.__value or b
		FrameModule.push(frame, Base.ensurePyObject(aVal - bVal))
	elseif opcode == "BINARY_MULTIPLY" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		local bVal = (type(b) == "table" and b.__value ~= nil) and b.__value or b
		FrameModule.push(frame, Base.ensurePyObject(aVal * bVal))
	elseif opcode == "BINARY_DIVIDE" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		local bVal = (type(b) == "table" and b.__value ~= nil) and b.__value or b
		if bVal == 0 then
			error("ZeroDivisionError: division by zero", 2)
		end
		FrameModule.push(frame, Base.ensurePyObject(aVal / bVal))
	elseif opcode == "BINARY_MODULO" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		local bVal = (type(b) == "table" and b.__value ~= nil) and b.__value or b
		if bVal == 0 then
			error("ZeroDivisionError: integer division or modulo by zero", 2)
		end
		FrameModule.push(frame, Base.ensurePyObject(aVal % bVal))
	elseif opcode == "BINARY_POWER" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		local bVal = (type(b) == "table" and b.__value ~= nil) and b.__value or b
		FrameModule.push(frame, Base.ensurePyObject(aVal ^ bVal))
	elseif opcode == "BINARY_FLOOR_DIVIDE" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local left = toPyObject(a)
		local right = toPyObject(b)
		FrameModule.push(frame, Base.operate("floordiv", left :: any, right :: any))
	elseif opcode == "BINARY_MATRIX_MULTIPLY" then
		local b = FrameModule.pop(frame)
		local a = FrameModule.pop(frame)
		local left = toPyObject(a)
		local right = toPyObject(b)
		FrameModule.push(frame, Base.operate("matmul", left :: any, right :: any))
	elseif opcode == "UNARY_POSITIVE" then
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		FrameModule.push(frame, Base.ensurePyObject(aVal))
	elseif opcode == "UNARY_NEGATIVE" then
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		FrameModule.push(frame, Base.ensurePyObject(-aVal))
	elseif opcode == "UNARY_NOT" then
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		FrameModule.push(frame, Base.ensurePyObject(not aVal))
	elseif opcode == "UNARY_INVERT" then
		local a = FrameModule.pop(frame)
		local aVal = (type(a) == "table" and a.__value ~= nil) and a.__value or a
		if type(aVal) == "number" and aVal == math.floor(aVal) then
			FrameModule.push(frame, Base.ensurePyObject(bit32.bnot(aVal)))
		else
			error("TypeError: bad operand type for unary ~", 2)
		end
	elseif opcode == "COMPARE_OP" then
		assert(arg ~= nil, "COMPARE_OP requires argument")
		local rawRight = FrameModule.pop(frame)
		local rawLeft = FrameModule.pop(frame)
		local op = compareOperations[arg]
		if not op then
			error("Unknown comparison operation: " .. tostring(arg), 2)
		end
		local result
		if arg == 6 or arg == 7 or arg == 8 or arg == 9 then
			result = op(rawLeft, rawRight)
		else
			local leftVal = (type(rawLeft) == "table" and (rawLeft :: any).__value ~= nil) and (rawLeft :: any).__value
				or rawLeft
			local rightVal = (type(rawRight) == "table" and (rawRight :: any).__value ~= nil)
					and (rawRight :: any).__value
				or rawRight
			result = op(leftVal, rightVal)
		end
		-- Optimize: check if result is already PyObject before wrapping
		FrameModule.push(frame, if isPyObject(result) then result else Base.ensurePyObject(result))
	elseif opcode == "JUMP_FORWARD" then
		assert(arg ~= nil, "JUMP_FORWARD requires argument")
		FrameModule.jump(frame, frame.pc + arg + 1)
		frame.skip_advance = true
	elseif opcode == "POP_JUMP_IF_TRUE" then
		assert(arg ~= nil, "POP_JUMP_IF_TRUE requires argument")
		local value = FrameModule.pop(frame)
		local isTrue = if isPyObject(value) then Base.truthy(value) else (value and true or false)
		if isTrue then
			FrameModule.jump(frame, arg)
			frame.skip_advance = true
		end
	elseif opcode == "POP_JUMP_IF_FALSE" then
		assert(arg ~= nil, "POP_JUMP_IF_FALSE requires argument")
		local value = FrameModule.pop(frame)
		local isTrue = if isPyObject(value) then Base.truthy(value) else (value and true or false)
		if not isTrue then
			FrameModule.jump(frame, arg)
			frame.skip_advance = true
		end
	elseif opcode == "JUMP_IF_TRUE_OR_POP" then
		assert(arg ~= nil, "JUMP_IF_TRUE_OR_POP requires argument")
		local value = FrameModule.peek(frame)
		local isTrue = if isPyObject(value) then Base.truthy(value) else (value and true or false)
		if isTrue then
			FrameModule.jump(frame, arg)
			frame.skip_advance = true
		else
			FrameModule.pop(frame)
		end
	elseif opcode == "JUMP_IF_FALSE_OR_POP" then
		assert(arg ~= nil, "JUMP_IF_FALSE_OR_POP requires argument")
		local value = FrameModule.peek(frame)
		local isTrue = if isPyObject(value) then Base.truthy(value) else (value and true or false)
		if not isTrue then
			FrameModule.jump(frame, arg)
			frame.skip_advance = true
		else
			FrameModule.pop(frame)
		end
	elseif opcode == "SETUP_LOOP" then
		assert(arg ~= nil, "SETUP_LOOP requires argument")
		FrameModule.pushBlock(frame, {
			type = "loop",
			start = frame.pc + 1,
			endPos = arg,
			handler = nil,
			stackSize = FrameModule.stackSize(frame),
		})
	elseif opcode == "SETUP_EXCEPT" then
		assert(arg ~= nil, "SETUP_EXCEPT requires argument")
		FrameModule.pushBlock(frame, {
			type = "except",
			handler = arg,
			start = nil,
			endPos = nil,
			stackSize = FrameModule.stackSize(frame),
		})
	elseif opcode == "SETUP_FINALLY" then
		assert(arg ~= nil, "SETUP_FINALLY requires argument")
		FrameModule.pushBlock(frame, {
			type = "finally",
			handler = arg,
			start = nil,
			endPos = nil,
			stackSize = FrameModule.stackSize(frame),
		})
	elseif opcode == "POP_BLOCK" then
		FrameModule.popBlock(frame)
	elseif opcode == "BREAK_LOOP" then
		local loopTarget: number? = nil
		for i = #frame.block_stack, 1, -1 do
			local blk = frame.block_stack[i]
			if blk.type == "loop" then
				loopTarget = blk.endPos
				break
			end
		end
		if loopTarget == nil then
			error("'break' outside loop", 2)
		end
		Blocks.cleanIteratorOnBreak(frame)
		Blocks.handleBreak(frame, loopTarget)
		return true
	elseif opcode == "CONTINUE_LOOP" then
		local continueTarget: number? = nil
		for i = #frame.block_stack, 1, -1 do
			local blk = frame.block_stack[i]
			if blk.type == "loop" then
				continueTarget = blk.start or arg
				break
			end
		end
		if continueTarget == nil then
			error("'continue' not properly in loop", 2)
		end
		Blocks.handleContinue(frame, continueTarget)
		return true
	elseif opcode == "BEGIN_FINALLY" then
		FrameModule.pushFinally(frame, { reason = "normal" })
	elseif opcode == "END_FINALLY" then
		local record = FrameModule.popFinally(frame)
		if record then
			local reason = record.reason
			if reason == "normal" or reason == nil then
				-- no-op
			elseif reason == "return" then
				local continue = Blocks.handleReturn(vm, frame, record.value)
				if not continue then
					return false
				end
				return true
			elseif reason == "break" then
				assert(record.target ~= nil, "break continuation missing target")
				Blocks.handleBreak(frame, record.target :: number)
				return true
			elseif reason == "continue" then
				assert(record.target ~= nil, "continue continuation missing target")
				Blocks.handleContinue(frame, record.target :: number)
				return true
			elseif reason == "exception" then
				local exc = record.value
				Exceptions.reraise(exc :: Helpers.PyObject)
			end
		end
	elseif opcode == "RETURN_VALUE" then
		local value = FrameModule.pop(frame)
		local continue = Blocks.handleReturn(vm, frame, value)
		if not continue then
			return false
		end
		return true
	elseif opcode == "YIELD_VALUE" then
		local value = FrameModule.pop(frame)
		local generatorFlag = (frame :: any).is_generator
		if not generatorFlag then
			error("RuntimeError: 'yield' outside generator", 2)
		end
		local info = (frame :: any).generator_state
		if not info then
			error("RuntimeError: generator metadata missing", 2)
		end
		info.lastYield = Helpers.coercePyObject(value)
		if not info.nextSend then
			info.nextSend = Base.newNone()
		end
		info.resumeValue = info.nextSend
		info.hasResumeValue = true
		info.nextSend = nil
		FrameModule.advance(frame)
		return false
	elseif opcode == "GET_ITER" then
		local iterable = FrameModule.pop(frame)
		local iterator = Iteration.createIterator(iterable)
		FrameModule.push(frame, iterator)
	elseif opcode == "FOR_ITER" then
		assert(arg ~= nil, "FOR_ITER requires argument")
		local iterator = FrameModule.peek(frame)
		local nextValue = Iteration.getNextValue(iterator)
		if nextValue ~= nil then
			FrameModule.push(frame, nextValue)
		else
			FrameModule.pop(frame)
			FrameModule.jump(frame, arg)
			frame.skip_advance = true
		end
	elseif opcode == "UNPACK_SEQUENCE" then
		assert(arg ~= nil, "UNPACK_SEQUENCE requires argument")
		local sequence = FrameModule.pop(frame)
		local values: { any }
		if type(sequence) == "table" then
			local obj = sequence :: any
			if obj.__type == "list" or obj.__type == "tuple" then
				values = obj.__value
			elseif obj.__type == "set" then
				local arr = {}
				for _, entry in pairs(obj.__value) do
					arr[#arr + 1] = entry.value
				end
				values = arr
			else
				error("ValueError: cannot unpack non-sequence", 2)
			end
		else
			error("ValueError: cannot unpack non-sequence", 2)
		end
		if #values ~= arg then
			error(
				string.format(
					"ValueError: too %s values to unpack (expected %d, got %d)",
					if #values < arg then "few" else "many",
					arg,
					#values
				),
				2
			)
		end
		for i = #values, 1, -1 do
			FrameModule.push(frame, values[i])
		end
	elseif opcode == "BUILD_LIST" then
		assert(arg ~= nil, "BUILD_LIST requires argument")
		local elements = Stack.popN(frame.stack, arg)
		-- Pre-allocate array with known size for better performance
		local arr = table.create(arg)
		-- Stack.popN returns elements in reverse order, so we iterate backwards
		-- and append in order to reverse them back (matching original behavior)
		for i = #elements, 1, -1 do
			arr[#elements - i + 1] = Base.ensurePyObject(elements[i])
		end
		local list = Base.newPyObject("list", arr)
		FrameModule.push(frame, list)
	elseif opcode == "BUILD_TUPLE" then
		assert(arg ~= nil, "BUILD_TUPLE requires argument")
		local elements = Stack.popN(frame.stack, arg)
		-- Pre-allocate array with known size for better performance
		local arr = table.create(arg)
		-- Stack.popN returns elements in reverse order, so we iterate backwards
		-- and append in order to reverse them back (matching original behavior)
		for i = #elements, 1, -1 do
			arr[#elements - i + 1] = Base.ensurePyObject(elements[i])
		end
		local tuple = Base.newPyObject("tuple", arr)
		FrameModule.push(frame, tuple)
	elseif opcode == "BUILD_SET" then
		assert(arg ~= nil, "BUILD_SET requires argument")
		local elements = Stack.popN(frame.stack, arg)
		-- Pre-allocate array with known size for better performance
		local values = table.create(arg)
		-- Stack.popN returns elements in reverse order, so we iterate backwards
		-- and append in order to reverse them back (matching original behavior)
		for i = #elements, 1, -1 do
			values[#elements - i + 1] = Base.ensurePyObject(elements[i])
		end
		local set = Collections.newSet(values)
		FrameModule.push(frame, set)
	elseif opcode == "BUILD_MAP" then
		assert(arg ~= nil, "BUILD_MAP requires argument")
		local popped = Stack.popN(frame.stack, arg * 2)
		local entries = {}
		for i = 1, #popped, 2 do
			local value = Base.ensurePyObject(popped[i])
			local key = Base.ensurePyObject(popped[i + 1])
			entries[#entries + 1] = { key, value }
		end
		local dict = Collections.newDict(entries)
		FrameModule.push(frame, dict)
	elseif opcode == "BUILD_MAP_UNPACK" then
		assert(arg ~= nil, "BUILD_MAP_UNPACK requires argument")
		local dicts = Stack.popN(frame.stack, arg)
		local orderedDicts = {}
		for i = #dicts, 1, -1 do
			orderedDicts[#orderedDicts + 1] = dicts[i]
		end
		local merged = Collections.newDict(nil)
		local dictType = Base.getTypeObject("dict")
		local methods = dictType.methods
		if not methods then
			error("Dict type has no methods", 2)
		end
		local setMethod = methods["set"]
		if not setMethod then
			error("Dict type has no set method", 2)
		end
		for _, dictObj in ipairs(orderedDicts) do
			local pyDict = Base.ensurePyObject(dictObj)
			if pyDict.__type ~= "dict" then
				error(string.format("Argument to ** unpacking must be a mapping, not %s", pyDict.__type), 2)
			end
			local storage = pyDict.__value :: any
			if storage then
				for _, entry in pairs(storage) do
					setMethod(merged, { entry.key, entry.value })
				end
			end
		end
		FrameModule.push(frame, merged)
	elseif opcode == "MAKE_FUNCTION" then
		assert(arg ~= nil, "MAKE_FUNCTION requires argument")
		local flags = arg
		if bit32.band(flags, 0x08) ~= 0 then
			FrameModule.pop(frame)
		end
		if bit32.band(flags, 0x04) ~= 0 then
			FrameModule.pop(frame)
		end
		if bit32.band(flags, 0x02) ~= 0 then
			FrameModule.pop(frame)
		end
		local defaults = nil
		if bit32.band(flags, 0x01) ~= 0 then
			local defaultsTuple = FrameModule.pop(frame)
			if defaultsTuple.__type == "tuple" then
				defaults = {}
				for _, value in ipairs(defaultsTuple.__value) do
					table.insert(defaults, value)
				end
			end
		end
		local codeObj = FrameModule.pop(frame)
		local Functions = require("../../objects/functions")
		local pyCode = Functions.newCode(
			codeObj.name or "<anonymous>",
			codeObj.argcount or 0,
			codeObj.kwonlyargcount or 0,
			codeObj.varnames,
			codeObj.bytecode,
			codeObj.constants,
			codeObj.names,
			defaults,
			"<string>",
			codeObj.firstlineno,
			codeObj.isGenerator
		)
		local func = Functions.newFunction(pyCode, vm.globals, defaults, nil)
		FrameModule.push(frame, func)
	elseif opcode == "BUILD_CLASS" then
		assert(arg ~= nil, "BUILD_CLASS requires argument")
		local baseCount = arg
		local bases = table.create(baseCount)
		for i = baseCount, 1, -1 do
			bases[i] = Helpers.coercePyObject(FrameModule.pop(frame))
		end
		local nameValue = FrameModule.pop(frame)
		local bodyFunc = Helpers.coercePyObject(FrameModule.pop(frame))
		local helpers = ensureCallHelpers()
		local classObj = ClassBuilder.build(vm, helpers, bodyFunc, nameValue, bases)
		FrameModule.push(frame, classObj)
	elseif opcode == "CALL_FUNCTION" then
		assert(arg ~= nil, "CALL_FUNCTION requires argument")
		local argc = arg
		local args = Stack.popN(frame.stack, argc)
		local func = FrameModule.pop(frame)
		local orderedArgs = {}
		for i = #args, 1, -1 do
			orderedArgs[#orderedArgs + 1] = Helpers.coercePyObject(args[i])
		end
		local helpers = ensureCallHelpers()
		if type(func) == "table" and func.__type == "builtin_function_or_method" then
			local result = Base.call(func, orderedArgs)
			FrameModule.push(frame, Helpers.coercePyObject(result))
		elseif type(func) == "table" and func.__type == "bound_method" then
			local result = Base.call(func, orderedArgs)
			FrameModule.push(frame, Helpers.coercePyObject(result))
		elseif type(func) == "table" and func.__type == "luau_function" then
			local callMethod = Base.getattr(func, "__call__")
			local result = Base.call(callMethod, orderedArgs)
			FrameModule.push(frame, Helpers.coercePyObject(result))
		elseif type(func) == "table" and func.__type == "type" then
			local instance = instantiateClass(func, orderedArgs)
			FrameModule.push(frame, instance)
		elseif type(func) == "function" then
			local wrapped = interop.wrapLuauFunction(func)
			local callMethod = Base.getattr(wrapped, "__call__")
			local result = Base.call(callMethod, orderedArgs)
			FrameModule.push(frame, Helpers.coercePyObject(result))
		else
			local result = helpers.call(vm, func, orderedArgs, {})
			FrameModule.push(frame, result)
		end
	elseif opcode == "IMPORT_NAME" then
		assert(arg ~= nil, "IMPORT_NAME requires argument")
		local name: string
		if names and names[arg + 1] then
			name = names[arg + 1]
		else
			name = tostring(arg)
		end

		local fromlist = FrameModule.pop(frame)
		local level = FrameModule.pop(frame)

		local importFunc = vm.builtins["__import__"]
		if not importFunc then
			error("ImportError: __import__ not found", 2)
		end

		-- Convert frame globals to a PyObject dict-like structure for __import__
		-- The import system needs access to __package__ for relative imports
		local globalsForImport = frame.globals

		local args = {
			Base.newPyObject("str", name),
			globalsForImport, -- Pass actual globals for __package__ access
			Base.newNone(), -- locals (not needed)
			toPyObject(fromlist),
			toPyObject(level),
		}

		local result = Base.call(importFunc, args)
		FrameModule.push(frame, result)
	elseif opcode == "IMPORT_FROM" then
		assert(arg ~= nil, "IMPORT_FROM requires argument")
		local name: string
		if names and names[arg + 1] then
			name = names[arg + 1]
		else
			name = tostring(arg)
		end

		local module = FrameModule.peek(frame)
		local pyModule = toPyObject(module)

		local ok, value = pcall(function()
			return Base.getattr(pyModule, name)
		end)

		if ok then
			FrameModule.push(frame, value)
		else
			error("ImportError: cannot import name '" .. name .. "'", 2)
		end
	elseif opcode == "IMPORT_STAR" then
		local module = FrameModule.pop(frame)
		local pyModule = toPyObject(module)

		-- Check for __all__
		local hasAll, allList = pcall(function()
			return Base.getattr(pyModule, "__all__")
		end)

		if hasAll and allList then
			-- Iterate __all__
			local iterator = Iteration.createIterator(allList)
			while true do
				local nameObj = Iteration.getNextValue(iterator)
				if not nameObj then
					break
				end

				if nameObj.__type == "str" then
					local name = nameObj.__value
					local value = Base.getattr(pyModule, name)
					FrameModule.setVariable(frame, name, value)
				end
			end
		else
			-- Iterate __dict__
			local dict = pyModule.__dict
			if dict then
				for name, value in pairs(dict) do
					if not string.match(name, "^_") then
						FrameModule.setVariable(frame, name, value)
					end
				end
			end
		end
	else
		error("Unimplemented opcode: " .. opcode, 2)
	end

	return true
end

return Dispatcher


local Base = require("../../objects/base")
local FrameModule = require("../frame")
local State = require("./state")

local Generator = {}

export type PyObject = Base.PyObject
export type VMState = State.VMState

export type ResumeFunction = (
	vm: VMState,
	frame: FrameModule.Frame,
	instruction: any,
	constants: { any }?,
	names: { string }?
) -> boolean

export type GeneratorParams = {
	code: any,
	globals: { [string]: any },
	builtins: { [string]: any },
	locals: { [string]: any },
	executeInstruction: ResumeFunction,
}

export type GeneratorInfo = {
	vm: VMState,
	frame: FrameModule.Frame,
	code: any,
	executeInstruction: ResumeFunction,
	lastYield: PyObject?,
	returnValue: PyObject?,
	finished: boolean,
	running: boolean,
	nextSend: PyObject?,
	resumeValue: PyObject?,
	hasResumeValue: boolean,
	object: PyObject?,
}

local generatorType: Base.PyType? = nil

local function ensureGeneratorType(): Base.PyType
	if generatorType then
		return generatorType
	end
	local typeObj: Base.PyType
	local ok, existing = pcall(Base.getTypeObject, "generator")
	if ok then
		typeObj = existing :: Base.PyType
	else
		typeObj = Base.registerType("generator", {})
	end
	generatorType = typeObj
	typeObj.methods = typeObj.methods or {}
	local methods = typeObj.methods :: { [string]: (PyObject, { PyObject }?) -> PyObject }
	if not methods["__iter__"] then
		methods["__iter__"] = function(self: PyObject): PyObject
			return self
		end
	end
	methods["__next__"] = function(self: PyObject): PyObject
		return Generator.resume(self, nil)
	end
	return typeObj
end

local function raiseStopIteration(value: PyObject?): never
	local payloadValue = value or Base.newNone()
	local exc = Base.newPyObject("StopIteration", { value = payloadValue, message = nil })
	exc.__dict = exc.__dict or {}
	(exc.__dict :: any).value = payloadValue
	error(exc, 0)
end

local function runGenerator(info: GeneratorInfo, sendValue: PyObject?): PyObject
	if info.finished then
		return raiseStopIteration(info.returnValue)
	end
	if info.running then
		error("ValueError: generator already executing", 2)
	end

	local incoming = sendValue
	if incoming == nil then
		incoming = Base.newNone()
	else
		incoming = Base.ensurePyObject(incoming)
	end

	local vm = info.vm
	local frame = info.frame

	info.running = true
	vm.current_frame = frame
	table.insert(vm.call_stack, frame)

	local ok, err = pcall(function()
		info.nextSend = incoming
		if info.hasResumeValue and info.resumeValue then
			FrameModule.push(frame, info.resumeValue)
			info.resumeValue = nil
			info.hasResumeValue = false
		end
		while FrameModule.hasNext(frame) do
			local instruction = FrameModule.getCurrentInstruction(frame)
			if not instruction then
				break
			end
			local shouldContinue = info.executeInstruction(vm, frame, instruction, info.code.constants, info.code.names)
			if not shouldContinue then
				break
			end
			if not frame.skip_advance then
				FrameModule.advance(frame)
			else
				frame.skip_advance = false
			end
		end
	end)

	info.running = false
	table.remove(vm.call_stack)
	vm.current_frame = nil

	if not ok then
		error(err, 0)
	end

	if info.lastYield then
		local result = info.lastYield
		info.lastYield = nil
		return result
	end

	if info.finished then
		return raiseStopIteration(info.returnValue)
	end

	-- Generator exhausted without explicit finish marker
	info.finished = true
	return raiseStopIteration(info.returnValue)
end

function Generator.new(params: GeneratorParams): PyObject
	local generatorVm = State.newState(params.globals, params.builtins)
	local frame =
		FrameModule.new(params.code.bytecode, params.globals, params.builtins, params.locals, params.code.name)
	local frameAny = frame :: any
	frameAny.is_generator = true

	local info: GeneratorInfo = {
		vm = generatorVm,
		frame = frame,
		code = params.code,
		executeInstruction = params.executeInstruction,
		lastYield = nil,
		returnValue = Base.newNone(),
		finished = false,
		running = false,
		nextSend = nil,
		resumeValue = nil,
		hasResumeValue = false,
		object = nil,
	}

	ensureGeneratorType()
	local generatorObj = Base.newPyObject("generator", info)
	info.object = generatorObj
	frameAny.generator_state = info

	return generatorObj
end

function Generator.resume(self: PyObject, sendValue: PyObject?): PyObject
	local info = self.__value :: GeneratorInfo
	local result = runGenerator(info, sendValue)
	return result
end

return Generator