--!optimize 2
--!nolint LocalShadow
local cst = require("./cst")

local function unreachable(_: never): never
	error("unreachable")
end

export type Visitor<State> = {
	visit_token: (state: State, token: cst.Token) -> (),

	visit_generic_declaration: (state: State, node: cst.GenericDeclaration) -> (),
	visit_generic_declaration_end: (state: State, node: cst.GenericDeclaration) -> (),

	visit_generic_declaration_with_defaults: (state: State, node: cst.GenericDeclarationWithDefaults) -> (),
	visit_generic_declaration_with_defaults_end: (state: State, node: cst.GenericDeclarationWithDefaults) -> (),

	visit_type_arg: (state: State, node: cst.TypeArg) -> (),
	visit_type_arg_end: (state: State, node: cst.TypeArg) -> (),

	visit_table_type_field_name_prop: (state: State, node: cst.TableTypeField_NameProp) -> (),
	visit_table_type_field_name_prop_end: (state: State, node: cst.TableTypeField_NameProp) -> (),
	visit_table_type_field_string_prop: (state: State, node: cst.TableTypeField_StringProp) -> (),
	visit_table_type_field_string_prop_end: (state: State, node: cst.TableTypeField_StringProp) -> (),
	visit_table_type_field_indexer: (state: State, node: cst.TableTypeField_Indexer) -> (),
	visit_table_type_field_indexer_end: (state: State, node: cst.TableTypeField_Indexer) -> (),
	visit_table_type_field: (state: State, node: cst.TableTypeField) -> (),
	visit_table_type_field_end: (state: State, node: cst.TableTypeField) -> (),

	visit_type_nil: (state: State, node: cst.Type_Nil) -> (),
	visit_type_nil_end: (state: State, node: cst.Type_Nil) -> (),
	visit_type_boolean: (state: State, node: cst.Type_Boolean) -> (),
	visit_type_boolean_end: (state: State, node: cst.Type_Boolean) -> (),
	visit_type_string: (state: State, node: cst.Type_String) -> (),
	visit_type_string_end: (state: State, node: cst.Type_String) -> (),
	visit_type_reference: (state: State, node: cst.Type_Reference) -> (),
	visit_type_reference_end: (state: State, node: cst.Type_Reference) -> (),
	visit_type_typeof: (state: State, node: cst.Type_Typeof) -> (),
	visit_type_typeof_end: (state: State, node: cst.Type_Typeof) -> (),
	visit_type_array: (state: State, node: cst.Type_Array) -> (),
	visit_type_array_end: (state: State, node: cst.Type_Array) -> (),
	visit_type_table: (state: State, node: cst.Type_Table) -> (),
	visit_type_table_end: (state: State, node: cst.Type_Table) -> (),
	visit_type_function: (state: State, node: cst.Type_Function) -> (),
	visit_type_function_end: (state: State, node: cst.Type_Function) -> (),
	visit_type_paren: (state: State, node: cst.Type_Paren) -> (),
	visit_type_paren_end: (state: State, node: cst.Type_Paren) -> (),
	visit_type_optional: (state: State, node: cst.Type_Optional) -> (),
	visit_type_optional_end: (state: State, node: cst.Type_Optional) -> (),
	visit_type_union: (state: State, node: cst.Type_Union) -> (),
	visit_type_union_end: (state: State, node: cst.Type_Union) -> (),
	visit_type_intersection: (state: State, node: cst.Type_Intersection) -> (),
	visit_type_intersection_end: (state: State, node: cst.Type_Intersection) -> (),
	visit_type: (state: State, node: cst.Type) -> (),
	visit_type_end: (state: State, node: cst.Type) -> (),

	visit_variadic_type_pack: (state: State, node: cst.VariadicTypePack) -> (),
	visit_variadic_type_pack_end: (state: State, node: cst.VariadicTypePack) -> (),

	visit_generic_type_pack: (state: State, node: cst.GenericTypePack) -> (),
	visit_generic_type_pack_end: (state: State, node: cst.GenericTypePack) -> (),

	visit_type_list: (state: State, node: cst.TypeList) -> (),
	visit_type_list_end: (state: State, node: cst.TypeList) -> (),

	visit_tail_type: (state: State, node: cst.TailType) -> (),
	visit_tail_type_end: (state: State, node: cst.TailType) -> (),

	visit_type_pack: (state: State, node: cst.TypePack) -> (),
	visit_type_pack_end: (state: State, node: cst.TypePack) -> (),

	visit_return_type: (state: State, node: cst.ReturnType) -> (),
	visit_return_type_end: (state: State, node: cst.ReturnType) -> (),

	visit_binding: (state: State, node: cst.Binding) -> (),
	visit_binding_end: (state: State, node: cst.Binding) -> (),

	visit_istring_simple: (state: State, node: cst.IString_Simple) -> (),
	visit_istring_simple_end: (state: State, node: cst.IString_Simple) -> (),
	visit_istring_complex: (state: State, node: cst.IString_Complex) -> (),
	visit_istring_complex_end: (state: State, node: cst.IString_Complex) -> (),
	visit_istring: (state: State, node: cst.IString) -> (),
	visit_istring_end: (state: State, node: cst.IString) -> (),

	visit_if_else_expr_branch: (state: State, node: cst.IfElseExprBranch) -> (),
	visit_if_else_expr_branch_end: (state: State, node: cst.IfElseExprBranch) -> (),

	visit_expr_nil: (state: State, node: cst.Expr_Nil) -> (),
	visit_expr_nil_end: (state: State, node: cst.Expr_Nil) -> (),
	visit_expr_boolean: (state: State, node: cst.Expr_Boolean) -> (),
	visit_expr_boolean_end: (state: State, node: cst.Expr_Boolean) -> (),
	visit_expr_number: (state: State, node: cst.Expr_Number) -> (),
	visit_expr_number_end: (state: State, node: cst.Expr_Number) -> (),
	visit_expr_string: (state: State, node: cst.Expr_String) -> (),
	visit_expr_string_end: (state: State, node: cst.Expr_String) -> (),
	visit_expr_varargs: (state: State, node: cst.Expr_Varargs) -> (),
	visit_expr_varargs_end: (state: State, node: cst.Expr_Varargs) -> (),
	visit_expr_istring: (state: State, node: cst.Expr_IString) -> (),
	visit_expr_istring_end: (state: State, node: cst.Expr_IString) -> (),
	visit_expr_table: (state: State, node: cst.Expr_Table) -> (),
	visit_expr_table_end: (state: State, node: cst.Expr_Table) -> (),
	visit_expr_function: (state: State, node: cst.Expr_Function) -> (),
	visit_expr_function_end: (state: State, node: cst.Expr_Function) -> (),
	visit_expr_if_else: (state: State, node: cst.Expr_IfElse) -> (),
	visit_expr_if_else_end: (state: State, node: cst.Expr_IfElse) -> (),
	visit_expr_var: (state: State, node: cst.Expr_Var) -> (),
	visit_expr_var_end: (state: State, node: cst.Expr_Var) -> (),
	visit_expr_assertion: (state: State, node: cst.Expr_Assertion) -> (),
	visit_expr_assertion_end: (state: State, node: cst.Expr_Assertion) -> (),
	visit_expr_unary: (state: State, node: cst.Expr_Unary) -> (),
	visit_expr_unary_end: (state: State, node: cst.Expr_Unary) -> (),
	visit_expr_binary: (state: State, node: cst.Expr_Binary) -> (),
	visit_expr_binary_end: (state: State, node: cst.Expr_Binary) -> (),
	visit_expr: (state: State, node: cst.Expr) -> (),
	visit_expr_end: (state: State, node: cst.Expr) -> (),

	visit_function_arg_pack: (state: State, node: cst.FunctionArg_Pack) -> (),
	visit_function_arg_pack_end: (state: State, node: cst.FunctionArg_Pack) -> (),
	visit_function_arg_table: (state: State, node: cst.FunctionArg_Table) -> (),
	visit_function_arg_table_end: (state: State, node: cst.FunctionArg_Table) -> (),
	visit_function_arg_string: (state: State, node: cst.FunctionArg_String) -> (),
	visit_function_arg_string_end: (state: State, node: cst.FunctionArg_String) -> (),
	visit_function_arg: (state: State, node: cst.FunctionArg) -> (),
	visit_function_arg_end: (state: State, node: cst.FunctionArg) -> (),

	visit_var_root_name: (state: State, node: cst.VarRoot_Name) -> (),
	visit_var_root_name_end: (state: State, node: cst.VarRoot_Name) -> (),
	visit_var_root_paren: (state: State, node: cst.VarRoot_Paren) -> (),
	visit_var_root_paren_end: (state: State, node: cst.VarRoot_Paren) -> (),
	visit_var_root: (state: State, node: cst.VarRoot) -> (),
	visit_var_root_end: (state: State, node: cst.VarRoot) -> (),

	visit_var_suffix_name_index: (state: State, node: cst.VarSuffix_NameIndex) -> (),
	visit_var_suffix_name_index_end: (state: State, node: cst.VarSuffix_NameIndex) -> (),
	visit_var_suffix_expr_index: (state: State, node: cst.VarSuffix_ExprIndex) -> (),
	visit_var_suffix_expr_index_end: (state: State, node: cst.VarSuffix_ExprIndex) -> (),
	visit_var_suffix_call: (state: State, node: cst.VarSuffix_Call) -> (),
	visit_var_suffix_call_end: (state: State, node: cst.VarSuffix_Call) -> (),
	visit_var_suffix: (state: State, node: cst.VarSuffix) -> (),
	visit_var_suffix_end: (state: State, node: cst.VarSuffix) -> (),

	visit_var: (state: State, node: cst.Var) -> (),
	visit_var_end: (state: State, node: cst.Var) -> (),

	visit_table_field_name_key: (state: State, node: cst.TableField_NameKey) -> (),
	visit_table_field_name_key_end: (state: State, node: cst.TableField_NameKey) -> (),
	visit_table_field_expr_key: (state: State, node: cst.TableField_ExprKey) -> (),
	visit_table_field_expr_key_end: (state: State, node: cst.TableField_ExprKey) -> (),
	visit_table_field_no_key: (state: State, node: cst.TableField_NoKey) -> (),
	visit_table_field_no_key_end: (state: State, node: cst.TableField_NoKey) -> (),
	visit_table_field: (state: State, node: cst.TableField) -> (),
	visit_table_field_end: (state: State, node: cst.TableField) -> (),

	visit_table: (state: State, node: cst.Table) -> (),
	visit_table_end: (state: State, node: cst.Table) -> (),

	visit_function_body: (state: State, node: cst.FunctionBody) -> (),
	visit_function_body_end: (state: State, node: cst.FunctionBody) -> (),

	visit_if_stat_branch: (state: State, node: cst.IfStatBranch) -> (),
	visit_if_stat_branch_end: (state: State, node: cst.IfStatBranch) -> (),

	visit_stat_assign: (state: State, node: cst.Stat_Assign) -> (),
	visit_stat_assign_end: (state: State, node: cst.Stat_Assign) -> (),
	visit_stat_compound_assign: (state: State, node: cst.Stat_CompoundAssign) -> (),
	visit_stat_compound_assign_end: (state: State, node: cst.Stat_CompoundAssign) -> (),
	visit_stat_call: (state: State, node: cst.Stat_Call) -> (),
	visit_stat_call_end: (state: State, node: cst.Stat_Call) -> (),
	visit_stat_do: (state: State, node: cst.Stat_Do) -> (),
	visit_stat_do_end: (state: State, node: cst.Stat_Do) -> (),
	visit_stat_while: (state: State, node: cst.Stat_While) -> (),
	visit_stat_while_end: (state: State, node: cst.Stat_While) -> (),
	visit_stat_repeat: (state: State, node: cst.Stat_Repeat) -> (),
	visit_stat_repeat_end: (state: State, node: cst.Stat_Repeat) -> (),
	visit_stat_if: (state: State, node: cst.Stat_If) -> (),
	visit_stat_if_end: (state: State, node: cst.Stat_If) -> (),
	visit_stat_numeric_for: (state: State, node: cst.Stat_NumericFor) -> (),
	visit_stat_numeric_for_end: (state: State, node: cst.Stat_NumericFor) -> (),
	visit_stat_for_in: (state: State, node: cst.Stat_ForIn) -> (),
	visit_stat_for_in_end: (state: State, node: cst.Stat_ForIn) -> (),
	visit_stat_function: (state: State, node: cst.Stat_Function) -> (),
	visit_stat_function_end: (state: State, node: cst.Stat_Function) -> (),
	visit_stat_local_function: (state: State, node: cst.Stat_LocalFunction) -> (),
	visit_stat_local_function_end: (state: State, node: cst.Stat_LocalFunction) -> (),
	visit_stat_type_function: (state: State, node: cst.Stat_TypeFunction) -> (),
	visit_stat_type_function_end: (state: State, node: cst.Stat_TypeFunction) -> (),
	visit_stat_local_variable: (state: State, node: cst.Stat_LocalVariable) -> (),
	visit_stat_local_variable_end: (state: State, node: cst.Stat_LocalVariable) -> (),
	visit_stat_type: (state: State, node: cst.Stat_Type) -> (),
	visit_stat_type_end: (state: State, node: cst.Stat_Type) -> (),
	visit_stat: (state: State, node: cst.Stat) -> (),
	visit_stat_end: (state: State, node: cst.Stat) -> (),

	visit_last_stat_return: (state: State, node: cst.LastStat_Return) -> (),
	visit_last_stat_return_end: (state: State, node: cst.LastStat_Return) -> (),
	visit_last_stat_break: (state: State, node: cst.LastStat_Break) -> (),
	visit_last_stat_break_end: (state: State, node: cst.LastStat_Break) -> (),
	visit_last_stat_continue: (state: State, node: cst.LastStat) -> (),
	visit_last_stat_continue_end: (state: State, node: cst.LastStat) -> (),
	visit_last_stat: (state: State, node: cst.LastStat) -> (),
	visit_last_stat_end: (state: State, node: cst.LastStat) -> (),

	visit_block: (state: State, node: cst.Block) -> (),
	visit_block_end: (state: State, node: cst.Block) -> (),

	visit_cst: (state: State, node: cst.Cst) -> (),
	visit_cst_end: (state: State, node: cst.Cst) -> (),
}

local function visitor<State>(): Visitor<State>
	local noop: any = function() end

	return {
		visit_token = noop,

		visit_generic_declaration = noop,
		visit_generic_declaration_end = noop,

		visit_generic_declaration_with_defaults = noop,
		visit_generic_declaration_with_defaults_end = noop,

		visit_type_arg = noop,
		visit_type_arg_end = noop,

		visit_table_type_field_name_prop = noop,
		visit_table_type_field_name_prop_end = noop,
		visit_table_type_field_string_prop = noop,
		visit_table_type_field_string_prop_end = noop,
		visit_table_type_field_indexer = noop,
		visit_table_type_field_indexer_end = noop,
		visit_table_type_field = noop,
		visit_table_type_field_end = noop,

		visit_type_nil = noop,
		visit_type_nil_end = noop,
		visit_type_boolean = noop,
		visit_type_boolean_end = noop,
		visit_type_string = noop,
		visit_type_string_end = noop,
		visit_type_reference = noop,
		visit_type_reference_end = noop,
		visit_type_typeof = noop,
		visit_type_typeof_end = noop,
		visit_type_array = noop,
		visit_type_array_end = noop,
		visit_type_table = noop,
		visit_type_table_end = noop,
		visit_type_function = noop,
		visit_type_function_end = noop,
		visit_type_paren = noop,
		visit_type_paren_end = noop,
		visit_type_optional = noop,
		visit_type_optional_end = noop,
		visit_type_union = noop,
		visit_type_union_end = noop,
		visit_type_intersection = noop,
		visit_type_intersection_end = noop,
		visit_type = noop,
		visit_type_end = noop,

		visit_variadic_type_pack = noop,
		visit_variadic_type_pack_end = noop,

		visit_generic_type_pack = noop,
		visit_generic_type_pack_end = noop,

		visit_type_list = noop,
		visit_type_list_end = noop,

		visit_tail_type = noop,
		visit_tail_type_end = noop,

		visit_type_pack = noop,
		visit_type_pack_end = noop,

		visit_return_type = noop,
		visit_return_type_end = noop,

		visit_binding = noop,
		visit_binding_end = noop,

		visit_istring_simple = noop,
		visit_istring_simple_end = noop,

		visit_istring_complex = noop,
		visit_istring_complex_end = noop,

		visit_istring = noop,
		visit_istring_end = noop,

		visit_if_else_expr_branch = noop,
		visit_if_else_expr_branch_end = noop,

		visit_expr_nil = noop,
		visit_expr_nil_end = noop,
		visit_expr_boolean = noop,
		visit_expr_boolean_end = noop,
		visit_expr_number = noop,
		visit_expr_number_end = noop,
		visit_expr_string = noop,
		visit_expr_string_end = noop,
		visit_expr_varargs = noop,
		visit_expr_varargs_end = noop,
		visit_expr_istring = noop,
		visit_expr_istring_end = noop,
		visit_expr_table = noop,
		visit_expr_table_end = noop,
		visit_expr_function = noop,
		visit_expr_function_end = noop,
		visit_expr_if_else = noop,
		visit_expr_if_else_end = noop,
		visit_expr_var = noop,
		visit_expr_var_end = noop,
		visit_expr_assertion = noop,
		visit_expr_assertion_end = noop,
		visit_expr_unary = noop,
		visit_expr_unary_end = noop,
		visit_expr_binary = noop,
		visit_expr_binary_end = noop,
		visit_expr = noop,
		visit_expr_end = noop,

		visit_function_arg_pack = noop,
		visit_function_arg_pack_end = noop,
		visit_function_arg_table = noop,
		visit_function_arg_table_end = noop,
		visit_function_arg_string = noop,
		visit_function_arg_string_end = noop,
		visit_function_arg = noop,
		visit_function_arg_end = noop,

		visit_var_root_name = noop,
		visit_var_root_name_end = noop,
		visit_var_root_paren = noop,
		visit_var_root_paren_end = noop,
		visit_var_root = noop,
		visit_var_root_end = noop,

		visit_var_suffix_name_index = noop,
		visit_var_suffix_name_index_end = noop,
		visit_var_suffix_expr_index = noop,
		visit_var_suffix_expr_index_end = noop,
		visit_var_suffix_call = noop,
		visit_var_suffix_call_end = noop,
		visit_var_suffix = noop,
		visit_var_suffix_end = noop,

		visit_var = noop,
		visit_var_end = noop,

		visit_table_field_name_key = noop,
		visit_table_field_name_key_end = noop,
		visit_table_field_expr_key = noop,
		visit_table_field_expr_key_end = noop,
		visit_table_field_no_key = noop,
		visit_table_field_no_key_end = noop,
		visit_table_field = noop,
		visit_table_field_end = noop,

		visit_table = noop,
		visit_table_end = noop,

		visit_function_body = noop,
		visit_function_body_end = noop,

		visit_if_stat_branch = noop,
		visit_if_stat_branch_end = noop,

		visit_stat_assign = noop,
		visit_stat_assign_end = noop,
		visit_stat_compound_assign = noop,
		visit_stat_compound_assign_end = noop,
		visit_stat_call = noop,
		visit_stat_call_end = noop,
		visit_stat_do = noop,
		visit_stat_do_end = noop,
		visit_stat_while = noop,
		visit_stat_while_end = noop,
		visit_stat_repeat = noop,
		visit_stat_repeat_end = noop,
		visit_stat_if = noop,
		visit_stat_if_end = noop,
		visit_stat_numeric_for = noop,
		visit_stat_numeric_for_end = noop,
		visit_stat_for_in = noop,
		visit_stat_for_in_end = noop,
		visit_stat_function = noop,
		visit_stat_function_end = noop,
		visit_stat_local_function = noop,
		visit_stat_local_function_end = noop,
		visit_stat_type_function = noop,
		visit_stat_type_function_end = noop,
		visit_stat_local_variable = noop,
		visit_stat_local_variable_end = noop,
		visit_stat_type = noop,
		visit_stat_type_end = noop,
		visit_stat = noop,
		visit_stat_end = noop,

		visit_last_stat_return = noop,
		visit_last_stat_return_end = noop,
		visit_last_stat_break = noop,
		visit_last_stat_break_end = noop,
		visit_last_stat_continue = noop,
		visit_last_stat_continue_end = noop,
		visit_last_stat = noop,
		visit_last_stat_end = noop,

		visit_block = noop,
		visit_block_end = noop,

		visit_cst = noop,
		visit_cst_end = noop,
	}
end

local visit_generic_declaration: <State>(visitor: Visitor<State>, state: State, node: cst.GenericDeclaration) -> ()
local visit_generic_declaration_with_defaults: <State>(visitor: Visitor<State>, state: State, node: cst.GenericDeclarationWithDefaults) -> ()
local visit_type: <State>(visitor: Visitor<State>, state: State, node: cst.Type) -> ()
local visit_tail_type: <State>(visitor: Visitor<State>, state: State, node: cst.TailType) -> ()
local visit_type_pack: <State>(visitor: Visitor<State>, state: State, node: cst.TypePack) -> ()
local visit_return_type: <State>(visitor: Visitor<State>, state: State, node: cst.ReturnType) -> ()
local visit_expr: <State>(visitor: Visitor<State>, state: State, node: cst.Expr) -> ()
local visit_var: <State>(visitor: Visitor<State>, state: State, node: cst.Var) -> ()
local visit_table: <State>(visitor: Visitor<State>, state: State, node: cst.Table) -> ()
local visit_function_body: <State>(visitor: Visitor<State>, state: State, node: cst.FunctionBody) -> ()
local visit_stat: <State>(visitor: Visitor<State>, state: State, node: cst.Stat) -> ()
local visit_last_stat: <State>(visitor: Visitor<State>, state: State, node: cst.LastStat) -> ()
local visit_block: <State>(visitor: Visitor<State>, state: State, node: cst.Block) -> ()

local function visit_token<State>(visitor: Visitor<State>, state: State, token: cst.Token)
	visitor.visit_token(state, token)
end

function visit_generic_declaration<State>(visitor: Visitor<State>, state: State, node: cst.GenericDeclaration)
	visitor.visit_generic_declaration(state, node)
	visit_token(visitor, state, node.angles.open)

	for _, pair in node.names do
		local node, sep = pair.node, pair.sep

		visit_token(visitor, state, node)

		if sep then
			visit_token(visitor, state, sep)
		end
	end

	for _, pair in node.packs do
		local node, sep = pair.node, pair.sep

		visit_token(visitor, state, node.name)
		visit_token(visitor, state, node.dots)

		if sep then
			visit_token(visitor, state, sep)
		end
	end

	visit_token(visitor, state, node.angles.close)
	visitor.visit_generic_declaration_end(state, node)
end

function visit_generic_declaration_with_defaults<State>(visitor: Visitor<State>, state: State, node: cst.GenericDeclarationWithDefaults)
	visitor.visit_generic_declaration_with_defaults(state, node)
	visit_token(visitor, state, node.angles.open)

	for _, pair in node.names do
		local node, sep = pair.node, pair.sep

		visit_token(visitor, state, node.name)

		local default = node.default
		if default then
			visit_token(visitor, state, default.equals)
			visit_type(visitor, state, default.type)
		end

		if sep then
			visit_token(visitor, state, sep)
		end
	end

	for _, pair in node.packs do
		local node, sep = pair.node, pair.sep

		visit_token(visitor, state, node.name)
		visit_token(visitor, state, node.dots)

		local default = node.default
		if default then
			visit_token(visitor, state, default.equals)
			visit_type_pack(visitor, state, default.pack)
		end

		if sep then
			visit_token(visitor, state, sep)
		end
	end

	visit_token(visitor, state, node.angles.close)
	visitor.visit_generic_declaration_with_defaults_end(state, node)
end

function visit_type<State>(visitor: Visitor<State>, state: State, node: cst.Type)
	visitor.visit_type(state, node)

	if node.kind == "nil" then
		visitor.visit_type_nil(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_type_nil_end(state, node)
	elseif node.kind == "boolean" then
		visitor.visit_type_boolean(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_type_boolean_end(state, node)
	elseif node.kind == "string" then
		visitor.visit_type_string(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_type_string_end(state, node)
	elseif node.kind == "reference" then
		node = node :: cst.Type_Reference
		visitor.visit_type_reference(state, node)

		local prefix = node.prefix
		if prefix then
			visit_token(visitor, state, prefix.name)
			visit_token(visitor, state, prefix.dot)
		end

		visit_token(visitor, state, node.name)

		local generics = node.generics
		if generics then
			visit_token(visitor, state, generics.angles.open)

			for _, pair in generics.types do
				local node, sep = pair.node, pair.sep

				visitor.visit_type_arg(state, node)

				if
					node.kind == "variadic"
					or node.kind == "generic"
					or node.kind == "list"
				then
					visit_type_pack(visitor, state, node)
				else
					visit_type(visitor, state, node)
				end

				visitor.visit_type_arg_end(state, node)

				if sep then
					visit_token(visitor, state, sep)
				end
			end

			visit_token(visitor, state, generics.angles.close)
		end

		visitor.visit_type_reference_end(state, node)
	elseif node.kind == "typeof" then
		visitor.visit_type_typeof(state, node)
		visit_token(visitor, state, node.typeof_tok)
		visit_token(visitor, state, node.parens.open)
		visit_expr(visitor, state, node.expr)
		visit_token(visitor, state, node.parens.close)
		visitor.visit_type_typeof_end(state, node)
	elseif node.kind == "array" then
		visitor.visit_type_array(state, node)
		visit_token(visitor, state, node.braces.open)
		visit_type(visitor, state, node.type)
		visit_token(visitor, state, node.braces.close)
		visitor.visit_type_array_end(state, node)
	elseif node.kind == "table" then
		visitor.visit_type_table(state, node)
		visit_token(visitor, state, node.braces.open)

		for _, pair in node.fields do
			local node, sep = pair.node, pair.sep

			visitor.visit_table_type_field(state, node)

			if node.kind == "nameprop" then
				visitor.visit_table_type_field_name_prop(state, node)

				local access = node.access
				if access then
					visit_token(visitor, state, access)
				end

				visit_token(visitor, state, node.name)
				visit_token(visitor, state, node.colon)
				visit_type(visitor, state, node.type)
				visitor.visit_table_type_field_name_prop_end(state, node)
			elseif node.kind == "stringprop" then
				visitor.visit_table_type_field_string_prop(state, node)

				local access = node.access
				if access then
					visit_token(visitor, state, access)
				end

				visit_token(visitor, state, node.bracks.open)
				visit_token(visitor, state, node.key)
				visit_token(visitor, state, node.bracks.close)
				visit_token(visitor, state, node.colon)
				visit_type(visitor, state, node.type)
				visitor.visit_table_type_field_string_prop_end(state, node)
			elseif node.kind == "indexer" then
				visitor.visit_table_type_field_indexer(state, node)

				local access = node.access
				if access then
					visit_token(visitor, state, access)
				end

				visit_token(visitor, state, node.bracks.open)
				visit_type(visitor, state, node.key)
				visit_token(visitor, state, node.bracks.close)
				visit_token(visitor, state, node.colon)
				visit_type(visitor, state, node.type)
				visitor.visit_table_type_field_indexer_end(state, node)
			else
				unreachable(node.kind)
			end

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visit_token(visitor, state, node.braces.close)
		visitor.visit_type_table_end(state, node)
	elseif node.kind == "function" then
		node = (node :: any) :: cst.Type_Function
		visitor.visit_type_function(state, node)

		local generics = node.generics
		if generics then
			visit_generic_declaration(visitor, state, generics)
		end

		visit_token(visitor, state, node.parens.open)

		for _, pair in node.params do
			local node, sep = pair.node, pair.sep

			local name = node.name
			if name then
				visit_token(visitor, state, name.name)
				visit_token(visitor, state, name.colon)
			end

			visit_type(visitor, state, node.type)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		local varargs = node.varargs
		if varargs then
			visit_tail_type(visitor, state, varargs)
		end

		visit_token(visitor, state, node.parens.close)
		visit_token(visitor, state, node.arrow)
		visit_return_type(visitor, state, node.ret)

		visitor.visit_type_function_end(state, node)
	elseif node.kind == "paren" then
		node = node :: cst.Type_Paren
		visitor.visit_type_paren(state, node)
		visit_token(visitor, state, node.parens.open)
		visit_type(visitor, state, node.type)
		visit_token(visitor, state, node.parens.close)
		visitor.visit_type_paren_end(state, node)
	elseif node.kind == "optional" then
		visitor.visit_type_optional(state, node)
		visit_type(visitor, state, node.type)
		visit_token(visitor, state, node.question)
		visitor.visit_type_optional_end(state, node)
	elseif node.kind == "union" then
		node = node :: cst.Type_Union
		visitor.visit_type_union(state, node)

		local leading = node.leading
		if leading then
			visit_token(visitor, state, leading)
		end

		for _, pair in node.types do
			local node, sep = pair.node, pair.sep

			visit_type(visitor, state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visitor.visit_type_union_end(state, node)
	elseif node.kind == "intersection" then
		node = node :: cst.Type_Intersection
		visitor.visit_type_intersection(state, node)

		local leading = node.leading
		if leading then
			visit_token(visitor, state, leading)
		end

		for _, pair in node.types do
			local node, sep = pair.node, pair.sep

			visit_type(visitor, state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visitor.visit_type_intersection_end(state, node)
	else
		unreachable(node.kind)
	end

	visitor.visit_type_end(state, node)
end

do
	local function visit_any_pack<State>(visitor: Visitor<State>, state: State, node: cst.TypePack)
		if node.kind == "variadic" then
			visitor.visit_variadic_type_pack(state, node)
			visit_token(visitor, state, node.dots)
			visit_type(visitor, state, node.type)
			visitor.visit_variadic_type_pack_end(state, node)
		elseif node.kind == "generic" then
			visitor.visit_generic_type_pack(state, node)
			visit_token(visitor, state, node.name)
			visit_token(visitor, state, node.dots)
			visitor.visit_generic_type_pack_end(state, node)
		elseif node.kind == "list" then
			visitor.visit_type_list(state, node)
			visit_token(visitor, state, node.parens.open)

			for _, pair in node.types do
				local node, sep = pair.node, pair.sep

				visit_type(visitor, state, node)

				if sep then
					visit_token(visitor, state, sep)
				end
			end

			local tail = node.tail
			if tail then
				visit_tail_type(visitor, state, tail)
			end

			visit_token(visitor, state, node.parens.close)
			visitor.visit_type_list_end(state, node)
		else
			unreachable(node.kind)
		end
	end

	function visit_tail_type<State>(visitor: Visitor<State>, state: State, node: cst.TailType)
		visitor.visit_tail_type(state, node)
		visit_any_pack(visitor, state, node)
		visitor.visit_tail_type_end(state, node)
	end

	function visit_type_pack<State>(visitor: Visitor<State>, state: State, node: cst.TypePack)
		visitor.visit_type_pack(state, node)
		visit_any_pack(visitor, state, node)
		visitor.visit_type_pack_end(state, node)
	end

	function visit_return_type<State>(visitor: Visitor<State>, state: State, node: cst.ReturnType)
		visitor.visit_return_type(state, node)

		if
			node.kind == "variadic"
			or node.kind == "generic"
			or node.kind == "list"
		then
			visit_type_pack(visitor, state, node)
		else
			visit_type(visitor, state, node)
		end

		visitor.visit_return_type_end(state, node)
	end
end

function visit_expr<State>(visitor: Visitor<State>, state: State, node: cst.Expr)
	visitor.visit_expr(state, node)

	if node.kind == "nil" then
		visitor.visit_expr_nil(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_expr_nil_end(state, node)
	elseif node.kind == "boolean" then
		visitor.visit_expr_boolean(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_expr_boolean_end(state, node)
	elseif node.kind == "number" then
		visitor.visit_expr_number(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_expr_number_end(state, node)
	elseif node.kind == "string" then
		visitor.visit_expr_string(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_expr_string_end(state, node)
	elseif node.kind == "varargs" then
		visitor.visit_expr_varargs(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_expr_varargs_end(state, node)
	elseif node.kind == "istring" then
		visitor.visit_expr_istring(state, node)

		local istring = node.istring
		visitor.visit_istring(state, istring)

		if istring.kind == "simple" then
			visitor.visit_istring_simple(state, istring)
			visit_token(visitor, state, istring.tok)
			visitor.visit_istring_simple_end(state, istring)
		elseif istring.kind == "complex" then
			visitor.visit_istring_complex(state, istring)
			visit_token(visitor, state, istring.first)

			for _, pair in istring.middle do
				local node, sep = pair.node, pair.sep

				visit_expr(visitor, state, node)

				if sep then
					visit_token(visitor, state, sep)
				end
			end

			visit_token(visitor, state, istring.last)
			visitor.visit_istring_complex_end(state, istring)
		else
			unreachable(istring.kind)
		end

		visitor.visit_istring_end(state, istring)
		visitor.visit_expr_istring_end(state, node)
	elseif node.kind == "table" then
		visitor.visit_expr_table(state, node)
		visit_table(visitor, state, node.table)
		visitor.visit_expr_table_end(state, node)
	elseif node.kind == "function" then
		visitor.visit_expr_function(state, node)

		for _, attribute in node.attributes do
			visit_token(visitor, state, attribute)
		end

		visit_token(visitor, state, node.function_tok)
		visit_function_body(visitor, state, node.body)
		visitor.visit_expr_function_end(state, node)
	elseif node.kind == "ifelse" then
		node = node :: cst.Expr_IfElse
		visitor.visit_expr_if_else(state, node)
		visit_token(visitor, state, node.if_tok)

		for _, pair in node.branches do
			local node, sep = pair.node, pair.sep

			visitor.visit_if_else_expr_branch(state, node)

			visit_expr(visitor, state, node.condition)
			visit_token(visitor, state, node.then_tok)
			visit_expr(visitor, state, node.body)

			visitor.visit_if_else_expr_branch_end(state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visit_token(visitor, state, node.else_tok)
		visit_expr(visitor, state, node.else_body)
		visitor.visit_expr_if_else_end(state, node)
	elseif node.kind == "var" then
		visitor.visit_expr_var(state, node)
		visit_var(visitor, state, node.var)
		visitor.visit_expr_var_end(state, node)
	elseif node.kind == "assertion" then
		node = node :: cst.Expr_Assertion
		visitor.visit_expr_assertion(state, node)
		visit_expr(visitor, state, node.expr)
		visit_token(visitor, state, node.colon)
		visit_type(visitor, state, node.type)
		visitor.visit_expr_assertion_end(state, node)
	elseif node.kind == "unary" then
		visitor.visit_expr_unary(state, node)
		visit_token(visitor, state, node.operator)
		visit_expr(visitor, state, node.expr)
		visitor.visit_expr_unary_end(state, node)
	elseif node.kind == "binary" then
		node = node :: cst.Expr_Binary
		visitor.visit_expr_binary(state, node)
		visit_expr(visitor, state, node.left)
		visit_token(visitor, state, node.operator)
		visit_expr(visitor, state, node.right)
		visitor.visit_expr_binary_end(state, node)
	else
		unreachable(node.kind)
	end

	visitor.visit_expr_end(state, node)
end

function visit_var<State>(visitor: Visitor<State>, state: State, node: cst.Var)
	visitor.visit_var(state, node)

	local root = node.root
	visitor.visit_var_root(state, root)

	if root.kind == "name" then
		visitor.visit_var_root_name(state, root)
		visit_token(visitor, state, root.tok)
		visitor.visit_var_root_name_end(state, root)
	elseif root.kind == "paren" then
		visitor.visit_var_root_paren(state, root)
		visit_token(visitor, state, root.parens.open)
		visit_expr(visitor, state, root.expr)
		visit_token(visitor, state, root.parens.close)
		visitor.visit_var_root_paren_end(state, root)
	else
		unreachable(root.kind)
	end

	visitor.visit_var_root_end(state, root)

	for _, suffix in node.suffixes do
		visitor.visit_var_suffix(state, suffix)

		if suffix.kind == "nameindex" then
			visitor.visit_var_suffix_name_index(state, suffix)
			visit_token(visitor, state, suffix.dot)
			visit_token(visitor, state, suffix.name)
			visitor.visit_var_suffix_name_index_end(state, suffix)
		elseif suffix.kind == "exprindex" then
			visitor.visit_var_suffix_expr_index(state, suffix)
			visit_token(visitor, state, suffix.bracks.open)
			visit_expr(visitor, state, suffix.expr)
			visit_token(visitor, state, suffix.bracks.close)
			visitor.visit_var_suffix_expr_index_end(state, suffix)
		elseif suffix.kind == "call" then
			visitor.visit_var_suffix_call(state, suffix)

			local method = suffix.method
			if method then
				visit_token(visitor, state, method.colon)
				visit_token(visitor, state, method.name)
			end

			local arg = suffix.arg
			visitor.visit_function_arg(state, arg)

			if arg.kind == "pack" then
				visitor.visit_function_arg_pack(state, arg)
				visit_token(visitor, state, arg.parens.open)

				for _, pair in arg.exprs do
					local node, sep = pair.node, pair.sep

					visit_expr(visitor, state, node)

					if sep then
						visit_token(visitor, state, sep)
					end
				end

				visit_token(visitor, state, arg.parens.close)
				visitor.visit_function_arg_pack_end(state, arg)
			elseif arg.kind == "table" then
				visitor.visit_function_arg_table(state, arg)
				visit_table(visitor, state, arg.table)
				visitor.visit_function_arg_table_end(state, arg)
			elseif arg.kind == "string" then
				visitor.visit_function_arg_string(state, arg)
				visit_token(visitor, state, arg.tok)
				visitor.visit_function_arg_string_end(state, arg)
			else
				unreachable(arg.kind)
			end
		else
			unreachable(suffix.kind)
		end

		visitor.visit_var_suffix_end(state, suffix)
	end

	visitor.visit_var_end(state, node)
end

function visit_table<State>(visitor: Visitor<State>, state: State, node: cst.Table)
	visitor.visit_table(state, node)
	visit_token(visitor, state, node.braces.open)

	for _, pair in node.fields do
		local node, sep = pair.node, pair.sep

		visitor.visit_table_field(state, node)

		if node.kind == "namekey" then
			visitor.visit_table_field_name_key(state, node)
			visit_token(visitor, state, node.name)
			visit_token(visitor, state, node.equals)
			visit_expr(visitor, state, node.value)
			visitor.visit_table_field_name_key_end(state, node)
		elseif node.kind == "exprkey" then
			visitor.visit_table_field_expr_key(state, node)
			visit_token(visitor, state, node.bracks.open)
			visit_expr(visitor, state, node.key)
			visit_token(visitor, state, node.bracks.close)
			visit_token(visitor, state, node.equals)
			visit_expr(visitor, state, node.value)
			visitor.visit_table_field_expr_key_end(state, node)
		elseif node.kind == "nokey" then
			visitor.visit_table_field_no_key(state, node)
			visit_expr(visitor, state, node.value)
			visitor.visit_table_field_no_key_end(state, node)
		else
			unreachable(node.kind)
		end

		visitor.visit_table_field_end(state, node)

		if sep then
			visit_token(visitor, state, sep)
		end
	end

	visit_token(visitor, state, node.braces.close)
	visitor.visit_table_end(state, node)
end

function visit_function_body<State>(visitor: Visitor<State>, state: State, node: cst.FunctionBody)
	visitor.visit_function_body(state, node)

	local generics = node.generics
	if generics then
		visit_generic_declaration(visitor, state, generics)
	end

	visit_token(visitor, state, node.parens.open)

	for _, pair in node.params do
		local node, sep = pair.node, pair.sep

		visitor.visit_binding(state, node)
		visit_token(visitor, state, node.name)

		local type = node.type
		if type then
			visit_token(visitor, state, type.colon)
			visit_type(visitor, state, type.type)
		end

		visitor.visit_binding_end(state, node)

		if sep then
			visit_token(visitor, state, sep)
		end
	end

	visit_token(visitor, state, node.parens.close)

	local varargs = node.varargs
	if varargs then
		visit_token(visitor, state, varargs.dots)

		local type = varargs.type
		if type then
			visit_token(visitor, state, type.colon)

			local type = type.type
			if type.kind == "generic" then
				visitor.visit_generic_type_pack(state, type)
				visit_token(visitor, state, type.name)
				visit_token(visitor, state, type.dots)
				visitor.visit_generic_type_pack_end(state, type)
			else
				visit_type(visitor, state, type)
			end
		end
	end

	local ret = node.ret
	if ret then
		visit_token(visitor, state, ret.colon)
		visit_return_type(visitor, state, ret.type)
	end

	visit_block(visitor, state, node.block)
	visit_token(visitor, state, node.end_tok)
	visitor.visit_function_body_end(state, node)
end

function visit_stat<State>(visitor: Visitor<State>, state: State, node: cst.Stat)
	visitor.visit_stat(state, node)

	if node.kind == "assign" then
		node = node :: cst.Stat_Assign
		visitor.visit_stat_assign(state, node)

		for _, pair in node.left do
			local node, sep = pair.node, pair.sep

			visit_var(visitor, state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visit_token(visitor, state, node.equals)

		for _, pair in node.right do
			local node, sep = pair.node, pair.sep

			visit_expr(visitor, state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visitor.visit_stat_assign_end(state, node)
	elseif node.kind == "compoundassign" then
		node = node :: cst.Stat_CompoundAssign
		visitor.visit_stat_compound_assign(state, node)
		visit_var(visitor, state, node.left)
		visit_token(visitor, state, node.operator)
		visit_expr(visitor, state, node.right)
		visitor.visit_stat_compound_assign_end(state, node)
	elseif node.kind == "call" then
		visitor.visit_stat_call(state, node)
		visit_var(visitor, state, node.call)
		visitor.visit_stat_call_end(state, node)
	elseif node.kind == "do" then
		visitor.visit_stat_do(state, node)
		visit_token(visitor, state, node.do_tok)
		visit_block(visitor, state, node.block)
		visit_token(visitor, state, node.end_tok)
		visitor.visit_stat_do_end(state, node)
	elseif node.kind == "while" then
		visitor.visit_stat_while(state, node)
		visit_token(visitor, state, node.while_tok)
		visit_expr(visitor, state, node.condition)
		visit_token(visitor, state, node.do_tok)
		visit_block(visitor, state, node.block)
		visit_token(visitor, state, node.end_tok)
		visitor.visit_stat_while_end(state, node)
	elseif node.kind == "repeat" then
		visitor.visit_stat_repeat(state, node)
		visit_token(visitor, state, node.repeat_tok)
		visit_block(visitor, state, node.block)
		visit_token(visitor, state, node.until_tok)
		visit_expr(visitor, state, node.condition)
		visitor.visit_stat_repeat_end(state, node)
	elseif node.kind == "if" then
		visitor.visit_stat_if(state, node)
		visit_token(visitor, state, node.if_tok)

		for _, pair in node.branches do
			local node, sep = pair.node, pair.sep

			visitor.visit_if_stat_branch(state, node)

			visit_expr(visitor, state, node.condition)
			visit_token(visitor, state, node.then_tok)
			visit_block(visitor, state, node.block)

			visitor.visit_if_stat_branch_end(state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		local else_branch = node.else_branch
		if else_branch then
			visit_token(visitor, state, else_branch.else_tok)
			visit_block(visitor, state, else_branch.block)
		end

		visit_token(visitor, state, node.end_tok)
		visitor.visit_stat_if_end(state, node)
	elseif node.kind == "numericfor" then
		visitor.visit_stat_numeric_for(state, node)
		visit_token(visitor, state, node.for_tok)

		local binding = node.binding
		visitor.visit_binding(state, binding)
		visit_token(visitor, state, binding.name)

		local type = binding.type
		if type then
			visit_token(visitor, state, type.colon)
			visit_type(visitor, state, type.type)
		end

		visitor.visit_binding_end(state, binding)

		visit_token(visitor, state, node.equals)
		visit_expr(visitor, state, node.start_expr)
		visit_token(visitor, state, node.comma)
		visit_expr(visitor, state, node.end_expr)

		local step = node.step
		if step then
			visit_token(visitor, state, step.comma)
			visit_expr(visitor, state, step.expr)
		end

		visit_token(visitor, state, node.do_tok)
		visit_block(visitor, state, node.block)
		visit_token(visitor, state, node.end_tok)
		visitor.visit_stat_numeric_for_end(state, node)
	elseif node.kind == "forin" then
		node = node :: cst.Stat_ForIn
		visitor.visit_stat_for_in(state, node)
		visit_token(visitor, state, node.for_tok)

		for _, pair in node.bindings do
			local node, sep = pair.node, pair.sep

			visitor.visit_binding(state, node)
			visit_token(visitor, state, node.name)

			local type = node.type
			if type then
				visit_token(visitor, state, type.colon)
				visit_type(visitor, state, type.type)
			end

			visitor.visit_binding_end(state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visit_token(visitor, state, node.in_tok)

		for _, pair in node.exprs do
			local node, sep = pair.node, pair.sep

			visit_expr(visitor, state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visit_token(visitor, state, node.do_tok)
		visit_block(visitor, state, node.block)
		visit_token(visitor, state, node.end_tok)
		visitor.visit_stat_for_in_end(state, node)
	elseif node.kind == "function" then
		visitor.visit_stat_function(state, node)

		for _, tok in node.attributes do
			visit_token(visitor, state, tok)
		end

		visit_token(visitor, state, node.function_tok)

		for _, pair in node.names do
			local node, sep = pair.node, pair.sep

			visit_token(visitor, state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		local method = node.method
		if method then
			visit_token(visitor, state, method.colon)
			visit_token(visitor, state, method.name)
		end

		visit_function_body(visitor, state, node.body)
		visitor.visit_stat_function_end(state, node)
	elseif node.kind == "localfunction" then
		node = node :: cst.Stat_LocalFunction
		visitor.visit_stat_local_function(state, node)

		for _, tok in node.attributes do
			visit_token(visitor, state, tok)
		end

		visit_token(visitor, state, node.local_tok)
		visit_token(visitor, state, node.function_tok)
		visit_token(visitor, state, node.name)
		visit_function_body(visitor, state, node.body)
		visitor.visit_stat_local_function_end(state, node)
	elseif node.kind == "typefunction" then
		node = node :: cst.Stat_TypeFunction

		visitor.visit_stat_type_function(state, node)

		local export_tok = node.export_tok
		if export_tok then
			visit_token(visitor, state, export_tok)
		end

		visit_token(visitor, state, node.type_tok)
		visit_token(visitor, state, node.function_tok)
		visit_token(visitor, state, node.name)

		visit_function_body(visitor, state, node.body)
		visitor.visit_stat_type_function_end(state, node)
	elseif node.kind == "localvariable" then
		node = node :: cst.Stat_LocalVariable
		visitor.visit_stat_local_variable(state, node)
		visit_token(visitor, state, node.local_tok)

		for _, pair in node.bindings do
			local node, sep = pair.node, pair.sep

			visit_token(visitor, state, node.name)

			local type = node.type
			if type then
				visit_token(visitor, state, type.colon)
				visit_type(visitor, state, type.type)
			end

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		local values = node.values
		if values then
			visit_token(visitor, state, values.equals)

			for _, pair in values.exprs do
				local node, sep = pair.node, pair.sep

				visit_expr(visitor, state, node)

				if sep then
					visit_token(visitor, state, sep)
				end
			end
		end

		visitor.visit_stat_local_variable_end(state, node)
	elseif node.kind == "type" then
		node = node :: cst.Stat_Type
		visitor.visit_stat_type(state, node)

		local export_tok = node.export_tok
		if export_tok then
			visit_token(visitor, state, export_tok)
		end

		visit_token(visitor, state, node.type_tok)
		visit_token(visitor, state, node.name)

		local generics = node.generics
		if generics then
			visit_generic_declaration_with_defaults(visitor, state, generics)
		end

		visit_token(visitor, state, node.equals)
		visit_type(visitor, state, node.type)
		visitor.visit_stat_type_end(state, node)
	end

	visitor.visit_stat_end(state, node)
end

function visit_last_stat<State>(visitor: Visitor<State>, state: State, node: cst.LastStat)
	visitor.visit_last_stat(state, node)

	if node.kind == "return" then
		visitor.visit_last_stat_return(state, node)
		visit_token(visitor, state, node.return_tok)

		for _, pair in node.exprs do
			local node, sep = pair.node, pair.sep

			visit_expr(visitor, state, node)

			if sep then
				visit_token(visitor, state, sep)
			end
		end

		visitor.visit_last_stat_return_end(state, node)
	elseif node.kind == "break" then
		visitor.visit_last_stat_break(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_last_stat_break_end(state, node)
	elseif node.kind == "continue" then
		visitor.visit_last_stat_continue(state, node)
		visit_token(visitor, state, node.tok)
		visitor.visit_last_stat_continue_end(state, node)
	else
		unreachable(node.kind)
	end

	visitor.visit_last_stat_end(state, node)
end

function visit_block<State>(visitor: Visitor<State>, state: State, block: cst.Block)
	visitor.visit_block(state, block)

	for _, pair in block.stats do
		local node, sep = pair.node, pair.sep

		visit_stat(visitor, state, node)

		if sep then
			visit_token(visitor, state, sep)
		end
	end

	local last_stat = block.last_stat
	if last_stat then
		visit_last_stat(visitor, state, last_stat.stat)

		local semicolon = last_stat.semicolon
		if semicolon then
			visit_token(visitor, state, semicolon)
		end
	end

	visitor.visit_block_end(state, block)
end

local function visit<State>(visitor: Visitor<State>, state: State, node: cst.Cst)
	visitor.visit_cst(state, node)
	visit_block(visitor, state, node.block)
	visit_token(visitor, state, node.eof)
	visitor.visit_cst_end(state, node)
end

return {
	visitor = visitor,
	visit = visit,
}


--- Stores the locational information of a token or AST node.
---
--- Components:
--- * x: The start index.
--- * y: The end index.
--- * z: Unused.
export type Span = { read x: number, read y: number, read z: number }

--- An identifier or name; such as `foo` or `bar`.
export type AstIdentifier = {
	--- The span of the node.
	read span: Span,

	--- The name of the identifier.
	read name: string,
}

--- A number literal; such as `123` or `0xABCD`.
export type AstNumber = {
	--- The span of the node.
	read span: Span,

	--- The text of the number.
	read text: string,
}

--- A string literal; such as `"foo"` or `'bar'`.
export type AstString = {
	--- The span of the node.
	read span: Span,

	--- The string value of the literal, including the quotes.
	read value: string,
}

--- An attribute, such as `@native`.
export type AstAttribute = {
	--- The span of the node.
	read span: Span,

	--- The name of the attribute, not including the `@`.
	read name: string,
}

--- A generic declaration without default types; the `<A, B, C...>` in
--- `<A, B, C...>() -> ()`.
---
--- This is used in function declarations, local function declarations, and
--- function types.
---
--- All names must come before all packs. For example, `<A, B..., C>` is invalid
--- because `C` comes after `B...`.
export type AstGenericDeclaration = {
	--- The span of the node.
	read span: Span,

	--- The generic types; the `A, B,` in `<A, B, C...>`
	read names: { AstIdentifier },

	--- The generic type packs; the `C...` in `<A, B, C...>`
	read packs: { AstIdentifier },
}

--- A generic declaration with optional default types; the
--- `<A = number, B... = (number, ...string)>` in
--- `type T<A = number, B... = (number, ...string)> = {}`.
---
--- This is used in type declarations.
---
--- In addition to the rules of `AstGenericDeclaration`, this node also requires
--- that any declarations after a declaration with a default type also have a
--- default type. For example, `<A = number, B>` is invalid because `B` doesn't
--- have a default type, but comes after a type with a default type.
export type AstGenericDeclarationWithDefaults = {
	--- The span of the node.
	read span: Span,

	--- The generic types; the `A = number,` in
	--- `<A = number, B... = (number, ...string)>`.
	read names: { {
		--- The span of the node.
		read span: Span,

		--- The name; the `A` in `A = number`.
		read name: AstIdentifier,

		--- The optional default type; the `number` in `A = number`.
		read default: AstType?,
	} },

	--- The generic type packs; the `B... = (number, ...string)` in
	--- `<A = number, B... = (number, ...string)>`.
	read packs: { {
		--- The span of the node.
		read span: Span,

		--- The name; the `B` in `B... = (number, ...string)`.
		read name: AstIdentifier,

		--- The optional default type; the `(number, ...string)` in
		--- `B... = (number, ...string)`.
		read default: AstTypePack?,
	} },
}

--- An argument to a type, such as `foo` or `...bar` or `baz...`.
---
--- This is used in reference types for arguments to types. As an example:
--- `foo<bar, baz...>` would have two `AstTypeArg`s: `bar` and `baz...`.
export type AstTypeArg = AstType | AstTypePack

--- A table type field with a name key, such as `read foo: bar` or
--- `bar: number`.
export type AstTableTypeField_NameProp = {
	read kind: "nameprop",

	--- The span of the node.
	read span: Span,

	--- The optional access modifier; the `read` in `read foo: bar`.
	---
	--- This identifier should only have values of `read` or `write`.
	read access: AstIdentifier?,

	--- The name; the `foo` in `read foo: bar`.
	read name: AstIdentifier,

	--- The type; the `bar` in `read foo: bar`.
	read type: AstType,
}

--- A table type field with a string key, such as `read ["foo"]: bar` or
--- `["bar"]: number`.
export type AstTableTypeField_StringProp = {
	read kind: "stringprop",

	--- The span of the node.
	read span: Span,

	--- The access modifier; the `read` in `read ["foo"]: bar`.
	---
	--- This identifier should only have values of `read` or `write`.
	read access: AstIdentifier?,

	--- The string key; the `"foo"` in `read ["foo"]: bar`.
	read key: AstString,

	--- The type; the `bar` in `read ["foo"]: bar`.
	read type: AstType,
}

--- A table type indexer field, such as `read [number]: string` or
--- `[string]: number`.
export type AstTableTypeField_Indexer = {
	read kind: "indexer",

	--- The span of the node.
	read span: Span,

	--- The access modifier; the `read` in `read [number]: string`.
	---
	--- This identifier should only have values of `read` or `write`.
	read access: AstIdentifier?,

	--- The key; the `number` in `read [number]: string`.
	read key: AstType,

	--- The type; the `string` in `read [number]: string`.
	read type: AstType,
}

--- A table type field, such as `read foo: bar`, `["foo"]: bar`, or
--- `write [number]: string`.
export type AstTableTypeField =
	| AstTableTypeField_NameProp
	| AstTableTypeField_StringProp
	| AstTableTypeField_Indexer

--- A tail type in a type pack, such as `...foo` or `bar...`.
export type AstTailType = AstTypePack_Variadic | AstTypePack_Generic

--- The literal type `nil`.
export type AstType_Nil = {
	read kind: "nil",

	--- The span of the node.
	read span: Span,
}

--- The literal types `true` and `false`.
export type AstType_Boolean = {
	read kind: "boolean",

	--- The span of the node.
	read span: Span,

	--- The value of the boolean.
	read value: boolean,
}

--- A string literal type; `"foo"` or `'bar'`.
export type AstType_String = {
	read kind: "string",

	--- The string literal.
	read value: AstString,
}

--- A reference type, such as `foo` or `foo<bar>` or `foo.bar<baz, quz...>`.
export type AstType_Reference = {
	read kind: "reference",

	--- The span of the node.
	read span: Span,

	--- The optional prefix; the `foo` in `foo.bar<baz, quz...>`.
	read prefix: AstIdentifier?,

	--- The name; the `bar` in `foo.bar<baz, quz...>`.
	read name: AstIdentifier,

	--- The generics; the `<baz, quz...>` in `foo.bar<baz, quz...>`.
	read generics: { AstTypeArg },
}

--- A typeof type, such as `typeof(foo)` or `typeof(10)`.
export type AstType_Typeof = {
	read kind: "typeof",

	--- The span of the node.
	read span: Span,

	--- The expr; the `foo` in `typeof(foo)`.
	read expr: AstExpr,
}

--- An array type, such as `{ foo }` or `{ number }`.
export type AstType_Array = {
	read kind: "array",

	--- The span of the node.
	read span: Span,

	--- The type; the `number` in `{ number }`.
	read type: AstType,
}

--- A table type, such as `{ foo: bar, ["baz"]: number, [number]: string }`.
export type AstType_Table = {
	read kind: "table",

	--- The span of the node.
	read span: Span,

	--- The fields; the `foo: bar` in `{ foo: bar }`.
	read fields: { AstTableTypeField },
}

--- A function type, such as `<T>(foo: T, ...bar) -> baz`.
export type AstType_Function = {
	read kind: "function",

	--- The span of the node.
	read span: Span,

	--- The generics; the `<T>` in `<T>(foo: T, ...bar) -> baz`.
	read generics: AstGenericDeclaration?,

	--- The parameters; the `foo: T` in `<T>(foo: T, ...bar) -> baz`.
	read params: { {
		--- The span of the node.
		read span: Span,

		--- The optional name of the parameter; the `foo` in `foo: T`.
		read name: AstIdentifier?,

		--- The type of the parameter; the `T` in `foo: T`.
		read type: AstType,
	} },

	--- The optional varargs; the `...bar` in `<T>(foo: T, ...bar) -> baz`.
	read varargs: AstTailType?,

	--- The return type; the `baz` in `<T>(foo: T, ...bar) -> baz`.
	read ret: AstReturnType?,
}

--- A parenthesized type, such as `(foo)` or `(bar)`.
export type AstType_Paren = {
	read kind: "paren",

	--- The span of the node.
	read span: Span,

	--- The type; the `foo` in `(foo)`.
	read type: AstType,
}

--- An optional type, such as `foo?` or `bar?`.
export type AstType_Optional = {
	read kind: "optional",

	--- The span of the node.
	read span: Span,

	--- The type; the `foo` in `foo?`.
	read type: AstType,
}

--- A union type, such as `| foo | bar` or `foo | bar`.
export type AstType_Union = {
	read kind: "union",

	--- The span of the node.
	read span: Span,

	--- The types; the `foo` and `bar` in `foo | bar`.
	read types: { AstType },
}

--- An intersection type, such as `& foo & bar` or `foo & bar`.
export type AstType_Intersection = {
	read kind: "intersection",

	--- The span of the node.
	read span: Span,

	--- The types; the `foo` and `bar` in `foo & bar`.
	read types: { AstType },
}

--- A type, such as `number` or `foo` or `{}`.
---
--- This node is used in many places, such as variable declarations, function
--- parameters, and type declarations themselves.
export type AstType =
	| AstType_Nil
	| AstType_Boolean
	| AstType_String
	| AstType_Reference
	| AstType_Typeof
	| AstType_Array
	| AstType_Table
	| AstType_Function
	| AstType_Paren
	| AstType_Optional
	| AstType_Union
	| AstType_Intersection

--- A variadic type pack, such as `...foo` or `...number`.
export type AstTypePack_Variadic = {
	read kind: "variadic",

	--- The span of the node.
	read span: Span,

	--- The type; the `foo` in `...foo`.
	read type: AstType,
}

--- A generic type pack, such as `foo...` or `T...`.
export type AstTypePack_Generic = {
	read kind: "generic",

	--- The span of the node.
	read span: Span,

	--- The name; the `foo` in `foo...`.
	read name: AstIdentifier,
}

--- A list type pack, such as `(foo, bar, baz...)` or `(number...)`.
export type AstTypePack_List = {
	read kind: "list",

	--- The span of the node.
	read span: Span,

	--- The types; the `foo`, `bar`, and `baz` in `(foo, bar, baz...)`.
	read types: { AstType },

	--- The optional tail type pack; the `baz...` in `(foo, bar, baz...)`.
	read tail: AstTailType?,
}

--- A type pack, such as `...foo` or `foo...` or `(foo, bar, baz...)`.
export type AstTypePack =
	| AstTypePack_Variadic
	| AstTypePack_Generic
	| AstTypePack_List

--- A return type, such as `(foo, bar, baz...)` or `(foo | bar, baz)` or `foo`.
export type AstReturnType = AstType | AstTypePack

--- A binding, such as `foo: bar` or `foo`.
---
--- This is used in a few places, such as function parameters and local variable
--- declaration.
export type AstBinding = {
	--- The span of the node.
	read span: Span,

	--- The name; the `foo` in `foo: bar`.
	read name: AstIdentifier,

	--- The optional type; the `bar` in `foo: bar`.
	read type: AstType?,
}

--- An interpolated string with no exprs, such as `'simple'`.
export type AstIString_Simple = {
	read kind: "simple",

	--- The span of the node.
	read span: Span,

	--- The string value of the literal, including the quotes.
	read value: string,
}

--- An interpolated string with exprs, such as `'foo {1} bar {2} baz'` or
--- `'hello {"world"}'`.
export type AstIString_Complex = {
	read kind: "complex",

	--- The span of the node.
	read span: Span,

	--- The first segment; the `'foo {'` in `'foo {1} bar {2} baz'`.
	---
	--- This includes the leading quote and trailing brace
	read first: string,

	--- The middle segments; the `1} bar {2` in `'foo {1} bar {2} baz'`.
	read middle: { {
		--- The span of the node.
		read span: Span,

		--- The expr; the `1` or `2` in `1} bar {2`.
		read expr: AstExpr,

		--- The string after the expr; the `} bar {` in `1} bar {2`.
		---
		--- This includes the leading and trailing braces. For the last segment,
		--- this will be nil.
		read after: string?,
	} },

	--- The last segment; the `} baz'` in `'foo {1} bar {2} baz'`.
	---
	--- This includes the leading brace and trailing quote.
	read last: string,
}

--- An interpolated string, such as `'simple'` or `'foo {1} bar {2} baz'` or
--- `'hello {"world"}'`.
export type AstIString = AstIString_Simple | AstIString_Complex

--- A branch of an if-else expr, such as `foo then bar`.
export type AstIfElseExprBranch = {
	--- The span of the node.
	read span: Span,

	--- The condition; the `foo` in `foo then bar`.
	read condition: AstExpr,

	--- The body; the `bar` in `foo then bar`.
	read body: AstExpr,
}

--- An unary operator.
export type AstUnaryOperator =
	| "+"
	| "-"
	| "not"
	| "#"

--- A binary operator.
export type AstBinaryOperator =
	| "+"
	| "-"
	| "*"
	| "/"
	| "//"
	| "%"
	| "^"
	| ".."
	| "<"
	| "<="
	| ">"
	| ">="
	| "=="
	| "~="
	| "and"
	| "or"

--- The literal expr `nil`.
export type AstExpr_Nil = {
	read kind: "nil",

	--- The span of the node.
	read span: Span,
}

--- The literal exprs `true` and `false`.
export type AstExpr_Boolean = {
	read kind: "boolean",

	--- The span of the node.
	read span: Span,

	--- The value of the boolean.
	read value: boolean,
}

--- A number literal expr, such as `123` or `0xABCD`.
export type AstExpr_Number = {
	read kind: "number",

	--- The number literal.
	read value: AstNumber,
}

--- A string literal expr, such as `"foo"` or `'bar'`.
export type AstExpr_String = {
	read kind: "string",

	--- The string literal.
	read value: AstString,
}

--- The literal expr `...`.
export type AstExpr_Varargs = {
	read kind: "varargs",

	--- The span of the node.
	read span: Span,
}

--- An interpolated string expr, such as `'simple'` or
--- `foo {1} bar {2} baz` or `hello {"world"}`.
export type AstExpr_IString = {
	read kind: "istring",

	--- The interpolated string.
	read value: AstIString,
}

--- A table literal expr, such as `{ foo = bar }` or `{ 1, 2, 3 }`.
export type AstExpr_Table = {
	read kind: "table",

	--- The table literal.
	read table: AstTable,
}

--- An anonymous function expr, such as
--- `@native function(foo, bar) return foo + bar end` or `function() end`.
export type AstExpr_Function = {
	read kind: "function",

	--- The span of the node.
	read span: Span,

	--- The function attributes; the `@native` in
	--- `@native function(foo, bar) return foo + bar end`.
	read attributes: { AstAttribute },

	--- The function body; the `(foo, bar) return foo + bar end` in
	--- `@native function(foo, bar) return foo + bar end`.
	read body: AstFunctionBody,
}

--- An if-else expr, such as `if foo then bar elseif baz then qux else quux`.
export type AstExpr_IfElse = {
	read kind: "ifelse",

	--- The span of the node.
	read span: Span,

	--- The branches; the `foo then bar elseif baz then qux` in
	--- `if foo then bar elseif baz then qux else quux`.
	read branches: { AstIfElseExprBranch },

	--- The else body; the `quux` in
	--- `if foo then bar elseif baz then qux else quux`.
	read else_body: AstExpr,
}

--- A var expr, such as `foo.bar[baz]()` or `foo`.
export type AstExpr_Var = {
	read kind: "var",

	--- The var.
	read var: AstVar,
}

--- A type assertion expr, such as `foo :: bar` or `{} :: { number }`.
export type AstExpr_Assertion = {
	read kind: "assertion",

	--- The span of the node.
	read span: Span,

	--- The expr; the `foo` in `foo :: bar`.
	read expr: AstExpr,

	--- The type; the `bar` in `foo :: bar`.
	read type: AstType,
}

--- A unary expr, such as `-foo` or `not bar`.
export type AstExpr_Unary = {
	read kind: "unary",

	--- The span of the node.
	read span: Span,

	--- The operator; the `-` in `-foo`.
	read operator: AstUnaryOperator,

	--- The expr; the `foo` in `-foo`.
	read expr: AstExpr,
}

--- A binary expr, such as `foo + bar` or `baz and qux`.
export type AstExpr_Binary = {
	read kind: "binary",

	--- The span of the node.
	read span: Span,

	--- The left expr; the `foo` in `foo + bar`.
	read left: AstExpr,

	--- The operator; the `+` in `foo + bar`.
	read operator: AstBinaryOperator,

	--- The right expr; the `bar` in `foo + bar`.
	read right: AstExpr,
}

--- An expression, such as `nil` or `foo` or `foo + bar`.
---
--- This node is used in many places, such as function arguments, table fields,
--- variable assignments, and return stats.
export type AstExpr =
	| AstExpr_Nil
	| AstExpr_Boolean
	| AstExpr_Number
	| AstExpr_String
	| AstExpr_Varargs
	| AstExpr_IString
	| AstExpr_Table
	| AstExpr_Function
	| AstExpr_IfElse
	| AstExpr_Var
	| AstExpr_Assertion
	| AstExpr_Unary
	| AstExpr_Binary

--- A pack argument to a call, such as `(foo, bar)` or `()`.
export type AstFunctionArg_Pack = {
	read kind: "pack",

	--- The span of the node.
	read span: Span,

	--- The exprs; the `foo, bar` in `(foo, bar)`.
	read exprs: { AstExpr },
}

--- A table argument to a call, such as `{ foo = bar }` or `{}`.
export type AstFunctionArg_Table = {
	read kind: "table",

	--- The table literal.
	read table: AstTable,
}

--- A string argument to a call, such as `"foo"` or `'bar'`.
export type AstFunctionArg_String = {
	read kind: "string",

	--- The string literal.
	read value: AstString,
}

--- An argument to a call, such as `(foo, bar)` or `()` or `"baz"`.
export type AstFunctionArg =
	| AstFunctionArg_Pack
	| AstFunctionArg_Table
	| AstFunctionArg_String

--- A name as a variable, such as `foo`.
export type AstVarRoot_Name = {
	read kind: "name",

	--- The name of the variable.
	read name: AstIdentifier,
}

--- A parenthesized expr, such as `(foo)` or `(1 + 2)`.
export type AstVarRoot_Paren = {
	read kind: "paren",

	--- The span of the node.
	read span: Span,

	--- The expr; the `foo` in `(foo)`.
	read expr: AstExpr,
}

--- The root of a variable, such as `foo` or `(foo)`.
export type AstVarRoot = AstVarRoot_Name | AstVarRoot_Paren

--- A named index, such as `root.foo` or `root.bar`.
export type AstVarSuffix_ExprIndex = {
	read kind: "nameindex",

	--- The span of the node.
	read span: Span,

	--- The name; the `foo` in `root.foo`.
	read name: AstIdentifier,
}

--- An expr index, such as `root[foo]` or `root[1 + 1]`.
export type AstVarSuffix_NameIndex = {
	read kind: "exprindex",

	--- The span of the node.
	read span: Span,

	--- The expr; the `foo` in `root[foo]`.
	read expr: AstExpr,
}

--- A call, such as `root:method(foo)` or `root()`.
export type AstVarSuffix_Call = {
	read kind: "call",

	--- The span of the node.
	read span: Span,

	--- The optional method; the `method` in `root:method(foo)`.
	read method: AstIdentifier?,

	--- The argument; the `(foo)` in `root:method(foo)`.
	read arg: AstFunctionArg,
}

--- A suffix of a variable, such as `.foo` or `[bar]` or `()`.
export type AstVarSuffix =
	| AstVarSuffix_NameIndex
	| AstVarSuffix_ExprIndex
	| AstVarSuffix_Call

--- A variable, such as `foo` or `foo.bar()[baz]`.
---
--- This node is used in exprs, assignments, and function calls. This is
--- essentially anything that can appear on the left side of an assignment stat
--- or before a function call.
export type AstVar = {
	--- The root of the variable; the `foo` in `foo.bar()[baz]`.
	read root: AstVarRoot,

	--- The suffixes of the variable; the `.bar()[baz]` in `foo.bar()[baz]`.
	read suffixes: { AstVarSuffix },
}

--- A table field with a name key, such as `foo = bar` or `bar = 1`.
export type AstTableField_NameKey = {
	read kind: "namekey",

	--- The span of the node.
	read span: Span,

	--- The name; the `foo` in `foo = bar`.
	read name: AstIdentifier,

	--- The value; the `bar` in `foo = bar`.
	read value: AstExpr,
}

--- A table field with an expr key, such as `[foo] = bar` or `[1] = 2`.
export type AstTableField_ExprKey = {
	read kind: "exprkey",

	--- The span of the node.
	read span: Span,

	--- The key; the `foo` in `[foo] = bar`.
	read key: AstExpr,

	--- The value; the `bar` in `[foo] = bar`.
	read value: AstExpr,
}

--- A table field with no key, such as `foo` or `1`.
export type AstTableField_NoKey = {
	read kind: "nokey",

	--- The value; the `foo` in `foo`.
	read value: AstExpr,
}

--- A table field, such as `foo = bar` or `[baz] = qux` or `quux`.
export type AstTableField =
	| AstTableField_NameKey
	| AstTableField_ExprKey
	| AstTableField_NoKey

--- A table literal, such as `{ foo = bar, [baz] = qux, quux }`.
export type AstTable = {
	--- The span of the node.
	read span: Span,

	--- The fields; the `foo = bar` in `{ foo = bar }`.
	read fields: { AstTableField },
}

--- A function body, such as `<T>(foo: T, ...: bar): baz return 10 end`.
---
--- This is used in anonymous functions, function declarations, and local
--- function declarations.
export type AstFunctionBody = {
	--- The span of the node.
	read span: Span,

	--- The optional generic declaration; the `<T>` in
	--- `<T>(foo: T, ...: bar): baz return 10 end`.
	read generics: AstGenericDeclaration?,

	--- The parameters; the `foo: T` in
	--- `<T>(foo: T, ...: bar): baz return 10 end`.
	read params: { AstBinding },

	--- The optional varargs; the `...: bar` in
	--- `<T>(foo: T, ...: bar): baz return 10 end`.
	read varargs: {
		--- The span of the node.
		read span: Span,

		--- The optional type; the `bar` in `...: bar`.
		read type: AstType?,
	}?,

	--- The return type; the `baz` in
	--- `<T>(foo: T, ...: bar): baz return 10 end`.
	read ret: AstReturnType?,

	--- The block; the `return 10` in
	--- `<T>(foo: T, ...: bar): baz return 10 end`.
	read block: AstBlock,
}

--- A branch in an if stat, such as `foo then bar()`.
export type AstIfStatBranch = {
	--- The span of the node.
	read span: Span,

	--- The condition; the `foo` in `foo then bar()`.
	read condition: AstExpr,

	--- The block; the `bar()` in `foo then bar()`.
	read block: AstBlock,
}

--- A compound operator.
export type AstCompoundOperator =
	| "+="
	| "-="
	| "*="
	| "/="
	| "//="
	| "%="
	| "^="
	| "..="

--- An assignment stat, such as `foo, bar = baz, qux` or `foo = bar`.
export type AstStat_Assign = {
	read kind: "assign",

	--- The left side of the assignment; the `foo, bar` in
	--- `foo, bar = baz, qux`.
	read left: { AstVar },

	--- The right side of the assignment; the `baz, qux` in
	--- `foo, bar = baz, qux`.
	read right: { AstExpr },
}

--- A compound assignment stat, such as `foo += bar`.
export type AstStat_CompoundAssign = {
	read kind: "compoundassign",

	--- The left side of the assignment; the `foo` in `foo += bar`.
	read left: AstVar,

	--- The operator; the `+=` in `foo += bar`.
	read operator: AstCompoundOperator,

	--- The right side of the assignment; the `bar` in `foo += bar`.
	read right: AstExpr,
}

--- A function call stat, such as `foo.bar()` or `baz()`.
export type AstStat_Call = {
	read kind: "call",

	--- A var ending with a call suffix.
	---
	--- This call not ending with a call suffix will result in a syntax error
	--- when emitted.
	read call: AstVar,
}

--- A do stat, such as `do foo() end`.
export type AstStat_Do = {
	read kind: "do",

	--- The span of the node.
	read span: Span,

	--- The block within the do; the `foo()` in `do foo() end`.
	read block: AstBlock,
}

--- A while stat, such as `while foo do bar() end`.
export type AstStat_While = {
	read kind: "while",

	--- The span of the node.
	read span: Span,

	--- The condition; the `foo` in `while foo do bar() end`.
	read condition: AstExpr,

	--- The block; the `bar()` in `while foo do bar() end`.
	read block: AstBlock,
}

--- A repeat stat, such as `repeat foo() until bar`.
export type AstStat_Repeat = {
	read kind: "repeat",

	--- The span of the node.
	read span: Span,

	--- The block; the `foo()` in `repeat foo() until bar`.
	read block: AstBlock,

	--- The condition; the `bar` in `repeat foo() until bar`.
	read condition: AstExpr,
}

--- An if stat, such as
--- `if foo then bar() elseif baz then qux() else quux() end`.
export type AstStat_If = {
	read kind: "if",

	--- The span of the node.
	read span: Span,

	--- The branches; the `foo then bar() elseif baz then qux()` in
	--- `if foo then bar() elseif baz then qux() else quux() end`.
	read branches: { AstIfStatBranch },

	--- The optional else block; the `quux()` in
	--- `if foo then bar() elseif baz then qux() else quux() end`.
	read else_block: AstBlock?,
}

--- A numeric for loop stat, such as `for foo = 1, 2, 3 do bar() end`.
export type AstStat_NumericFor = {
	read kind: "numericfor",

	--- The span of the node.
	read span: Span,

	--- The binding; the `foo` in `for foo = 1, 2, 3 do bar() end`.
	read binding: AstBinding,

	--- The start expr; the `1` in `for foo = 1, 2, 3 do bar() end`.
	read start_expr: AstExpr,

	--- The end expr; the `2` in `for foo = 1, 2, 3 do bar() end`.
	read end_expr: AstExpr,

	--- The optional step expr; the `3` in `for foo = 1, 2, 3 do bar() end`.
	read step_expr: AstExpr?,

	--- The block; the `bar()` in `for foo = 1, 2, 3 do bar() end`.
	read block: AstBlock,
}

--- A for-in loop stat, such as `for foo: bar, baz in qux, quux do quuz() end`.
export type AstStat_ForIn = {
	read kind: "forin",

	--- The span of the node.
	read span: Span,

	--- The bindings; the `foo: bar, baz` in
	--- `for foo: bar, baz in qux, quux do quuz() end`.
	read bindings: { AstBinding },

	--- The exprs; the `qux, quux` in
	--- `for foo: bar, baz in qux, quux do quuz() end`.
	read exprs: { AstExpr },

	--- The block; the `quuz()` in
	--- `for foo: bar, baz in qux, quux do quuz() end`.
	read block: AstBlock,
}

--- A function declaration stat, such as
--- `@native function foo.bar:baz() return 1 end`.
export type AstStat_Function = {
	read kind: "function",

	--- The span of the node.
	read span: Span,

	--- The function attributes; the `@native` in
	--- `@native function foo.bar:baz() return 1 end`.
	read attributes: { AstAttribute },

	--- The name; the `foo.bar` in
	--- `@native function foo.bar:baz() return 1 end`.
	read names: { AstIdentifier },

	--- The optional method; the `baz` in
	--- `@native function foo.bar:baz() return 1 end`.
	read method: AstIdentifier?,

	--- The function body; the `() return 1 end` in
	--- `@native function foo.bar:baz() return 1 end`.
	read body: AstFunctionBody,
}

--- A local function declaration stat, such as
--- `@native local function foo() return 1 end`.
export type AstStat_LocalFunction = {
	read kind: "localfunction",

	--- The span of the node.
	read span: Span,

	--- The function attributes; the `@native` in
	--- `@native local function foo() return 1 end`.
	read attributes: { AstAttribute },

	--- The name; the `foo` in
	--- `@native local function foo() return 1 end`.
	read name: AstIdentifier,

	--- The function body; the `() return 1 end` in
	--- `@native local function foo() return 1 end`.
	read body: AstFunctionBody,
}

--- A local variable stat, such as `local foo: bar, baz = qux, quux, quuz`.
export type AstStat_LocalVariable = {
	read kind: "localvariable",

	--- The span of the node.
	read span: Span,

	--- The bindings; the `foo: bar, baz` in
	--- `local foo: bar, baz = qux, quux, quuz`.
	read bindings: { AstBinding },

	--- The exprs; the `qux, quux, quuz` in
	--- `local foo: bar, baz = qux, quux, quuz`.
	read exprs: { AstExpr },
}

--- A type declaration stat, such as `export type foo<bar> = baz`.
export type AstStat_Type = {
	read kind: "type",

	--- The span of the node.
	read span: Span,

	--- If the type is exported.
	read export: boolean,

	--- The name; the `foo` in `export type foo<bar> = baz`.
	read name: AstIdentifier,

	--- The optional generic declaration; the `<bar>` in `export type foo<bar> = baz`.
	read generics: AstGenericDeclarationWithDefaults?,

	--- The type; the `baz` in `export type foo<bar> = baz`.
	read type: AstType,
}

--- A statement in a block, such as `foo()` or `local foo = bar` or `foo = bar`.
export type AstStat =
	| AstStat_Assign
	| AstStat_CompoundAssign
	| AstStat_Call
	| AstStat_Do
	| AstStat_While
	| AstStat_Repeat
	| AstStat_If
	| AstStat_NumericFor
	| AstStat_ForIn
	| AstStat_Function
	| AstStat_LocalFunction
	| AstStat_LocalVariable
	| AstStat_Type

--- A return laststat, such as `return foo, bar` or `return`.
export type AstLastStat_Return = {
	read kind: "return",

	--- The span of the node.
	read span: Span,

	--- The optional exprs; the `foo, bar` in `return foo, bar`.
	read exprs: { AstExpr },
}

--- A `break` laststat.
export type AstLastStat_Break = {
	read kind: "break",

	--- The span of the node.
	read span: Span,
}

--- A `continue` laststat.
export type AstLastStat_Continue = {
	read kind: "continue",

	--- The span of the node.
	read span: Span,
}

--- The last statement in a block, such as `return`, `break`, or `continue`.
export type AstLastStat =
	| AstLastStat_Return
	| AstLastStat_Break
	| AstLastStat_Continue

--- A block of stats, ending with an optional laststat, such as
--- `foo(); bar(); return baz;`.
export type AstBlock = {
	--- The span of the node.
	read span: Span,

	--- The stats; the `foo(); bar();` in `foo(); bar(); return baz;`.
	read stats: { AstStat },

	--- The optional laststat; the `return baz;` in
	--- `foo(); bar(); return baz;`.
	read last: AstLastStat?,
}

--- A full AST; a block ending with an eof.
export type Ast = {
	--- The block; the `foo(); bar(); return baz;` in
	--- `foo(); bar(); return baz;`.
	read block: AstBlock,

	--- This field is always `true` and exists to ensure that the AST is a
	--- unique type.
	read eof: true,
}

return {}


local process = require("@lune/process")
local stdio = require("@lune/stdio")
local task = require("@lune/task")

local processes = {}

local COMMANDS = {
    "npm run watch",
    "rojo sourcemap default.project.json -o sourcemap.json --watch",
    "rojo serve",
}

for _, command in COMMANDS do
    local args = string.split(command, " ")
    local program = table.remove(args, 1) :: string

    table.insert(processes, process.create(program, args, {
        shell = true,
    }))
end

local function stdout(childProcess: process.ChildProcess)
    while task.wait() do
        local out = childProcess.stdout:read()
        if not out then
            continue
        end

        stdio.ewrite(out)
	end
end

local function stderr(childProcess: process.ChildProcess)
	while task.wait() do
        local out = childProcess.stderr:read()
        if not out then
            continue
        end

        stdio.ewrite(out)
	end
end

for _, childProcess in processes do
    task.spawn(stdout, childProcess)
    task.spawn(stderr, childProcess)
end


return {
	testMatch = {
		"**/__tests__/*.(spec|test)",
	},
	testPathIgnorePatterns = {
		"_Index",
		"DevPackages",
	},
}


local AdjacencyMatrix = {}
AdjacencyMatrix.__index = AdjacencyMatrix

export type AdjacencyMatrix = {
	matrix: { { number } },
	length: number,
	width: number,

	extend: (self: AdjacencyMatrix) -> (),
	setEdge: (self: AdjacencyMatrix, i: number, j: number, v: number) -> (),
	toAdjacencyList: (self: AdjacencyMatrix) -> { { number } },
	topologicalSort: (self: AdjacencyMatrix) -> { number }?,
	new: () -> AdjacencyMatrix,
}

function AdjacencyMatrix:__tostring()
	local s = "\n"

	for i = 1, self.length do
		if i == 1 then
			s ..= "\n"
		end

		local sub = ""
		for j = 1, self.width do
			if j == self.width then
				sub ..= `{self.matrix[i][j]}`
				continue
			end

			sub ..= `{self.matrix[i][j]}, `
		end

		sub ..= "\n"
		s ..= sub
	end

	return s
end

function AdjacencyMatrix:extend()
	self.length = (self.length :: number) + 1
	self.width = (self.length :: number) + 1

	self.matrix[self.length] = {}
	for j = 1, self.width do
		self.matrix[self.length][j] = 0
	end

	for i = 1, self.length do
		self.matrix[i][self.width] = 0
	end
end

function AdjacencyMatrix:setEdge(i, j, val)
	self.matrix[i][j] = val
end

function AdjacencyMatrix:toAdjacencyList()
	local list = {}

	for i = 1, self.length do
		list[i] = {}
		for j = 1, self.width do
			if self.matrix[i][j] ~= 0 then
				table.insert(list[i], j)
			end
		end
	end

	return list
end

function AdjacencyMatrix:topologicalSort(): { number }?
	local adjacencyList = self:toAdjacencyList()

	local result = {}
	local inDegrees = table.create(self.length, 0)

	for i = 1, self.length do
		for _, j in adjacencyList[i] do
			inDegrees[j] += 1
		end
	end

	local queue = {}
	for i = 1, self.length do
		if inDegrees[i] == 0 then
			table.insert(queue, i)
		end
	end

	while #queue ~= 0 do
		local i = table.remove(queue, 1) :: number
		table.insert(result, i)

		for _, j in adjacencyList[i] do
			inDegrees[j] -= 1
			if inDegrees[j] == 0 then
				table.insert(queue, j)
			end
		end
	end

	if #result ~= self.length then
		return nil
	end

	return result
end

function AdjacencyMatrix.new(): AdjacencyMatrix
	return setmetatable({
		matrix = {},
		length = 0,
		width = 0,
	}, AdjacencyMatrix) :: any
end

export type DependencyGraph<T> = {
	nodes: { T },
	matrix: AdjacencyMatrix,
	length: number,
	width: number,

	getOrderedList: (self: DependencyGraph<T>) -> { T }?,
	insert: (self: DependencyGraph<T>, node: T) -> DependencyGraph<T>,
	insertAfter: (
		self: DependencyGraph<T>,
		node: T,
		afterNode: T
	) -> DependencyGraph<T>,
	insertBefore: (
		self: DependencyGraph<T>,
		node: T,
		beforeNode: T
	) -> DependencyGraph<T>,
	new: () -> DependencyGraph<T>,
}

local DependencyGraph = {}
DependencyGraph.__index = DependencyGraph

function DependencyGraph:getOrderedList(): { any }?
	local orderedList = {}

	local topologicalSort = self.matrix:topologicalSort()
	if not topologicalSort then
		return nil
	end

	for _, i in topologicalSort do
		table.insert(orderedList, self.nodes[i])
	end

	return orderedList
end

function DependencyGraph:insertBefore(node, beforeNode)
	if not table.find(self.nodes, beforeNode) then
		error("Node not found in DependencyGraph:insertBefore(_, unknown)")
	end

	local j = table.find(self.nodes, node)
	if not j then
		table.insert(self.nodes, node)
		j = #self.nodes
	end

	local i = table.find(self.nodes, beforeNode)

	self.matrix:extend()
	self.matrix:setEdge(j, i, 1)

	return self
end

function DependencyGraph:insertAfter(node, afterNode)
	if not table.find(self.nodes, afterNode) then
		error("Node not found in DependencyGraph:insertAfter(_, unknown)")
	end

	local j = table.find(self.nodes, node)
	if not j then
		table.insert(self.nodes, node)
		j = #self.nodes
	end

	local i = table.find(self.nodes, afterNode)

	self.matrix:extend()
	self.matrix:setEdge(i, j, 1)

	return self
end

function DependencyGraph:insert(node)
	local i = #self.nodes
	table.insert(self.nodes, node)
	local j = #self.nodes

	self.matrix:extend()

	if i ~= 0 then
		self.matrix:setEdge(i, j, 1)
	end

	return self
end

function DependencyGraph.new<T>(): DependencyGraph<T>
	return setmetatable({
		nodes = {},
		matrix = AdjacencyMatrix.new(),
		length = 0,
		width = 0,
	}, DependencyGraph) :: any
end

return DependencyGraph


--- @class Phase
---
--- Phases represent tags that tell the scheduler when to
--- schedule a set of systems.
local Phase = {}
Phase.__index = Phase

function Phase:__tostring()
	return self._name
end

--- @within Phase
---
--- Creates a new Phase, with an optional name to use for debugging.
--- When no name is provided, the script and line number will be used.
function Phase.new(name: string?)
	name = name or debug.info(2, "sl")
	return setmetatable({
		_name = name,
		_type = "phase",
	}, Phase)
end

--- @prop PreStartup Phase
--- @within Phase
--- Runs before the `Startup` Phase.

--- @prop Startup Phase
--- @within Phase
--- This Phase will run once, the first time the Scheduler is ran,
--- before any other Phases are ran.

--- @prop PostStartup Phase
--- @within Phase
--- Runs after the `Startup` phase.

Phase.PreStartup = Phase.new("PreStartup")
Phase.Startup = Phase.new("Startup")
Phase.PostStartup = Phase.new("PostStartup")

return Phase


--!nonstrict
local DependencyGraph = require(script.Parent.DependencyGraph)
local Phase = require(script.Parent.Phase)

--- @class Pipeline
---
--- Pipelines represent a set of ordered Phases. Systems cannot be
--- assigned to Pipelines themselves, but rather to Phases within
--- those Pipelines.
local Pipeline = {}
Pipeline.__index = Pipeline

function Pipeline:__tostring()
	return self._name
end

--- @method insert
--- @within Pipeline
--- @param phase Phase
--- @return Pipeline
---
--- Adds a Phase to the Pipeline, ordering it implicitly.
function Pipeline:insert(phase)
	self.dependencyGraph:insert(phase)
	return self
end

--- @method insertAfter
--- @within Pipeline
--- @param phase Phase
--- @param after Phase
--- @return Pipeline
---
--- Adds a Phase to the Pipeline after another Phase, ordering it explicitly.
function Pipeline:insertAfter(phase, afterPhase)
	local i = table.find(self.dependencyGraph.nodes, afterPhase)
	assert(
		i,
		"Unknown Phase in Pipeline:insertAfter(_, unknown), try adding this Phase to the Pipeline."
	)

	self.dependencyGraph:insertAfter(phase, afterPhase)
	return self
end

--- @method insertBefore
--- @within Pipeline
--- @param phase Phase
--- @param before Phase
--- @return Pipeline
---
--- Adds a Phase to the Pipeline before another Phase, ordering it explicitly.
function Pipeline:insertBefore(phase, beforePhase)
	local i = table.find(self.dependencyGraph.nodes, beforePhase)
	assert(
		i,
		"Unknown Phase in Pipeline:insertBefore(_, unknown), try adding this Phase to the Pipeline."
	)

	self.dependencyGraph:insertBefore(phase, beforePhase)
	return self
end

--- @within Pipeline
---
--- Creates a new Pipeline, with an optional name to use for debugging.
--- When no name is provided, the script and line number will be used.
function Pipeline.new(name: string?)
	name = name or debug.info(2, "sl")
	return setmetatable({
		_name = name,
		_type = "pipeline",
		dependencyGraph = DependencyGraph.new(),
	}, Pipeline)
end

--- @prop Startup Pipeline
--- @within Pipeline
---
--- A Pipeline containing the `PreStartup`, `Startup`, and `PostStartup` phases.
Pipeline.Startup = Pipeline.new()
	:insert(Phase.PreStartup)
	:insert(Phase.Startup)
	:insert(Phase.PostStartup)

return Pipeline


--!nonstrict
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local root = script.Parent.Parent

local Phase = require(root.Phase)
local Scheduler = require(root.Scheduler)

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local test = JestGlobals.test

describe("Initializer Systems", function()
	describe("Basic Initialization", function()
		test("standard system", function()
			expect.assertions(3)

			local runCount = 0

			local function standardSystem()
				runCount += 1
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(standardSystem, myPhase)

			myScheduler:runPhase(myPhase)
			expect(runCount).toBe(1)
			expect(myScheduler._systemInfo[standardSystem].initialized).toBe(
				true
			)

			myScheduler:runPhase(myPhase)
			expect(runCount).toBe(2)
		end)

		test("initializer system", function()
			expect.assertions(7)

			local initCount = 0
			local runCount = 0

			local function initializerSystem()
				initCount += 1

				return function()
					runCount += 1
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(initializerSystem, myPhase)

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(1)
			expect(runCount).toBe(1)
			expect(myScheduler._systemInfo[initializerSystem].initialized).toBe(
				true
			)

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(1)
			expect(runCount).toBe(2)

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(1)
			expect(runCount).toBe(3)
		end)

		test("initializer with cleanup", function()
			expect.assertions(5)

			local initCount = 0
			local runCount = 0
			local cleanupCount = 0

			local function initializerWithCleanup()
				initCount += 1

				return function()
					runCount += 1
				end, function()
					cleanupCount += 1
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler
				:insert(myPhase)
				:addSystem(initializerWithCleanup, myPhase)

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(1)
			expect(runCount).toBe(1)
			expect(myScheduler._systemInfo[initializerWithCleanup].cleanup).toBeTruthy()

			myScheduler:runPhase(myPhase)
			expect(runCount).toBe(2)

			myScheduler:removeSystem(initializerWithCleanup)
			expect(cleanupCount).toBe(1)
		end)

		test("init once", function()
			expect.assertions(2)

			local initCount = 0
			local runCount = 0

			local function initOnceSystem()
				initCount += 1
				return function()
					runCount += 1
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(initOnceSystem, myPhase)

			myScheduler:runPhase(myPhase)
			myScheduler:runPhase(myPhase)
			myScheduler:runPhase(myPhase)
			myScheduler:runPhase(myPhase)

			expect(initCount).toBe(1)
			expect(runCount).toBe(4)
		end)
	end)

	describe("Error Cases", function()
		test("invalid return type", function()
			expect.assertions(4)

			local attemptCount = 0

			local function brokenSystem()
				attemptCount += 1
				return "invalid"
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(brokenSystem, myPhase)

			myScheduler:runPhase(myPhase)
			expect(myScheduler._systemInfo[brokenSystem].initialized).toBe(true)
			expect(attemptCount).toBe(1)

			myScheduler:runPhase(myPhase)
			expect(myScheduler._systemInfo[brokenSystem].initialized).toBe(true)
			expect(attemptCount).toBe(2)
		end)

		test("init with run condition", function()
			expect.assertions(5)

			local initCount = 0
			local runCount = 0
			local allowRun = false

			local function conditionalInit()
				initCount += 1
				return function()
					runCount += 1
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler
				:insert(myPhase)
				:addSystem(conditionalInit, myPhase)
				:addRunCondition(conditionalInit, function()
					return allowRun
				end)

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(0)
			expect(runCount).toBe(0)

			allowRun = true

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(1)
			expect(runCount).toBe(1)

			myScheduler:runPhase(myPhase)
			expect(runCount).toBe(2)
		end)

		test("nested initializer should not run inner", function()
			expect.assertions(3)

			local initCount = 0
			local runtimeReturnCount = 0
			local innerRunCount = 0

			local function nestedInit()
				initCount += 1
				return function()
					runtimeReturnCount += 1
					return function()
						innerRunCount += 1
					end
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(nestedInit, myPhase)

			myScheduler:runPhase(myPhase)
			myScheduler:runPhase(myPhase)
			myScheduler:runPhase(myPhase)

			expect(initCount).toBe(1)
			expect(runtimeReturnCount).toBe(3)
			expect(innerRunCount).toBe(0)
		end)

		test("nil with second value", function()
			expect.assertions(1)

			local function invalidNilSystem()
				return nil, "oops"
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(invalidNilSystem, myPhase)

			myScheduler:runPhase(myPhase)
			expect(myScheduler._systemInfo[invalidNilSystem].initialized).toBe(
				true
			)
		end)

		test("same system in multiple phases", function()
			expect.assertions(2)

			local initCount = 0

			local function sharedSystem()
				initCount += 1
				return function() end
			end

			local myScheduler = Scheduler.new()
			local phase1 = Phase.new("Phase1")
			local phase2 = Phase.new("Phase2")

			myScheduler
				:insert(phase1)
				:insert(phase2)
				:addSystem(sharedSystem, phase1)
				:addSystem(sharedSystem, phase2)

			myScheduler:runPhase(phase1)
			expect(initCount).toBe(1)

			myScheduler:runPhase(phase2)
			expect(initCount).toBe(1)
		end)

		test("table with 2 functions", function()
			expect.assertions(4)

			local initCount = 0
			local runCount = 0
			local cleanupCount = 0

			local function tableStyleInit()
				initCount += 1
				return {
					system = function()
						runCount += 1
					end,
					cleanup = function()
						cleanupCount += 1
					end,
				}
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(tableStyleInit, myPhase)

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(1)
			expect(runCount).toBe(1)

			myScheduler:runPhase(myPhase)
			expect(runCount).toBe(2)

			myScheduler:removeSystem(tableStyleInit)
			expect(cleanupCount).toBe(1)
		end)

		test("table with 1 function", function()
			expect.assertions(3)

			local initCount = 0
			local runCount = 0

			local function singleTableInit()
				initCount += 1
				return {
					system = function()
						runCount += 1
					end,
				}
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(singleTableInit, myPhase)

			myScheduler:runPhase(myPhase)
			expect(initCount).toBe(1)
			expect(runCount).toBe(1)

			myScheduler:runPhase(myPhase)
			expect(runCount).toBe(2)
		end)

		test("table with non-functions", function()
			expect.assertions(1)

			local function invalidTableSystem()
				return { system = "not a function", cleanup = 123 }
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(invalidTableSystem, myPhase)

			myScheduler:runPhase(myPhase)
			expect(myScheduler._systemInfo[invalidTableSystem].initialized).toBe(
				true
			)
		end)

		test("table with cleanup only (startup system)", function()
			expect.assertions(5)

			local startupRunCount = 0
			local cleanupRan = false

			local function startupSystem()
				startupRunCount += 1
				-- Returns cleanup only - no runtime system
				return {
					cleanup = function()
						cleanupRan = true
					end,
				}
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(startupSystem, myPhase)

			-- First run: startup logic executes
			myScheduler:runPhase(myPhase)
			expect(startupRunCount).toBe(1)
			expect(myScheduler._systemInfo[startupSystem].initialized).toBe(
				true
			)

			-- Second run: original function runs again (startup-only pattern)
			myScheduler:runPhase(myPhase)
			expect(startupRunCount).toBe(2)

			-- Cleanup on removal
			expect(cleanupRan).toBe(false)
			myScheduler:removeSystem(startupSystem)
			expect(cleanupRan).toBe(true)
		end)

		test("table with neither system nor cleanup", function()
			expect.assertions(1)

			local function invalidSystem()
				return { foo = "bar" } -- No system or cleanup fields
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(invalidSystem, myPhase)

			myScheduler:runPhase(myPhase)
			expect(myScheduler._systemInfo[invalidSystem].initialized).toBe(
				true
			)
		end)
	end)

	describe("Cleanup", function()
		test("cleanup once", function()
			expect.assertions(1)

			local cleanupCount = 0

			local function systemWithCleanup()
				return function() end, function()
					cleanupCount += 1
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(systemWithCleanup, myPhase)

			myScheduler:runPhase(myPhase)
			myScheduler:removeSystem(systemWithCleanup)

			expect(cleanupCount).toBe(1)
		end)

		test("no cleanup if uninitialized", function()
			expect.assertions(1)

			local cleanupCount = 0
			local allowRun = false

			local function conditionalSystem()
				return function() end, function()
					cleanupCount += 1
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler
				:insert(myPhase)
				:addSystem(conditionalSystem, myPhase)
				:addRunCondition(conditionalSystem, function()
					return allowRun
				end)

			myScheduler:runPhase(myPhase)

			myScheduler:removeSystem(conditionalSystem)
			expect(cleanupCount).toBe(0)
		end)

		test("cleanup failure", function()
			expect.assertions(2)

			local cleanupRan = false

			local function brokenCleanupSystem()
				return function() end, function()
					cleanupRan = true
					error("Cleanup failed!")
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(brokenCleanupSystem, myPhase)

			myScheduler:runPhase(myPhase)

			myScheduler:removeSystem(brokenCleanupSystem)

			expect(cleanupRan).toBe(true)
			expect(myScheduler._systemInfo[brokenCleanupSystem]).toBeNil()
		end)

		test("cleanup on replaceSystem", function()
			expect.assertions(3)

			local cleanupCount = 0
			local newSystemRan = false

			local function oldSystem()
				return function() end, function()
					cleanupCount += 1
				end
			end

			local function newSystem()
				newSystemRan = true
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(oldSystem, myPhase)

			myScheduler:runPhase(myPhase)
			expect(cleanupCount).toBe(0)

			myScheduler:replaceSystem(oldSystem, newSystem)
			expect(cleanupCount).toBe(1)

			myScheduler:runPhase(myPhase)
			expect(newSystemRan).toBe(true)
		end)

		test("cleanup receives custom args on removeSystem", function()
			expect.assertions(1)

			local capturedArgs = nil

			local function systemWithArgsInCleanup()
				return function() end, function(...)
					capturedArgs = { ... }
				end
			end

			local myScheduler = Scheduler.new(1, 2, 3)
			local myPhase = Phase.new("myPhase")

			myScheduler
				:insert(myPhase)
				:addSystem(systemWithArgsInCleanup, myPhase)

			myScheduler:runPhase(myPhase) -- Initialize
			myScheduler:removeSystem(systemWithArgsInCleanup)

			expect(capturedArgs).toEqual({ 1, 2, 3 })
		end)

		test("cleanup receives custom args on replaceSystem", function()
			expect.assertions(1)

			local capturedArgs = nil

			local function oldSystem()
				return function() end, function(...)
					capturedArgs = { ... }
				end
			end

			local function newSystem() end

			local myScheduler = Scheduler.new(1, 2, 3)
			local myPhase = Phase.new("myPhase")

			myScheduler:insert(myPhase):addSystem(oldSystem, myPhase)

			myScheduler:runPhase(myPhase) -- Initialize
			myScheduler:replaceSystem(oldSystem, newSystem)

			expect(capturedArgs).toEqual({ 1, 2, 3 })
		end)
	end)

	describe("Integration", function()
		test("with ordering", function()
			expect.assertions(6)

			local order = {}

			local function systemA()
				table.insert(order, "A-init")
				return function()
					table.insert(order, "A-run")
				end
			end

			local function systemB()
				table.insert(order, "B-init")
				return function()
					table.insert(order, "B-run")
				end
			end

			local myScheduler = Scheduler.new()
			local myPhase = Phase.new("myPhase")

			myScheduler
				:insert(myPhase)
				:addSystem(systemA, myPhase)
				:addSystem(systemB, myPhase)

			myScheduler:runPhase(myPhase)
			expect(order[1]).toBe("A-init")
			expect(order[2]).toBe("A-run")
			expect(order[3]).toBe("B-init")
			expect(order[4]).toBe("B-run")

			table.clear(order)

			myScheduler:runPhase(myPhase)
			expect(order[1]).toBe("A-run")
			expect(order[2]).toBe("B-run")
		end)

		test("with dependency graph", function()
			expect.assertions(6)

			local order = {}

			local function systemA()
				table.insert(order, "A-init")
				return function()
					table.insert(order, "A-run")
				end
			end

			local function systemB()
				table.insert(order, "B-init")
				return function()
					table.insert(order, "B-run")
				end
			end

			local myScheduler = Scheduler.new()
			local phaseA = Phase.new("PhaseA")
			local phaseB = Phase.new("PhaseB")

			myScheduler
				:insert(phaseA)
				:insertAfter(phaseB, phaseA)
				:addSystem(systemA, phaseA)
				:addSystem(systemB, phaseB)

			myScheduler:runPhase(phaseA)
			myScheduler:runPhase(phaseB)

			expect(order[1]).toBe("A-init")
			expect(order[2]).toBe("A-run")
			expect(order[3]).toBe("B-init")
			expect(order[4]).toBe("B-run")

			table.clear(order)

			myScheduler:runPhase(phaseA)
			myScheduler:runPhase(phaseB)

			expect(order[1]).toBe("A-run")
			expect(order[2]).toBe("B-run")
		end)
	end)
end)