--!nocheck
--!nolint

-------------------------------------------------------------------------------
-- testkit.luau
-- v0.7.3
--------------------------------------------------------------------------------

local color = {
	white_underline = function(s: string)
		return `\27[1;4m{s}\27[0m`
	end,

	white = function(s: string)
		return `\27[37;1m{s}\27[0m`
	end,

	green = function(s: string)
		return `\27[32;1m{s}\27[0m`
	end,

	red = function(s: string)
		return `\27[31;1m{s}\27[0m`
	end,

	yellow = function(s: string)
		return `\27[33;1m{s}\27[0m`
	end,

	red_highlight = function(s: string)
		return `\27[41;1;30m{s}\27[0m`
	end,

	green_highlight = function(s: string)
		return `\27[42;1;30m{s}\27[0m`
	end,

	gray = function(s: string)
		return `\27[30;1m{s}\27[0m`
	end,
}

local function convert_units(unit: string, value: number): (number, string)
	local prefix_colors = {
		[4] = color.red,
		[3] = color.red,
		[2] = color.yellow,
		[1] = color.yellow,
		[0] = color.green,
		[-1] = color.red,
		[-2] = color.yellow,
		[-3] = color.green,
		[-4] = color.red,
	}

	local prefixes = {
		[4] = "T",
		[3] = "G",
		[2] = "M",
		[1] = "k",
		[0] = " ",
		[-1] = "m",
		[-2] = "u",
		[-3] = "n",
		[-4] = "p",
	}

	local order = 0

	while value >= 1000 do
		order += 1
		value /= 1000
	end

	while value ~= 0 and value < 1 do
		order -= 1
		value *= 1000
	end

	if value >= 100 then
		value = math.floor(value)
	elseif value >= 10 then
		value = math.floor(value * 1e1) / 1e1
	elseif value >= 1 then
		value = math.floor(value * 1e2) / 1e2
	end

	return value, prefix_colors[order](prefixes[order] .. unit)
end

local WALL = color.white("│")

--------------------------------------------------------------------------------
-- Testing
--------------------------------------------------------------------------------

type Test = {
	name: string,
	case: Case?,
	cases: { Case },
	duration: number,
	error: {
		message: string,
		trace: string,
	}?,
}

type Case = {
	name: string,
	result: number,
	line: number?,
}

local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4

local skip: string?
local test: Test?
local tests: { Test } = {}

local function output_test_result(test: Test)
	print(color.white_underline(string.lower(test.name)))

	for _, case in test.cases do
		local status = ({
			[PASS] = color.green("PASS"),
			[FAIL] = color.red("FAIL"),
			[NONE] = color.yellow("NONE"),
			[ERROR] = color.red("FAIL"),
		})[case.result]

		local line = case.result == FAIL and color.red(`{case.line}:`) or ""

		print(`{status}{WALL} {line}{color.white(case.name)}`)
	end

	if test.error then
		print(color.white("error: ") .. color.red(test.error.message))
		print(color.white("trace: ") .. color.red(test.error.trace))
	else
		print()
	end
end

local function CASE(name: string)
	assert(test, "no active test")

	local case = {
		name = name,
		result = NONE,
	}

	test.case = case
	table.insert(test.cases, case)
end

local function CHECK<T>(value: T, stack: number?): T
	assert(test, "no active test")
	local case = test.case

	if not case then
		CASE("")
		case = test.case
	end

	assert(case, "no active case")

	if case.result ~= FAIL then
		case.result = value and PASS or FAIL
		case.line = debug.info(stack and stack + 1 or 2, "l")
	end

	return value
end

local function TEST(name: string, fn: () -> ())
	if skip and name ~= skip then
		return
	end

	local active = test
	assert(not active, "cannot start test while another test is in progress")

	test = {
		name = name,
		cases = {},
		duration = 0,
	}
	assert(test)

	table.insert(tests, test)

	local start = os.clock()
	local err
	local success = xpcall(fn, function(m: string)
		err = { message = m, trace = debug.traceback(nil, 2) }
	end)
	test.duration = os.clock() - start

	if not test.case then
		CASE("")
	end
	assert(test.case, "no active case")

	if not success then
		test.case.result = ERROR
		test.error = err
	end

	test = nil
end

local function FINISH(): boolean
	local success = true
	local total_cases = 0
	local passed_cases = 0
	local duration = 0

	for _, test in tests do
		duration += test.duration
		for _, case in test.cases do
			total_cases += 1
			if case.result == PASS or case.result == NONE then
				passed_cases += 1
			else
				success = false
			end
		end

		output_test_result(test)
	end

	print(color.white(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3)))

	local fails = total_cases - passed_cases

	print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`))

	return success, table.clear(tests)
end

local function SKIP(name: string)
	assert(not test, "cannot skip during test")
	skip = name
end

--------------------------------------------------------------------------------
-- Benchmarking
--------------------------------------------------------------------------------

type Bench = {
	time_start: number?,
	memory_start: number?,
	iterations: number?,
}

local bench: Bench?

function START(iter: number?): number
	local n = iter or 1
	assert(n > 0, "iterations must be greater than 0")
	assert(bench, "no active benchmark")
	assert(not bench.time_start, "clock was already started")

	bench.iterations = n
	bench.memory_start = gcinfo()
	bench.time_start = os.clock()
	return n
end

local function BENCH(name: string, fn: () -> ())
	local active = bench
	assert(not active, "a benchmark is already in progress")

	bench = {}
	assert(bench)

	local mem_start = gcinfo()
	local time_start = os.clock()
	local err_msg: string?

	local success = xpcall(fn, function(m: string)
		err_msg = m .. debug.traceback(nil, 2)
	end)

	local time_stop = os.clock()
	local mem_stop = gcinfo()

	if not success then
		print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
		print(color.gray(err_msg :: string))
	else
		time_start = bench.time_start or time_start
		mem_start = bench.memory_start or mem_start

		local n = bench.iterations or 1
		local d, d_unit = convert_units("s", (time_stop - time_start) / n)
		local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))

		local function round(x: number): string
			return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x)
				or string.format("%3.f", x)
		end

		print(
			string.format(
				`%s %s %s %s{WALL} %s`,
				color.gray(round(d)),
				d_unit,
				color.gray(round(a)),
				a_unit,
				color.gray(name)
			)
		)
	end

	bench = nil
end

--------------------------------------------------------------------------------
-- Printing
--------------------------------------------------------------------------------

local function print2(v: unknown)
	type Buffer = { n: number, [number]: string }
	type Cyclic = { n: number, [{}]: number }

	-- overkill concatenationless string buffer
	local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
		local TAB = "    "
		local indent = table.concat(table.create(stack, TAB))

		if type(value) == "string" then
			local n = str.n
			str[n + 1] = '"'
			str[n + 2] = value
			str[n + 3] = '"'
			str.n = n + 3
		elseif type(value) ~= "table" then
			local n = str.n
			str[n + 1] = value == nil and "nil" or tostring(value)
			str.n = n + 1
		elseif next(value) == nil then
			local n = str.n
			str[n + 1] = "{}"
			str.n = n + 1
		else -- is table
			local tabbed_indent = indent .. TAB

			if cyclic[value] then
				str.n += 1
				str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
				return
			else
				cyclic.n += 1
				cyclic[value] = cyclic.n
			end

			str.n += 3
			str[str.n - 2] = "{ "
			str[str.n - 1] = color.gray(tostring(cyclic[value]))
			str[str.n - 0] = "\n"

			local i, v = next(value, nil)
			while v ~= nil do
				local n = str.n
				str[n + 1] = tabbed_indent

				if type(i) ~= "string" then
					str[n + 2] = "["
					str[n + 3] = tostring(i)
					str[n + 4] = "]"
					n += 4
				else
					str[n + 2] = tostring(i)
					n += 2
				end

				str[n + 1] = " = "
				str.n = n + 1

				tos(v, stack + 1, str, cyclic)

				i, v = next(value, i)

				n = str.n
				str[n + 1] = v ~= nil and ",\n" or "\n"
				str.n = n + 1
			end

			local n = str.n
			str[n + 1] = indent
			str[n + 2] = "}"
			str.n = n + 2
		end
	end

	local str = { n = 0 }
	local cyclic = { n = 0 }
	tos(v, 0, str, cyclic)
	print(table.concat(str))
end

--------------------------------------------------------------------------------
-- Equality
--------------------------------------------------------------------------------

local function shallow_eq(a: {}, b: {}): boolean
	if #a ~= #b then
		return false
	end

	for i, v in next, a do
		if b[i] ~= v then
			return false
		end
	end

	for i, v in next, b do
		if a[i] ~= v then
			return false
		end
	end

	return true
end

local function deep_eq(a: {}, b: {}): boolean
	if #a ~= #b then
		return false
	end

	for i, v in next, a do
		if type(b[i]) == "table" and type(v) == "table" then
			if deep_eq(b[i], v) == false then
				return false
			end
		elseif b[i] ~= v then
			return false
		end
	end

	for i, v in next, b do
		if type(a[i]) == "table" and type(v) == "table" then
			if deep_eq(a[i], v) == false then
				return false
			end
		elseif a[i] ~= v then
			return false
		end
	end

	return true
end

return table.freeze({
	test = function()
		return TEST, CASE, CHECK, FINISH, SKIP
	end,
	benchmark = function()
		return BENCH, START
	end,

	seq = shallow_eq,
	print = print2,
	deq = deep_eq,
	color = color,
})


-- warmluau
-- darklua but warm and luau
-- by checkraisefold
local PROJECT_FILE_EXTENSION = "warm.luau"
local PROJECT_FILE_MATCH = "^(.*)%.warm%.luau?$"

local cli = require("@batteries/cli")
local defs = require("@warmluau/")
local filesystem = require("@lute/fs")
local fspath = require("@util/fspath")
local luau = require("@lute/luau")
local meta = require("@lib/meta")
local options = require("./options")
local oven = require("@lib/oven")
local printer = require("@util/syntax/printer")
local process = require("@lute/process")
local recipe = require("@lib/oven/recipe")
local require_vfs = require("@lib/require_vfs")
local requirer = require("@util/requirer")
local styled = require("@util/pretty_print")

type Process = index<defs.PreparedIngredient, "process">

local parser = cli.parser()
options.add_options(parser)

parser:parse({ ... })

if parser:has("help") then
	parser:help()
	return
end
if parser:has("version") then
	print(`warmluau {meta.version}`)
	return
end

local project_path = fspath.normalize(parser:get("project") or meta.default_project_name)
assert(project_path, "failed to normalize project path")
if not string.match(project_path, PROJECT_FILE_MATCH) then
	if fspath.extension(project_path) ~= "luau" then
		error(`project path {styled(project_path)} is not a luau file.`) --
	end
	project_path = fspath.set_extension(project_path, PROJECT_FILE_EXTENSION)
end
project_path = fspath.to_str(fspath.push(project_path, process.cwd()))

if not filesystem.exists(project_path) then
	error(`project file at {styled(project_path)} does not exist.`, 2) --
end

assert(filesystem.exists(project_path), `project file at {styled(project_path)} does not exist.`)
assert(filesystem.type(project_path) == "file", `project file at {styled(project_path)} is not a file.`)

local project_modified = filesystem.stat(project_path).modified

local preheated: typeof(oven.preheat(...))

if not parser:has("bake") then
	print("Not preheating.")
else
	print("Preheating...")
	local input_dir = parser:get("input")
	local input_exists = input_dir and filesystem.exists(input_dir)
	assert(input_exists, `bake requires an input dir that exists, got {styled(input_dir)}`)
	local output_dir = parser:get("output")
	if not filesystem.exists(output_dir) then
		filesystem.mkdir(output_dir)
	end

	preheated = oven.preheat({
		project_modified = project_modified,
		recursive = parser:has("recursive"),
		should_cache = parser:has("cached"),
		input = input_dir,
		output = output_dir,
	})
end

local function get_processors(steps: { defs.Recipe | defs.PreparedIngredient }, list: { Process })
	for steps_index = 1, #steps do
		local step = steps[steps_index]
		if step._steps then
			get_processors(step._steps, list)
		else
			table.insert(list, step.process)
		end
	end
end

local function warmluau_bake(...: defs.PreparedIngredient | defs.Recipe)
	assert(preheated, "failed; did you forget to specify --bake?")
	preheated.bake(...)
end

local function warmluau_recipe(...: defs.Recipe | defs.PreparedIngredient)
	return recipe.create(...)
end

local function warmluau_batch(...: defs.Recipe | defs.PreparedIngredient): defs.PreparedIngredient
	local processors: { Process } = {}
	get_processors({ ... }, processors)
	local processors_count = #processors

	local function batch_process(parsed: defs.Parsley, oven: defs.Cooking): ()
		for processors_index = 1, processors_count do
			processors[processors_index](parsed, oven)
		end
	end

	local prepared_ingredient: defs.PreparedIngredient = {
		process = batch_process,
	}

	return prepared_ingredient
end

local function warmluau_oven(preheat)
	return oven.preheat(preheat)
end

local function warmluau_forwarded(): { string }
	local forwarded = parser:forwarded()
	if forwarded then
		return table.clone(forwarded)
	end
	forwarded = {}
	return forwarded
end

local api: typeof(defs) = {
	recipe = warmluau_recipe,
	batch = warmluau_batch,
	bake = warmluau_bake,
	oven = warmluau_oven,
	forwarded = warmluau_forwarded,
}

require_vfs.value = api

local project_file_contents = filesystem.readfiletostring(project_path)
project_file_contents = requirer.prepend(project_file_contents)

local compiler_success, project_bytecode: luau.Bytecode = pcall(luau.compile, project_file_contents)
if not compiler_success then
	error(
		`Failed to load project at {styled(project_path)}! Bytecode compiler errored:` .. `\n{styled(project_bytecode)}`
	)
end

local success, loaded_project: defs.LoadedProject = pcall(luau.load, project_bytecode, "warmluau project", nil)
if not success then
	error(`Failed to load project at {styled(project_path)}! luau.load errored:\n{styled(loaded_project)}`) --
end

local warmluau_require = requirer.new(project_path, {}, require_vfs)

print("Cooking...")

loaded_project(warmluau_require)

if preheated then
	for _, file in preheated.files do
		local handle = file.write_handle
		local code_output = printer.printfile(file.parsed)
		filesystem.write(handle, code_output)
		filesystem.close(handle)
	end
end

print("Ding! Fries are done")


local cli = require("@batteries/cli")

local function add_options(parser: cli.Parser)
	parser:add("help", "flag", {
		help = "get help",
		aliases = { "h" },
	})
	parser:add("version", "flag", {
		help = "get the version",
		aliases = { "v" },
	})
	parser:add("bake", "flag", {
		help = "base mode (bake)",
		aliases = { "b" },
	})
	parser:add("recursive", "flag", {
		help = "recurse into subdirectories",
		aliases = { "r" },
	})
	parser:add("cached", "flag", {
		help = "attempt to cache the build output for unmodified files",
		aliases = { "cache", "c" },
	})

	parser:add("input", "option", {
		help = "input directory",
		aliases = { "i" },
	})
	parser:add("output", "option", {
		help = "output directory",
		aliases = { "o" },
	})
	parser:add("project", "option", {
		help = "project file path",
		aliases = { "p" },
	})
end

return {
	add_options = add_options,
}


local warmluau = require("@warmluau/")

export type Mode = "early" | "late"

export type Opts = {
	text: string,
	mode: Mode?,
}

return {} :: warmluau.Ingredient<Opts>


local analyzer = require("@util/syntax/analyzer")
local warmluau = require("@warmluau/")

export type ImportValue =
	| string --
	| number
	| boolean
	| vector

export type ImportChain = {
	value: ImportValue | {
		literal: analyzer.ConstantData,
	}?,
	index: {
		[string]: ImportValue | ImportChain,
	}?,
	_const: analyzer.KnownConstant?,
}

export type Opts = {
	inject_imports: { [string]: ImportChain }?,
}

return {} :: warmluau.Ingredient<Opts>


local warmluau = require("@warmluau/")

export type Opts = {
	functions: boolean?,
	variables: boolean?,
	branches: boolean?,
	loops: boolean?,
}

return {} :: warmluau.Ingredient<Opts>


local warmluau = require("@warmluau/")

export type IndexingStyle = "waitforchild" | "property"

export type Opts = {
	sourcemap_path: string,

	indexing: IndexingStyle?,
}

return {} :: warmluau.Ingredient<Opts>


local filesystem = require("@lute/fs")
local parser = require("@util/syntax/parser")

export type Ast = parser.Ast
export type AstExpr = parser.AstExpr
export type Parsley = parser.Parsley

export type LoadedProject = (require: <Path>(target: Path, ...never) -> unknown) -> ...unknown

export type File = {
	read parsed: parser.Parsley,
	read dense: number,
	read src_path: string,
	read dst_path: string,
	read write_handle: filesystem.FileHandle,
}

export type Ingredient<Options = any> = {
	read prepare: (with: Options) -> PreparedIngredient<Options>,
}

export type Recipe = {
	read add: (adding: { Recipe | PreparedIngredient }) -> (),
	read _steps: { Recipe | PreparedIngredient },
}

export type Oven = {
	cooking_file: File?,
	files: { File },
	bake: (...Recipe | PreparedIngredient) -> (),
}

export type Cooking = Oven & { cooking_file: File }

export type PreparedIngredient<Options = any> = {
	read process: (parsley: parser.Parsley, cooking: Cooking) -> (),
	-- stupid hack to make this safely discriminated with Recipe
	read _steps: nil,
}

export type OvenPreheat = {
	input: string,
	output: string,
	recursive: boolean,
	should_cache: boolean,
	project_modified: number?,
}

local warmluau = {}

function warmluau.recipe(...: Recipe | PreparedIngredient): Recipe
	error("unimplemented")
end

function warmluau.batch(...: Recipe | PreparedIngredient): PreparedIngredient
	error("unimplemented")
end

function warmluau.bake(...: Recipe | PreparedIngredient): ()
	error("unimplemented")
end

function warmluau.oven(preheat: OvenPreheat): Oven
	error("unimplemented")
end

function warmluau.forwarded(): { string }
	error("unimplemented")
end

local function WARMLUAU_API_FAIL()
	error("api not implemented; did you forget to use the cli?")
end

(setmetatable :: any)(warmluau, {
	__index = WARMLUAU_API_FAIL,
});
(table.clear :: any)(warmluau)

return warmluau


local analyzer = require("@util/syntax/analyzer")

export type UnaryOperator = analyzer.UnaryOperator
export type BinaryOperator = analyzer.BinaryOperator
export type UnknownConstant = analyzer.UnknownConstant
export type NilConstant = analyzer.NilConstant
export type BooleanConstant = analyzer.BooleanConstant
export type NumberConstant = analyzer.NumberConstant
export type VectorConstant = analyzer.VectorConstant
export type StringConstant = analyzer.StringConstant
export type TableConstant = analyzer.TableConstant
export type Constant = analyzer.Constant
export type KnownConstant = analyzer.KnownConstant
export type SimpleConstant = analyzer.SimpleConstant
export type ConstantData = analyzer.ConstantData
export type Variable = analyzer.Variable
export type Tracked = analyzer.Tracked

return {} :: typeof(analyzer)


local parser = require("@util/syntax/parser")

export type Ast = parser.Ast
export type AstExpr = parser.AstExpr
export type Parsley = parser.Parsley

return {} :: typeof(parser)


local printer = require("@util/syntax/printer")

return {} :: typeof(printer)


local visitors = require("@util/syntax/visitors")

export type Base<State = unknown> = visitors.Base<State>
export type AddType = visitors.AddType
export type AddScope = visitors.AddScope
export type Create<State = unknown> = visitors.Create<State>

return {} :: typeof(visitors)


local cli = {}
cli.__index = cli

type ArgKind = "positional" | "flag" | "option"
type ArgOptions = {
	help: string?,
	aliases: { string }?,
	default: string?,
	required: boolean?,
}

type ArgData = {
	name: string,
	kind: ArgKind,
	options: ArgOptions,
}

type ParseResult = {
	values: { [string]: string },
	flags: { [string]: boolean },
	fwdArgs: { string },
}

type ParserData = {
	arguments: { [number]: ArgData },
	positional: { [number]: ArgData },
	parsed: ParseResult,
}

type ParserInterface = typeof(cli)
export type Parser = setmetatable<ParserData, ParserInterface>

function cli.parser(): Parser
	local self = {
		arguments = {},
		positional = {},
		parsed = { values = {}, flags = {}, fwdArgs = {} },
	}

	return setmetatable(self, cli)
end

function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): ()
	local argument = {
		name = name,
		kind = kind,
		options = options or { aliases = {}, required = false },
	}

	table.insert(self.arguments, argument)
	if kind == "positional" then
		table.insert(self.positional, argument)
	end
end

function cli.parse(self: Parser, args: { string }): ()
	local i = 0
	local pos_index = 1

	while i < #args do
		i += 1

		local arg = args[i]

		-- if the argument is exactly "--", we pass everything along
		if arg == "--" then
			table.move(args, i + 1, #args, 1, self.parsed.fwdArgs)
			break
		end

		-- if the argument starts with two dashes, we're parsing either a flag or an option
		if string.sub(arg, 1, 2) == "--" then
			local name = string.sub(arg, 3)
			local found = false

			for _, argument in self.arguments do
				local aliases = argument.options.aliases or {}

				if argument.name == name or table.find(aliases, name) then
					found = true

					if argument.kind == "option" then
						-- advance past the argument
						i += 1

						assert(i <= #args, "Missing value for argument: " .. argument.name)
						self.parsed.values[argument.name] = args[i]

						break
					end

					self.parsed.flags[argument.name] = true
					break
				end
			end

			assert(found, "Unknown argument: " .. name)
			continue
		end

		-- if the argument starts with a single dash, we're parsing a flag
		if string.sub(arg, 1, 1) == "-" then
			local flags = string.sub(arg, 2)

			for j = 1, #flags do
				local name = string.sub(flags, j, j)
				local found = false
				for _, argument in self.arguments do
					local aliases = argument.options.aliases or {}
					if argument.name == name or table.find(aliases, name) then
						found = true
						if argument.kind == "option" then
							i += 1
							assert(i <= #args, "Missing value for argument: " .. argument.name)
							self.parsed.values[argument.name] = args[i]
						else
							self.parsed.flags[argument.name] = true
						end
						break
					end
				end

				assert(found, "Unknown argument: " .. name)
			end

			continue
		end

		-- if we have positional arguments left, we can take this argument as one
		if pos_index <= #self.positional then
			self.parsed.values[self.positional[pos_index].name] = arg
			pos_index += 1
			continue
		end

		-- otherwise, the argument is forwarded on
		table.insert(self.parsed.fwdArgs, arg)
	end

	-- check that all required arguments are present, and set any default values as needed
	for _, argument in self.arguments do
		assert(argument)

		if argument.options.required and self.parsed.values[argument.name] == nil then
			assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name)
		end

		if self.parsed.values[argument.name] == nil and argument.options.default then
			self.parsed.values[argument.name] = argument.options.default
		end
	end
end

function cli.get(self: Parser, name: string): string
	return self.parsed.values[name]
end

function cli.has(self: Parser, name: string): boolean
	return self.parsed.flags[name] ~= nil
end

function cli.forwarded(self: Parser): { string }?
	return self.parsed.fwdArgs
end

function cli.help(self: Parser): ()
	print("Usage:")
	for _, argument in self.arguments do
		local aliasStr = table.concat(argument.options.aliases or {}, ", ")
		local reqStr = if argument.options.required then "(required) " else ""
		print(string.format("  --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or ""))
	end
end

return table.freeze(cli)


type ExistingOptions = {
	rawStrings: boolean?,
}

local isPrimitiveType = { string = true, number = true, boolean = true }

local typeSortOrder: { [string]: number } = {
	["boolean"] = 1,
	["number"] = 2,
	["string"] = 3,
	["function"] = 4,
	["vector"] = 5,
	["buffer"] = 6,
	["thread"] = 7,
	["table"] = 8,
	["userdata"] = 9,
	["nil"] = 10,
}

local function isPrimitiveArray(array: { [unknown]: unknown }): boolean
	local max, len = 0, #array

	for key, value in array do
		if type(key) ~= "number" then
			return false
		elseif key <= 0 then
			return false
		-- userdatas arent primitives
		elseif not isPrimitiveType[type(value)] then
			return false
		end

		max = math.max(key, max)
	end

	return len == max
end

local function getFormattedAdress(t: {}): string
	return `table<({t})>`
end

local function formatValue(value: unknown, options: ExistingOptions?): string
	if type(value) == "table" then
		return getFormattedAdress(value) -- simple representation for table values
	elseif type(value) ~= "string" then
		return tostring(value)
	end

	if options and options.rawStrings then
		return `"{value}"`
	end
	return string.format("%q", value)
end

local function formatKey(key: unknown, seq: boolean): string
	if seq then
		return ""
	end

	if type(key) == "table" then
		return `[{getFormattedAdress(key)}] = ` -- TODO: handling for table keys
	end
	if type(key) ~= "string" then
		return `[{tostring(key)}] =`
	end

	-- key is a simple identifier
	if string.match(key, "^[%a_][%w_]-$") == key then
		return `{key} = `
	end

	return `[{string.format("%q", key)}] = `
end

local function isEmpty(t: { [unknown]: unknown }): boolean
	for _ in t do
		return false
	end
	return true
end

-- FIXME(luau): mark `dataTable` indexer as read-only
local function traverseTable(
	dataTable: { [unknown]: unknown },
	seen: { [unknown]: boolean },
	indent: number,
	options: ExistingOptions?
): string
	local output = ""
	local indentStr = string.rep("  ", indent)

	local keys = {}

	-- Collect all keys, not just primitives
	for key in dataTable do
		table.insert(keys, key)
	end

	table.sort(keys, function(a: string, b: string): boolean
		local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b])

		if typeofTableA ~= typeofTableB then
			return typeSortOrder[typeofTableA] < typeSortOrder[typeofTableB]
		end

		if type(a) == "number" and type(b) == "number" then
			return a < b
		end

		return tostring(a) < tostring(b)
	end)

	local inSequence = false
	local previousKey = 0

	for idx, key in keys do
		if type(key) == "number" and key > 0 and key - 1 == previousKey then
			previousKey = key
			inSequence = true
		else
			inSequence = false
		end

		local value = dataTable[key]

		if type(value) ~= "table" then
			output = `{output}{indentStr}{formatKey(key, inSequence)}{formatValue(value, options)},\n`
			continue
		end

		-- prevents self-referential tables from looping infinitely
		if seen[value] then
			output = `{output}{indentStr}{formatKey(key, inSequence)}[Circular Reference <({value})>],\n`
			continue
		else
			seen[value] = true
		end

		if isEmpty(value :: { [unknown]: unknown }) then
			output = string.format("%s%s%s{},\n", output, indentStr, formatKey(key, inSequence))
			continue
		end

		if isPrimitiveArray(value :: { [unknown]: unknown }) then -- collapse primitive arrays
			local outputConcatTbl = table.create(#value) :: { string }

			for valueIndex, valueInArray in value :: { unknown } do
				outputConcatTbl[valueIndex] = formatValue(valueInArray)
			end

			output = string.format(
				"%s%s%s{%*},\n",
				output,
				indentStr,
				formatKey(key, inSequence),
				table.concat(outputConcatTbl, ", ")
			)
			continue
		end

		output = string.format(
			"%s%s%s{\n%s%s},\n",
			output,
			indentStr,
			formatKey(key, inSequence),
			traverseTable(value :: any, seen, indent + 1, options),
			indentStr
		)

		seen[value] = nil
	end

	return output
end

return function(data: unknown, options: ExistingOptions?): string
	options = options or {} :: ExistingOptions

	-- if it's not a primitive, we'll pretty print it as a value
	if type(data) ~= "table" then
		return formatValue(data, options)
	end

	return `\{\n{traverseTable(data :: { [unknown]: unknown }, { [data] = true }, 1, options)}\}`
end


--[[
	richterm
	terminal ansi formatter
]]

export type Formatter = (s: string, nocolor: boolean?) -> string

-- @noinline (if it ever exists)
local function format(s: string, open: string, close: string, replace: string): string
	local index = string.find(s, close, #open + 1, true)
	local middle = s

	if index then
		local closeLen = #close
		local middleTbl = {}
		local cursor = 1

		while index do
			table.insert(middleTbl, string.sub(s, cursor, index - 1))
			cursor = index + closeLen
			index = string.find(s, close, cursor, true)
		end

		middle = table.concat(middleTbl, replace)
	end

	return open .. middle .. close
end

local function formatter(open: string, close: string, replace: string?): Formatter
	replace = replace or open

	return function(s, nocolor)
		return if nocolor then s else format(s, open, close, replace :: string)
	end
end

local richterm = {
	reset = formatter("\x1b[0m", "\x1b[0m"),
	bold = formatter("\x1b[1m", "\x1b[22m", "\x1b[22m\x1b[1m"),
	dim = formatter("\x1b[2m", "\x1b[22m", "\x1b[22m\x1b[2m"),
	italic = formatter("\x1b[3m", "\x1b[23m"),
	underline = formatter("\x1b[4m", "\x1b[24m"),
	inverse = formatter("\x1b[7m", "\x1b[27m"),
	hidden = formatter("\x1b[8m", "\x1b[28m"),
	strikethrough = formatter("\x1b[9m", "\x1b[29m"),

	black = formatter("\x1b[30m", "\x1b[39m"),
	red = formatter("\x1b[31m", "\x1b[39m"),
	green = formatter("\x1b[32m", "\x1b[39m"),
	yellow = formatter("\x1b[33m", "\x1b[39m"),
	blue = formatter("\x1b[34m", "\x1b[39m"),
	magenta = formatter("\x1b[35m", "\x1b[39m"),
	cyan = formatter("\x1b[36m", "\x1b[39m"),
	white = formatter("\x1b[37m", "\x1b[39m"),
	gray = formatter("\x1b[90m", "\x1b[39m"),

	bgBlack = formatter("\x1b[40m", "\x1b[49m"),
	bgRed = formatter("\x1b[41m", "\x1b[49m"),
	bgGreen = formatter("\x1b[42m", "\x1b[49m"),
	bgYellow = formatter("\x1b[43m", "\x1b[49m"),
	bgBlue = formatter("\x1b[44m", "\x1b[49m"),
	bgMagenta = formatter("\x1b[45m", "\x1b[49m"),
	bgCyan = formatter("\x1b[46m", "\x1b[49m"),
	bgWhite = formatter("\x1b[47m", "\x1b[49m"),

	brightBlack = formatter("\x1b[90m", "\x1b[39m"),
	brightRed = formatter("\x1b[91m", "\x1b[39m"),
	brightGreen = formatter("\x1b[92m", "\x1b[39m"),
	brightYellow = formatter("\x1b[93m", "\x1b[39m"),
	brightBlue = formatter("\x1b[94m", "\x1b[39m"),
	brightMagenta = formatter("\x1b[95m", "\x1b[39m"),
	brightCyan = formatter("\x1b[96m", "\x1b[39m"),
	brightWhite = formatter("\x1b[97m", "\x1b[39m"),

	bgBrightBlack = formatter("\x1b[100m", "\x1b[49m"),
	bgBrightRed = formatter("\x1b[101m", "\x1b[49m"),
	bgBrightGreen = formatter("\x1b[102m", "\x1b[49m"),
	bgBrightYellow = formatter("\x1b[103m", "\x1b[49m"),
	bgBrightBlue = formatter("\x1b[104m", "\x1b[49m"),
	bgBrightMagenta = formatter("\x1b[105m", "\x1b[49m"),
	bgBrightCyan = formatter("\x1b[106m", "\x1b[49m"),
	bgBrightWhite = formatter("\x1b[107m", "\x1b[49m"),
}

function richterm.combine(...: Formatter): Formatter
	local formatters = { ... }

	return function(s, nocolor)
		if not nocolor then
			for _, formatter in formatters do
				s = formatter(s)
			end
		end

		return s
	end
end

return table.freeze(richterm)


local append_comment = require("@warmluau/ingredients/append_comment")
local luau = require("@lute/luau")
local visitors = require("@util/syntax/visitors")
local warmluau = require("@warmluau/")

type Opts = append_comment.Opts
type Mode = append_comment.Mode

-- Luau
local function INSERT<Item>(tbl: { Item }, value: Item): ()
	table.insert(tbl, value)
end

local function prepare_append_comment(opts: Opts): warmluau.PreparedIngredient<Opts>
	local mode: Mode = opts.mode or "early"

	local visitor = visitors.create() :: visitors.Create<true?>
	local function comment(): luau.SingleLineComment
		return {
			tag = "comment",
			text = opts.text,
			location = {
				begin = { column = 0, line = 0 },
				["end"] = { column = 0, line = 0 },
			},
		}
	end

	function visitor.vtoken(token)
		if visitor.state then
			return
		end
		visitor.state = true
		if mode == "late" then
			INSERT(token.leadingtrivia, comment())
		elseif mode == "early" then
			table.insert(token.leadingtrivia, 1, comment())
		else
			error("unreachable; exhaustive")
		end
	end

	local function process(parsley: warmluau.Parsley): ()
		local ast = parsley.root
		local statements = ast.statements
		if next(statements) then
			visitor:visit_ast_block(ast)
		else
			visitor:visit_ast_token(parsley.eof)
		end
	end

	local prepared_ingredient: warmluau.PreparedIngredient<Opts> = {
		process = process,
	}

	return prepared_ingredient
end

local ingredient: warmluau.Ingredient<Opts> = {
	prepare = prepare_append_comment,
}

return ingredient


local luau = require("@lute/luau")
local visitors = require("@util/syntax/visitors")

export type Processor = (luau.AstExpr) -> luau.AstExpr?
type VisitorState = Processor

local function EXHAUSTIVE_MATCH<Msg>(value: never, msg: Msg?): never
	error(msg or `Unknown value in exhaustive match: {value}`)
end

local value_visitor = visitors.create() :: visitors.Create<VisitorState>

local function try_eval(expression: luau.AstExpr): luau.AstExpr?
	local evaluated = value_visitor.state(expression)
	if not evaluated then
		visitors.inner_visit_expression(expression)
		return nil
	end

	return evaluated
end

function value_visitor.vif(node)
	do
		local evaluated = try_eval(node.condition)
		if evaluated then
			node.condition = evaluated
		end
	end
	visitors.inner_visit_block(node.consequent)
	for _, elseif_node in node.elseifs do
		local evaluated = try_eval(elseif_node.condition)
		if evaluated then
			elseif_node.condition = evaluated
		end
		visitors.inner_visit_block(elseif_node.consequent)
	end
	if node.antecedent then
		visitors.inner_visit_block(node.antecedent)
	end
	return false
end

function value_visitor.vwhile(node)
	do
		local evaluated = try_eval(node.condition)
		if evaluated then
			node.condition = evaluated
		end
	end
	visitors.inner_visit_block(node.body)
	return false
end

function value_visitor.vrepeat(node)
	visitors.inner_visit_block(node.body)
	do
		local evaluated = try_eval(node.condition)
		if evaluated then
			node.condition = evaluated
		end
	end
	return false
end

function value_visitor.vreturn(node)
	for _, expression_pair in node.expressions do
		local evaluated = try_eval(expression_pair.node)
		if evaluated then
			expression_pair.node = evaluated
		end
	end
	return false
end

function value_visitor.vlocal_declaration(node)
	visitors.inner_visit_punctuated(node.variables, visitors.inner_visit_local)
	for _, value in node.values do
		local evaluated = try_eval(value.node)
		if evaluated then
			value.node = evaluated
		end
	end
	return false
end

function value_visitor.vfor(node)
	visitors.inner_visit_local(node.variable)
	do
		local evaluated = try_eval(node.from)
		if evaluated then
			node.from = evaluated
		end
	end
	do
		local evaluated = try_eval(node.to)
		if evaluated then
			node.to = evaluated
		end
	end
	if node.step then
		local evaluated = try_eval(node.step)
		if evaluated then
			node.step = evaluated
		end
	end
	visitors.inner_visit_block(node.body)
	return false
end

function value_visitor.vfor_in(node)
	visitors.inner_visit_token(node.forkeyword)
	visitors.inner_visit_punctuated(node.variables, visitors.inner_visit_local)
	visitors.inner_visit_token(node.inkeyword)
	for _, value in node.values do
		local evaluated = try_eval(value.node)
		if evaluated then
			value.node = evaluated
		end
	end
	visitors.inner_visit_block(node.body)
	visitors.inner_visit_token(node.endkeyword)
	return false
end

function value_visitor.vassign(node)
	visitors.inner_visit_punctuated(node.variables, visitors.inner_visit_expression)
	for _, value in node.values do
		local evaluated = try_eval(value.node)
		if evaluated then
			value.node = evaluated
		end
	end
	return false
end

function value_visitor.vcompound_assign(node)
	visitors.inner_visit_expression(node.variable)
	local evaluated = try_eval(node.value)
	if evaluated then
		node.value = evaluated
	end
	return false
end

function value_visitor.vcall(node)
	visitors.inner_visit_expression(node.func)
	for _, argument_pair in node.arguments do
		local evaluated = try_eval(argument_pair.node)
		if evaluated then
			argument_pair.node = evaluated
		end
	end
	return false
end

function value_visitor.vunary(node)
	local evaluated = try_eval(node.operand)
	if evaluated then
		node.operand = evaluated
	end
	return false
end

function value_visitor.vbinary(node)
	do
		local evaluated = try_eval(node.lhsoperand)
		if evaluated then
			node.lhsoperand = evaluated
		end
	end
	do
		local evaluated = try_eval(node.rhsoperand)
		if evaluated then
			node.rhsoperand = evaluated
		end
	end
	return false
end

function value_visitor.vtable_item(node)
	if node.kind == "list" then
		local evaluated = try_eval(node.value)
		if evaluated then
			node.value = evaluated
		end
	elseif node.kind == "record" then
		local evaluated = try_eval(node.value)
		if evaluated then
			node.value = evaluated
		end
	elseif node.kind == "general" then
		do
			local evaluated = try_eval(node.key)
			if evaluated then
				node.key = evaluated
			end
		end
		do
			local evaluated = try_eval(node.value)
			if evaluated then
				node.value = evaluated
			end
		end
	else
		EXHAUSTIVE_MATCH(node.kind)
	end
	return false
end

function value_visitor.vindex_name(node)
	do
		local evaluated = try_eval(node.expression)
		if evaluated then
			local group: luau.AstExprGroup = {
				tag = "group",
				openparens = {
					leadingtrivia = {},
					position = {
						column = 0,
						line = 0,
					},
					text = "(",
					trailingtrivia = {},
				},
				expression = evaluated,
				closeparens = {
					leadingtrivia = {},
					position = {
						column = 0,
						line = 0,
					},
					text = ")",
					trailingtrivia = {},
				},
			}
			node.expression = group
		end
	end
	return false
end

function value_visitor.vinterpolated_string(node)
	for i = 1, #node.strings do
		local expressions = node.expressions
		if i <= #expressions then
			local evaluated = try_eval(expressions[i])
			if evaluated then
				expressions[i] = evaluated
			end
		end
	end
	return false
end

function value_visitor.vif_expression(node)
	do
		local evaluated = try_eval(node.condition)
		if evaluated then
			node.condition = evaluated
		end
	end
	do
		local evaluated = try_eval(node.consequent)
		if evaluated then
			node.consequent = evaluated
		end
	end
	for _, elseif_node in node.elseifs do
		do
			local evaluated = try_eval(elseif_node.condition)
			if evaluated then
				elseif_node.condition = evaluated
			end
		end
		do
			local evaluated = try_eval(elseif_node.consequent)
			if evaluated then
				elseif_node.consequent = evaluated
			end
		end
	end
	do
		local evaluated = try_eval(node.antecedent)
		if evaluated then
			node.antecedent = evaluated
		end
	end
	return false
end

function value_visitor.type_vtypeof(node)
	local evaluated = try_eval(node.expression)
	if evaluated then
		node.expression = evaluated
	end
	return false
end

local function process_values(ast: luau.AstStatBlock, with: Processor): ()
	value_visitor:visit_ast_block(ast, with)
end

return {
	process_values = process_values,
}


local REQUIRE_GLOBAL = "require"
local DEFAULT_INDEXING_STYLE: IndexingStyle = "property"

local convert_require = require("@warmluau/ingredients/resolve_require")
local defs = require("@warmluau/")
local filesystem = require("@lute/fs")
local fspath = require("@util/fspath")
local json = require("@batteries/json")
local luau = require("@lute/luau")
local process = require("@lute/process")
local require_resolver = require("@util/require_resolver")
local sourcemap = require("@util/sourcemap")
local styled = require("@util/pretty_print")
local visitors = require("@util/syntax/visitors")

type Opts = convert_require.Opts
type IndexingStyle = convert_require.IndexingStyle

type VisitorState = {
	path: string,
	resolver: require_resolver.Identity,
}

local function ASSERT<Value>(value: Value, message: string?)
	if value then
		return value
	end
	error(message or "Assertion Failed!", 2)
end

local function sanitized(str: string): string
	str = string.gsub(str, '"', '\\"')
	str = string.gsub(str, "\\", "\\\\")
	return str
end

local function rojo_sourcemap_indices(
	sourcemap_parent: fspath.ValidPath,
	rojo_sourcemap: sourcemap.Node,
	indexing_style: IndexingStyle
): { [string]: string }
	sourcemap_parent = fspath.to_nav(sourcemap_parent)

	local indices: { [string]: string } = {}

	local nodes = rojo_sourcemap.children
	if not nodes then
		return {}
	end
	nodes = table.clone(nodes)
	local nodes_count = #nodes
	for index = 1, nodes_count do
		local service = nodes[index]
		service.name = `game:GetService("{sanitized(service.name)}")`
	end

	local index = 0
	while index < nodes_count do
		index += 1
		local node = nodes[index]
		local node_name = node.name

		local file_paths = node["filePaths"]
		if file_paths then
			local file_paths_count = #file_paths
			for file_paths_index = 1, file_paths_count do
				local file_path = file_paths[file_paths_index]
				local absolute_path = ASSERT(
					fspath.normalize(fspath.push(file_path, sourcemap_parent)),
					`couldn't normalize path {styled(file_path)} from sourcemap`
				)
				indices[absolute_path] = node_name
			end
		end
		local children = node.children
		if children then
			local children_count = #children
			if indexing_style == "property" then
				for children_index = 1, children_count do
					local child = children[children_index]
					child.name = `{node_name}["{sanitized(child.name)}"]`
				end
			elseif indexing_style == "waitforchild" then
				for children_index = 1, children_count do
					local child = children[children_index]
					child.name = `{node_name}:WaitForChild("{sanitized(child.name)}")`
				end
			else
				error("unreachable")
			end
			table.move(children, 1, children_count, nodes_count + 1, nodes)
			nodes_count += children_count
		end
	end

	return indices
end

local function prepare(opts: Opts): defs.PreparedIngredient<Opts>
	local sourcemap_path = fspath.to_str(fspath.push(opts.sourcemap_path, process.cwd()))
	ASSERT(filesystem.exists(sourcemap_path), `sourcemap at {styled(sourcemap_path)} doesn't exist`)
	ASSERT(filesystem.type(sourcemap_path) == "file", `sourcemap at {styled(sourcemap_path)} is not a file`)
	local sourcemap_parent_path =
		ASSERT(fspath.normalize(fspath.parent(sourcemap_path)), `sourcemap's parent could not be normalized.`)

	local sourcemap_contents = filesystem.readfiletostring(sourcemap_path)
	local deserialized_sourcemap = json.deserialize(sourcemap_contents)
	local rojo_sourcemap = sourcemap.validate(deserialized_sourcemap)

	local indexing_style = opts.indexing or DEFAULT_INDEXING_STYLE

	local indices = rojo_sourcemap_indices(sourcemap_parent_path, rojo_sourcemap, indexing_style)

	local resolving_visitor = visitors.create() :: visitors.Create<VisitorState>

	function resolving_visitor.vcall(node)
		local func = node.func
		if (func.tag ~= "global") or (func.name.text ~= REQUIRE_GLOBAL) then
			return true
		end
		local arg = node.arguments[1]
		if not arg then
			return true
		end
		local call_expr = arg.node
		if call_expr.tag ~= "string" then
			return true
		end
		local state = resolving_visitor.state
		local resolved = state.resolver:resolve_path(call_expr.text)

		local roblox_target = indices[resolved]
		if roblox_target then
			call_expr = luau.parseexpr(roblox_target)
			arg.node = call_expr
		else
			print(`Could not resolve roblox path for "{call_expr.text}" at {state.path}:{func.name.position.line}:`)
		end

		return true
	end

	local function process(parsley: defs.Parsley, oven: defs.Cooking): ()
		local ast = parsley.root
		if not next(ast.statements) then
			return
		end

		local src_path = oven.cooking_file.src_path
		resolving_visitor:visit_ast_block(ast, {
			path = src_path,
			resolver = require_resolver.from_path(src_path),
		})
	end

	local prepared_ingredient: defs.PreparedIngredient<Opts> = {
		process = process,
	}

	return prepared_ingredient
end

local ingredient: defs.Ingredient<Opts> = {
	prepare = prepare,
}

return ingredient


return {
	version = "pre-v0.1.0",
	default_project_name = "default.warm.luau",
}


local FAILED_OUTPUT_FILE = "return error('warmluau could not generate an output for this file');"

local defs = require("@warmluau/")
local filesystem = require("@lute/fs")
local fspath = require("@util/fspath")
local luau = require("@lute/luau")
local styled = require("@util/pretty_print")

type Process = index<defs.PreparedIngredient, "process">

local function get_handle(root: fspath.Nav, file: fspath.Nav): filesystem.FileHandle?
	local file_path = fspath.to_str(fspath.push(file, root))
	if not filesystem.exists(file_path) then
		local merged = table.clone(root)
		local merged_count = #merged
		for file_index = 1, #file - 1 do
			merged_count += 1
			merged[merged_count] = file[file_index]
			local dir_path = fspath.to_str(merged)
			if not filesystem.exists(dir_path) then
				local success, err = pcall(filesystem.mkdir, dir_path)
				if not success then
					print(
						`Could not ensure file {file_path} exists;`
							.. `mkdir errored while ensuring dir {dir_path}:\n{err}`
					) --
					return nil
				end
			end
		end
	end

	local success, file_handle = pcall(filesystem.open, file_path, "w+" :: "w+")
	if not success then
		print(`Failed to open path {styled(file_path)}, errored while opening handle:\n{file_handle}`) --
		return nil
	end

	return file_handle
end

local function locate_luau_files(directory_path: string, recurse: boolean): { fspath.Nav }
	local file_positions: { fspath.Nav } = {}
	local file_positions_count = 0

	local entries = filesystem.listdir(directory_path)
	local entries_count = #entries

	local entries_index = 0
	while entries_index < entries_count do
		entries_index += 1
		local entry = entries[entries_index]

		local entry_name = entry.name
		local entry_nav = fspath.to_nav(entry_name)

		if entry.type ~= "file" then
			if entry.type == "dir" and recurse then
				local full_dir = fspath.to_str(fspath.push(entry_nav, directory_path))
				local child_entries = filesystem.listdir(full_dir)
				local child_entries_count = #child_entries
				for child_index = 1, child_entries_count do
					local child = child_entries[child_index]
					child.name = fspath.to_str(fspath.push(child.name, entry_nav))
				end
				table.move(child_entries, 1, child_entries_count, entries_count + 1, entries)
				entries_count += child_entries_count
			end
			continue
		elseif fspath.extension(entry_name) ~= "luau" then
			continue
		end

		file_positions_count += 1
		file_positions[file_positions_count] = entry_nav
	end

	return file_positions
end

local function oven_preheat(preheat: defs.OvenPreheat): defs.Oven
	local parse_failure = luau.parse(FAILED_OUTPUT_FILE)

	local input_dir, output_dir = fspath.normalize(preheat.input), fspath.normalize(preheat.output)
	assert(input_dir, `Oven failed to preheat; input dir {styled(preheat.input)} could not be normalized.`)
	assert(output_dir, `Oven failed to preheat; output dir {styled(preheat.output)} could not be normalized.`)
	local should_cache = preheat.should_cache

	local input_nav = fspath.to_nav(input_dir)
	local output_nav = fspath.to_nav(output_dir)

	--[[
	TODO: Is this really necessary?
	]]
	if filesystem.exists(output_dir) then
		local output_files = locate_luau_files(output_dir, true)
		for output_files_index = 1, #output_files do
			local file_location = output_files[output_files_index]
			local src_path = fspath.to_str(fspath.push(file_location, output_nav))
			if filesystem.exists(src_path) then
				continue
			end
			local dst_path = fspath.to_str(fspath.push(file_location, input_nav))
			if filesystem.exists(dst_path) then
				filesystem.remove(dst_path)
			end
		end
	end

	local project_modified = preheat.project_modified

	local files: { defs.File } = {}
	local files_count = 0
	local input_files = locate_luau_files(input_dir, preheat.recursive)
	for input_files_index = 1, #input_files do
		local file_location = input_files[input_files_index]
		local src_path = fspath.to_str(fspath.push(file_location, input_nav))
		assert(filesystem.exists(src_path), "path does not exist")
		local dst_path = fspath.to_str(fspath.push(file_location, output_nav))
		if should_cache then
			local src_modified = filesystem.stat(src_path).modified
			if project_modified and project_modified > src_modified then
				src_modified = project_modified --
			end
			local cached_eh = --
				filesystem.exists(dst_path) --
				and (src_modified < filesystem.stat(dst_path).modified)
			if cached_eh then
				continue
			end
		end

		local dst_handle = get_handle(output_nav, file_location)

		if not dst_handle then
			continue
		end

		local read_success, src_contents: string = pcall(filesystem.readfiletostring, src_path)
		if not read_success then
			print(`Failed to open path {styled(src_path)}, filesystem api errored:\n{src_contents}`)
			continue
		end

		local parse_success, parse_result: luau.ParseResult = pcall(luau.parse, src_contents)
		if not parse_success then
			print(`Failed to parse path {styled(src_path)}, parser errored:\n{parse_result}`)
			parse_result = parse_failure
		end

		files_count += 1
		files[files_count] = {
			parsed = parse_result,
			dense = files_count,
			src_path = src_path,
			dst_path = dst_path,
			write_handle = dst_handle,
		}
	end

	local oven: defs.Oven

	--[[
	workaround for Luau typestate
	]]
	local function process_file(process: Process, file: defs.File)
		assert(oven.cooking_file)
		process(file.parsed, oven)
	end

	local function bake_steps(steps: { defs.Recipe | defs.PreparedIngredient }): ()
		for steps_index = 1, #steps do
			local step = steps[steps_index]
			if step._steps then
				bake_steps(step._steps) --
			else
				local process = step.process
				for files_index = 1, files_count do
					local file = files[files_index]
					oven.cooking_file = file
					process_file(process, file)
				end
			end
		end
	end

	local function oven_bake(...: defs.Recipe | defs.PreparedIngredient): ()
		if oven.cooking_file then
			error("already cooking", 2)
		end
		bake_steps({ ... })
		oven.cooking_file = nil
	end

	local value: defs.Oven = {
		cooking_file = nil :: defs.File?,
		files = files,

		bake = oven_bake,
	}
	oven = value

	return oven
end

return {
	preheat = oven_preheat,
}


local defs = require("@warmluau/")

local function recipe_create(...: defs.Recipe | defs.PreparedIngredient): defs.Recipe
	local steps: { defs.Recipe | defs.PreparedIngredient } = { ... }
	local steps_count = #steps

	local function recipe_add(adding: { defs.Recipe | defs.PreparedIngredient }): ()
		local adding_count = #adding
		table.move(adding, 1, adding_count, steps_count + 1, steps)
		steps_count += adding_count
	end

	return {
		_steps = steps,
		add = recipe_add,
	}
end

return {
	create = recipe_create,
}


local IMPL: any = nil

local defs = require("@warmluau/")
local requirer = require("@util/requirer")

local dst = requirer.override

local function dir<Children>(children: Children): Children
	local children = children :: { [string]: unknown }
	for index, value in children do
		children[index] = dst(index, value)
	end
	return children
end

return dst("@warmluau", IMPL :: typeof(defs), {
	ingredients = dst(
		"ingredients",
		nil,
		dir({
			append_comment = require("@lib/ingredients/append_comment"),
			resolve_require = require("@lib/ingredients/resolve_require"),
			resolve_constant = require("@lib/ingredients/resolve_constant"),
		})
	),
	syntax = dst(
		"syntax",
		nil,
		dir({
			analyzer = require("@util/syntax/analyzer"),
			parser = require("@util/syntax/parser"),
			printer = require("@util/syntax/printer"),
			visitors = require("@util/syntax/visitors"),
		})
	),
})


--[[
Copyright 2024 ffrostfall

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
--!nocheck
local ESCAPE = string.char(27)
local ANSI_16 = `{ESCAPE}[%dm`

export type Styler = (text: string) -> string

local function createStylerFunction(opener: number, closer: number): Styler
	local open = string.format(ANSI_16, opener)
	local close = string.format(ANSI_16, closer)

	return function(text: string)
		return `{open}{text}{close}`
	end
end

local function combineStyles(styleArray: { (text: string) -> string }): Styler
	return function(text: string)
		local result = text

		for _, style in styleArray do
			result = style(result)
		end

		return result
	end
end

return {
	combineStyles = combineStyles,

	modifier = {
		dim = createStylerFunction(2, 22),
		reset = createStylerFunction(0, 0),
		bold = createStylerFunction(1, 22),
		italic = createStylerFunction(3, 23),
		hidden = createStylerFunction(8, 28),
		inverse = createStylerFunction(7, 27),
		underline = createStylerFunction(4, 24),
		overline = createStylerFunction(53, 55),
		strikethrough = createStylerFunction(9, 29),
	},

	color = {
		red = createStylerFunction(31, 39),
		blue = createStylerFunction(34, 39),
		cyan = createStylerFunction(36, 39),
		gray = createStylerFunction(90, 39),
		grey = createStylerFunction(90, 39),
		black = createStylerFunction(30, 39),
		green = createStylerFunction(32, 39),
		white = createStylerFunction(37, 39),
		yellow = createStylerFunction(33, 39),
		magenta = createStylerFunction(35, 39),
		red_bright = createStylerFunction(91, 39),
		blue_bright = createStylerFunction(94, 39),
		cyan_bright = createStylerFunction(96, 39),
		black_bright = createStylerFunction(90, 39),
		green_bright = createStylerFunction(92, 39),
		white_bright = createStylerFunction(97, 39),
		yellow_bright = createStylerFunction(93, 39),
		magenta_bright = createStylerFunction(95, 39),
	},
}


local system = require("@lute/system")
local types = require("@self/types")

export type Nav = types.Nav
export type ValidPath = types.ValidPath

return (if system.os:lower():match("windows") then require("@self/windows") else require("@self/unix")) :: types.Api


export type Nav = { string }
export type ValidPath = Nav | string

export type Api = {
	to_nav: (path: ValidPath) -> Nav,
	from_str: (path: string) -> Nav,
	clone_nav: (path: ValidPath) -> Nav,
	to_str: (path: ValidPath) -> string,
	from_nav: (path: Nav) -> string,
	has_root: (path: ValidPath) -> boolean,
	absolute_eh: (path: ValidPath) -> boolean,
	parent: (path: ValidPath) -> string,
	starts_with: (path: ValidPath, base: ValidPath) -> boolean,
	stops_with: (path: ValidPath, child: ValidPath) -> boolean,
	normalize: (path: ValidPath) -> string?,
	real_parent: (path: ValidPath) -> string?,
	equal: (first: ValidPath?, second: ValidPath?) -> boolean,
	last: (path: ValidPath) -> string?,
	stem: (path: ValidPath) -> string?,
	extension: (path: ValidPath) -> string?,
	set_extension: (path: ValidPath, to: string?) -> string,
	push: (path: ValidPath, onto: ValidPath) -> Nav,

	SEPARATOR: string,
}

return nil


local NAVIGATE_BACK = ".."
local NAVIGATE_HERE = "."
local SEPARATOR_MATCH = "/+"
local SEPARATOR = "/"

local types = require("./types")

type ValidPath = types.ValidPath
type Nav = types.Nav
type Api = types.Api

local function to_nav(path: ValidPath): Nav
	if type(path) == "table" then
		return path
	end
	return (string.split(string.gsub(string.gsub(path, SEPARATOR_MATCH, SEPARATOR), `{SEPARATOR}$`, ""), SEPARATOR))
end

local function from_str(path: string): Nav
	return (string.split(string.gsub(string.gsub(path, SEPARATOR_MATCH, SEPARATOR), `{SEPARATOR}$`, ""), SEPARATOR))
end

local function clone_nav(path: ValidPath): Nav
	if type(path) == "table" then
		return table.clone(path)
	end
	return (string.split(string.gsub(string.gsub(path, SEPARATOR_MATCH, SEPARATOR), `{SEPARATOR}$`, ""), SEPARATOR))
end

local function to_str(path: ValidPath): string
	if type(path) == "string" then
		return path
	end
	return (table.concat(path, SEPARATOR))
end

local function from_nav(path: Nav): string
	return (table.concat(path, SEPARATOR))
end

local function has_root(path: ValidPath): boolean
	local str = to_str(path)
	if string.match(str, `^{SEPARATOR_MATCH}`) then
		return true
	end
	return false
end

local function absolute_eh(path: ValidPath): boolean
	path = to_str(path)
	return has_root(path)
end

local function parent(path: ValidPath): string
	return `{to_str(path)}{SEPARATOR}{NAVIGATE_BACK}`
end

local function starts_with(path: ValidPath, base: ValidPath): boolean
	local path_nav = to_nav(path)
	local base_nav = to_nav(base)

	local path_len = #path_nav
	local base_len = #base_nav

	if base_len > path_len then
		return false
	end

	for index = 1, base_len do
		if path_nav[index] ~= base_nav[index] then
			return false
		end
	end

	return true
end

local function stops_with(path: ValidPath, child: ValidPath): boolean
	local path_nav = to_nav(path)
	local child_nav = to_nav(child)

	local path_len = #path_nav
	local child_len = #child_nav

	if child_len > path_len then
		return false
	end

	local path_index = path_len
	for child_index = child_len, 1, -1 do
		if child_nav[child_index] ~= path_nav[path_index] then
			return false
		end
		path_index -= 1
	end

	return true
end

local function normalize(path: ValidPath): string?
	local nav = clone_nav(path)

	local nav_count = #nav
	local backnav_count = 0
	for nav_index = nav_count, 1, -1 do
		local component = nav[nav_index]
		if component == NAVIGATE_HERE then
			table.remove(nav, nav_index)
		elseif component == NAVIGATE_BACK then
			backnav_count += 1
			table.remove(nav, nav_index)
		elseif backnav_count > 0 then
			backnav_count -= 1
			table.remove(nav, nav_index)
		end
	end

	if backnav_count > 0 or not nav[1] then
		return nil
	end

	return (from_nav(nav))
end

local function real_parent(path: ValidPath): string?
	local nav = clone_nav(path)
	table.remove(nav, #nav)
	if nav[1] then
		return (from_nav(nav))
	end
	return nil
end

local function equal(first: ValidPath?, second: ValidPath?): boolean
	if first then
		first = normalize(first)
	end
	if second then
		second = normalize(second)
	end
	return first == second
end

local function last(path: ValidPath): string?
	path = to_str(path)
	local reversed_path = string.reverse(path)
	local sep_location = string.find(reversed_path, SEPARATOR_MATCH)
	if not sep_location then
		return path
	end
	local filename = string.reverse(string.sub(reversed_path, 1, sep_location - 1))
	return filename
end

local function stem(path: ValidPath): string?
	path = to_str(path)
	local reversed_path = string.reverse(path)
	local dot_location = string.find(reversed_path, "%.")
	if not dot_location then
		return nil
	end
	local sep_location = string.find(reversed_path, SEPARATOR_MATCH) or #path
	if sep_location < dot_location then
		return nil
	end
	local stem = string.reverse(string.sub(reversed_path, dot_location + 1, sep_location - 1))
	return stem
end

local function extension(path: ValidPath): string?
	path = to_str(path)
	local reversed_path = string.reverse(path)
	local dot_location = string.find(reversed_path, "%.")
	if not dot_location then
		return nil
	end
	local sep_location = string.find(reversed_path, SEPARATOR_MATCH)
	if sep_location and sep_location < dot_location then
		return nil
	end
	local extension = string.reverse(string.sub(reversed_path, 1, dot_location - 1))
	return extension
end

local function set_extension(path: ValidPath, to: string?): string
	path = to_str(path)
	local filename = last(path)
	if not filename then
		if to then
			return `{path}.{to}`
		end
		return path
	end
	if not string.find(filename, "%.") then
		if to then
			return `{path}.{to}`
		end
		return path
	end
	local reversed_path = string.reverse(to_str(path))
	local ext_stop = string.find(reversed_path, "%.")
	if not ext_stop then
		if to then
			return `{path}.{to}`
		end
		return path
	end
	local until_extension = string.reverse(string.sub(reversed_path, ext_stop, #reversed_path))
	if to then
		return `{until_extension}{to}`
	end
	return until_extension
end

local function push(path: ValidPath, onto: ValidPath): Nav
	local path_nav = to_nav(path)
	if absolute_eh(path) then
		return path_nav
	end
	local onto_nav = clone_nav(onto)
	table.move(path_nav, 1, #path_nav, #onto_nav + 1, onto_nav)
	return onto_nav
end

local api: Api = {
	to_nav = to_nav,
	from_str = from_str,
	clone_nav = clone_nav,
	to_str = to_str,
	from_nav = from_nav,
	has_root = has_root,
	absolute_eh = absolute_eh,
	parent = parent,
	starts_with = starts_with,
	stops_with = stops_with,
	normalize = normalize,
	real_parent = real_parent,
	equal = equal,
	last = last,
	stem = stem,
	extension = extension,
	set_extension = set_extension,
	push = push,

	SEPARATOR = SEPARATOR,
}

return api


--!optimize 2
--!native
local NAVIGATE_BACK = ".."
local NAVIGATE_HERE = "."
local SEPARATOR_MATCH = "[/\\]+"
local SEPARATOR = "/"

local types = require("./types")

type ValidPath = types.ValidPath
type Nav = types.Nav
type Api = types.Api

local function to_nav(path: ValidPath): Nav
	if type(path) == "table" then
		return path
	end
	return (string.split(string.gsub(string.gsub(path, SEPARATOR_MATCH, SEPARATOR), `{SEPARATOR}$`, ""), SEPARATOR))
end

local function from_str(path: string): Nav
	return (string.split(string.gsub(string.gsub(path, SEPARATOR_MATCH, SEPARATOR), `{SEPARATOR}$`, ""), SEPARATOR))
end

local function clone_nav(path: ValidPath): Nav
	if type(path) == "table" then
		return table.clone(path)
	end
	return (string.split(string.gsub(string.gsub(path, SEPARATOR_MATCH, SEPARATOR), `{SEPARATOR}$`, ""), SEPARATOR))
end

local function to_str(path: ValidPath): string
	if type(path) == "string" then
		return path
	end
	return (table.concat(path, SEPARATOR))
end

local function from_nav(path: Nav): string
	return (table.concat(path, SEPARATOR))
end

local function has_root(path: ValidPath): boolean
	local str = to_str(path)
	if string.match(str, `^[a-zA-z]:{SEPARATOR_MATCH}`) or string.match(str, `^{SEPARATOR_MATCH}`) then
		return true
	end
	return false
end

local function absolute_eh(path: ValidPath): boolean
	path = to_str(path)
	if string.match(path, `^[a-zA-z]:{SEPARATOR_MATCH}`) then
		return true --
	end
	return false
end

local function parent(path: ValidPath): string
	return `{to_str(path)}{SEPARATOR}{NAVIGATE_BACK}`
end

local function starts_with(path: ValidPath, base: ValidPath): boolean
	local path_nav = to_nav(path)
	local base_nav = to_nav(base)

	local path_len = #path_nav
	local base_len = #base_nav

	if base_len > path_len then
		return false
	end

	for index = 1, base_len do
		if path_nav[index] ~= base_nav[index] then
			return false
		end
	end

	return true
end

local function stops_with(path: ValidPath, child: ValidPath): boolean
	local path_nav = to_nav(path)
	local child_nav = to_nav(child)

	local path_len = #path_nav
	local child_len = #child_nav

	if child_len > path_len then
		return false
	end

	local path_index = path_len
	for child_index = child_len, 1, -1 do
		if child_nav[child_index] ~= path_nav[path_index] then
			return false
		end
		path_index -= 1
	end

	return true
end

local function normalize(path: ValidPath): string?
	local nav = clone_nav(path)

	local nav_count = #nav
	local backnav_count = 0
	for nav_index = nav_count, 1, -1 do
		local component = nav[nav_index]
		if component == NAVIGATE_HERE then
			table.remove(nav, nav_index)
		elseif component == NAVIGATE_BACK then
			backnav_count += 1
			table.remove(nav, nav_index)
		elseif backnav_count > 0 then
			backnav_count -= 1
			table.remove(nav, nav_index)
		end
	end

	if backnav_count > 0 or not nav[1] then
		return nil
	end

	return (from_nav(nav))
end

local function real_parent(path: ValidPath): string?
	local nav = clone_nav(path)
	table.remove(nav, #nav)
	if nav[1] then
		return (from_nav(nav))
	end
	return nil
end

local function equal(first: ValidPath?, second: ValidPath?): boolean
	if first then
		first = normalize(first)
	end
	if second then
		second = normalize(second)
	end
	return first == second
end

local function last(path: ValidPath): string?
	path = to_str(path)
	local reversed_path = string.reverse(path)
	local sep_location = string.find(reversed_path, SEPARATOR_MATCH)
	if not sep_location then
		return path
	end
	local filename = string.reverse(string.sub(reversed_path, 1, sep_location - 1))
	return filename
end

local function stem(path: ValidPath): string?
	path = to_str(path)
	local reversed_path = string.reverse(path)
	local dot_location = string.find(reversed_path, "%.")
	if not dot_location then
		return nil
	end
	local sep_location = string.find(reversed_path, SEPARATOR_MATCH) or #path
	if sep_location < dot_location then
		return nil
	end
	local stem = string.reverse(string.sub(reversed_path, dot_location + 1, sep_location - 1))
	return stem
end

local function extension(path: ValidPath): string?
	path = to_str(path)
	local reversed_path = string.reverse(path)
	local dot_location = string.find(reversed_path, "%.")
	if not dot_location then
		return nil
	end
	local sep_location = string.find(reversed_path, SEPARATOR_MATCH)
	if sep_location and sep_location < dot_location then
		return nil
	end
	local extension = string.reverse(string.sub(reversed_path, 1, dot_location - 1))
	return extension
end

local function set_extension(path: ValidPath, to: string?): string
	path = to_str(path)
	local filename = last(path)
	if not filename then
		if to then
			return `{path}.{to}`
		end
		return path
	end
	if not string.find(filename, "%.") then
		if to then
			return `{path}.{to}`
		end
		return path
	end
	local reversed_path = string.reverse(to_str(path))
	local ext_stop = string.find(reversed_path, "%.")
	if not ext_stop then
		if to then
			return `{path}.{to}`
		end
		return path
	end
	local until_extension = string.reverse(string.sub(reversed_path, ext_stop, #reversed_path))
	if to then
		return `{until_extension}{to}`
	end
	return until_extension
end

local function push(path: ValidPath, onto: ValidPath): Nav
	local path_nav = to_nav(path)
	if absolute_eh(path) then
		return path_nav
	end
	local onto_nav = clone_nav(onto)
	table.move(path_nav, 1, #path_nav, #onto_nav + 1, onto_nav)
	return onto_nav
end

local api: Api = {
	to_nav = to_nav,
	from_str = from_str,
	clone_nav = clone_nav,
	to_str = to_str,
	from_nav = from_nav,
	has_root = has_root,
	absolute_eh = absolute_eh,
	parent = parent,
	starts_with = starts_with,
	stops_with = stops_with,
	normalize = normalize,
	real_parent = real_parent,
	equal = equal,
	last = last,
	stem = stem,
	extension = extension,
	push = push,
	set_extension = set_extension,

	SEPARATOR = SEPARATOR,
}

return api


local INDENT = "   "
local DEFAULT_PRETTY_PRINT_DECIMALS = 4
-- scientific notation is in base 10, this is the closest power of 10 above 2^53 which is the max integer an f64 (luau
-- number) can accurately represent.
local SCIENTIFIC_NOTATION_THRESHOLD = 10 ^ 16

local colorful = require("./colorful")

local color = colorful.color
local modifier = colorful.modifier

local brightblue = color.blue_bright
local cyan = color.cyan
local grey = color.grey
local magenta = color.magenta
local underline = modifier.underline

type UnknownTbl = { [unknown]: unknown }

local pretty_print_digits_precision = 4
local indent_print
local pretty_print_from_precision
local _pretty_print
local indent_newlines
local pretty_print_nil
local pretty_print_str
local pretty_print_tbl
local pretty_print_bool
local pretty_print_func
local pretty_print_number
local pretty_print_thread
local pretty_print_vector
local pretty_print_unknown

function indent_newlines(str: string): string
	local indented = str:gsub("\n", `\n{INDENT}`)
	return indented
end

function indent_print(input: unknown): string
	return indent_newlines(_pretty_print(input))
end

do
	local comma_value
	function pretty_print_number(num: number): string
		-- Luau
		local string_format = string.format :: any

		local as_str: string

		if num >= SCIENTIFIC_NOTATION_THRESHOLD then
			local formatted_round = string_format(`%.{pretty_print_digits_precision}e`, num)

			as_str = formatted_round
		else
			local remainder = num % (10 ^ -pretty_print_digits_precision)
			local rounded = num - remainder

			as_str = `{comma_value(rounded)}`
		end

		return brightblue(as_str)
	end
	function comma_value(amount: number): string
		-- radix matches can be 0 or 1, because the match is greedy and anchored to the beginning of the string.
		-- once it's zero, there are no more groups of three digits to separate by comma.
		local formatted, radix_matches = tostring(amount):gsub("^(%d+)(%d%d%d)", "%1,%2")
		while radix_matches ~= 0 do
			-- Luau
			formatted, radix_matches = (formatted :: string):gsub("^(%d+)(%d%d%d)", "%1,%2")
		end
		return formatted
	end
end

function pretty_print_str(str: string): string
	if str:match("\n") then
		return magenta("[[\n") .. indent_newlines(str) .. magenta("\n]]") --
	end
	return `{magenta('"')}{brightblue(str)}{magenta('"')}`
end

function pretty_print_nil(nothing: nil): string
	return brightblue(underline("nil"))
end

function pretty_print_bool(bool: boolean): string
	return brightblue(underline(`{bool}`))
end

function pretty_print_vector(vector: vector): string
	return "vector"
		.. `{magenta("<")}`
		.. `{pretty_print_number(vector.x)}`
		.. `, {pretty_print_number(vector.y)}`
		.. `, {pretty_print_number(vector.z)}`
		.. `{magenta(">")}`
end

function pretty_print_func<Params..., Returns...>(func: (Params...) -> Returns...): string
	local address = tostring(func):gsub(".* ", "")

	local name = debug.info(func, "n")

	if (name == nil) or (name == "") then
		return magenta("<") .. `function ({address})` .. magenta(">")
	else
		return magenta("<") .. `function "{name}" ({address})` .. magenta(">")
	end
end

function pretty_print_thread(thread: thread): string
	local STATUS = coroutine.status(thread)
	STATUS = STATUS:upper()

	local output = magenta("<") .. `{tostring(thread)} {STATUS} @ ` .. magenta("[")

	output ..= indent_newlines(`\n{debug.traceback(thread)}`)

	-- weird hack to remove ending newline
	output = output:gsub(`\n{INDENT}$`, "")

	output ..= "\n" .. magenta("]") .. magenta(">")

	return output
end

function pretty_print_tbl(tbl: UnknownTbl): string
	local output = magenta(`\{\n`)

	for key, value in tbl do
		output ..= `{INDENT}{cyan("[")}{indent_print(key)}{cyan("]")} = {indent_print(value)}\n`
	end

	output ..= magenta("}")

	return output
end

function pretty_print_unknown(value: unknown): string
	local typeof_value = typeof(value)

	-- remove any potential interfering beginning/ending chevrons
	local stringified = tostring(value):gsub("^<", ""):gsub(">$", "")

	-- if value's tostring contains its type, don't include it in the stringification
	if stringified:lower():match(typeof_value:lower()) then
		stringified = magenta("<") .. grey(stringified) .. magenta(">")
	else
		stringified = magenta("<") .. grey(`{typeof_value}: {stringified}`) .. magenta(">")
	end

	return stringified
end

function _pretty_print(value: any): string
	if type(value) == "number" then
		return pretty_print_number(value)
	end
	if type(value) == "string" then
		return pretty_print_str(value)
	end
	if type(value) == "nil" then
		return pretty_print_nil(value)
	end
	if type(value) == "boolean" then
		return pretty_print_bool(value)
	end
	if type(value) == "vector" then
		return pretty_print_vector(value)
	end
	if type(value) == "function" then
		return pretty_print_func(value :: (...any) -> ...any)
	end
	if type(value) == "thread" then
		return pretty_print_thread(value)
	end
	if type(value) == "table" then
		return pretty_print_tbl(value :: UnknownTbl)
	end
	return pretty_print_unknown(value)
end

function pretty_print_from_precision(value: any, digits_precision: number?): string
	if not digits_precision then
		return _pretty_print(value)
	end

	pretty_print_digits_precision = digits_precision
	local output = _pretty_print(value)
	pretty_print_digits_precision = DEFAULT_PRETTY_PRINT_DECIMALS

	return output
end

function pretty_print_from_epsilon(value: any, epsilon: number?): string
	if not epsilon then
		return _pretty_print(value)
	end

	pretty_print_digits_precision = math.max(math.clamp(-math.log10(epsilon), 0, 100))
	local output = _pretty_print(value)
	pretty_print_digits_precision = DEFAULT_PRETTY_PRINT_DECIMALS

	return output
end

local function indent(str: string): string
	return `{INDENT}{str}`
end

local api = {
	from_precision = pretty_print_from_precision,
	from_epsilon = pretty_print_from_epsilon,
	indent_newlines = indent_newlines,
	indent = indent,
}

setmetatable(api :: any, {
	__call = function(_, value: any)
		return pretty_print_from_epsilon(value)
	end,
})

return (api :: any) :: typeof(api) & (value: any) -> string


local filesystem = require("@lute/fs")
local fspath = require("@util/fspath")
local json = require("@batteries/json")
local process = require("@lute/process")
local resolver_types = require("./types")
local styled = require("@util/pretty_print")

type RequireAliases = resolver_types.RequireAliases

local function ASSERT<Value>(value: Value, message: string?)
	if value then
		return value
	end
	error(message or "Assertion Failed!", 2)
end

local homedir_nav = fspath.to_nav(process.homedir())

local cache: { [unknown]: RequireAliases } = {}
local function aliases_under_dir(module_directory: string): RequireAliases
	local normalized_file_directory =
		ASSERT(fspath.normalize(module_directory), `could not normalize luaurc directory {styled(module_directory)}`)
	do
		local cached = cache[normalized_file_directory]
		if cached then
			return table.clone(cached)
		end
	end

	local all_aliases = {}

	local parent = normalized_file_directory
	while parent do
		if not fspath.absolute_eh(parent) then
			break
		end
		local luaurc_parent = parent
		parent = fspath.normalize(fspath.parent(parent))

		local luaurc_path = fspath.push(".luaurc", luaurc_parent)

		local luaurc_path_str = fspath.to_str(luaurc_path)

		local file_eh = filesystem.exists(luaurc_path_str)
		if not file_eh then
			continue
		end

		local luaurc_content = filesystem.readfiletostring(luaurc_path_str)
		local luaurc_data: any = json.deserialize(luaurc_content)

		local luaurc_aliases: { [string]: any }? = luaurc_data.aliases

		if luaurc_aliases == nil then
			continue
		end

		for alias, target in luaurc_aliases do
			if all_aliases[alias] ~= nil then
				continue
			end

			if fspath.starts_with(target, "~") then
				local output_nav = table.clone(homedir_nav)
				local cloned_target = fspath.clone_nav(target)
				table.move(cloned_target, 2, #cloned_target, #homedir_nav + 1, output_nav)
				target = fspath.to_str(output_nav)
			else
				target = ASSERT(
					fspath.normalize(fspath.push(target, luaurc_parent)),
					`normalizing target for alias "{alias}" failed`
				)
			end

			if not fspath.absolute_eh(target) then
				error(
					`Problem registering alias "{alias}": "{target}" in luaurc "{luaurc_path}",`
						.. ` path parent is not absolute.`
				)
			end

			all_aliases[alias] = target
		end
	end

	cache[normalized_file_directory] = all_aliases

	return (table.clone(all_aliases))
end

return {
	under_dir = aliases_under_dir,
}


local ALIAS_PATTERN = `^@(.+)`

local filesystem = require("@lute/fs")
local fspath = require("@util/fspath")
local get_aliases = require("@self/get_aliases")
local resolver_types = require("@self/types")

type RequireAliases = resolver_types.RequireAliases

local function ASSERT<Value>(value: Value, message: string?)
	if value then
		return value
	end
	error(message or "Assertion Failed!", 2)
end

local function resolve_module_path(file: string): (string, string?)
	file = ASSERT(fspath.normalize(file), `could not normalize file's path '{file}'`)
	local requiring_dir = ASSERT(
		fspath.normalize(fspath.parent(file)),
		`Problem resolving module for path '{file}'; path's parent could not be normalized.`
	)

	local filename = ASSERT(fspath.last(file), `Problem getting module for path '{file}'; No filename??`)

	local init_path: string?
	if (filename == "init.luau") or (filename == "init.lua") then
		init_path = requiring_dir
		requiring_dir = ASSERT(
			fspath.normalize(fspath.parent(requiring_dir)),
			`Problem getting module for path '{file}'; could not normalize init module directory.`
		)
	end

	return requiring_dir, init_path
end

local resolver = {}
local metatable = { __index = resolver }

export type Identity = setmetatable<{
	_aliases: RequireAliases,
	_directory: string,
}, typeof(metatable)>

local function resolver_from_path(file: string): Identity
	ASSERT(filesystem.exists(file), `Problem constructing resolver for path '{file}'; path does not exist.`)
	ASSERT(filesystem.type(file) == "file", `Problem constructing resolver for path '{file}'; path is not a file.`)

	local module_path, self_path = resolve_module_path(file)

	local aliases = get_aliases.under_dir(module_path)

	if self_path then
		aliases.self = self_path
	end

	local self = setmetatable({
		_aliases = aliases,
		_directory = module_path,
	}, metatable)

	return self
end

function resolver.resolve_path(self: Identity, path: string): string
	local nav = fspath.from_str(path)

	local path_begins = ASSERT(table.remove(nav, 1), `could not resolve path '{path}' because it is empty`)
	local alias = string.match(path_begins, ALIAS_PATTERN)

	local resolve_root: string

	if alias ~= nil then
		local aliases = self._aliases

		local alias_target: string? = aliases[alias]

		if alias_target == nil then
			error(`Could not resolve module for '{path}'; could not find alias "{alias}".`) --
		end

		resolve_root = alias_target
	elseif path_begins == ".." then
		resolve_root = fspath.parent(self._directory)
	elseif path_begins == "." then
		resolve_root = self._directory
	else
		error(`Could not resolve module for '{path}'; paths must begin with either "@<alias>/", "./", or "../"`)
	end

	local resolve_suffix = fspath.from_nav(nav)

	local full_resolve_path = ASSERT(
		fspath.normalize(fspath.push(resolve_suffix, resolve_root)),
		`Could not resolve module for '{path}'; resulting path could not be normalized.`
	)
	ASSERT(
		fspath.absolute_eh(full_resolve_path),
		`Could not resolve module for '{path}'; resulting path was not absolute.`
	)

	local try_list = {
		fspath.to_str(fspath.push("init.luau", full_resolve_path)),
		fspath.to_str(fspath.push("init.lua", full_resolve_path)),
		fspath.to_str(fspath.set_extension(full_resolve_path, "luau")),
		fspath.to_str(fspath.set_extension(full_resolve_path, "lua")),
	}

	local found_file: string
	for _, try in try_list do
		if not filesystem.exists(try) then
			continue
		elseif filesystem.type(try) ~= "file" then
			continue
		end
		ASSERT(
			not found_file,
			`Could not resolve module for '{path}'; found ambiguous filepaths: '{found_file}' and '{try}'`
		)
		found_file = try
	end

	ASSERT(
		found_file,
		`Could not resolve module for '{path}'; could not find a matching path.`
			.. ` Tried:\n{table.concat(try_list, ",\n")}`
	)

	found_file = ASSERT(
		fspath.normalize(found_file),
		`Could not resolve module for '{path}'; failed to normalize filename {found_file}??`
	)

	return found_file
end

return {
	from_path = resolver_from_path,
	get_module = resolve_module_path,
	get_aliases = get_aliases,
}


local PREPEND = [[math.randomseed(0); local require = ...;]]

local filesystem = require("@lute/fs")
local fspath = require("@util/fspath")
local luau = require("@lute/luau")
local override = require("@self/override")
local require_resolver = require("@util/require_resolver")
local styled = require("@util/pretty_print")

export type ResultCache = { [string]: { value: unknown } }

local function loader_prepend(file: string): string
	return `{PREPEND}{file}`
end

local function count_vararg(...: unknown): (unknown, number)
	local output = ...
	return output, select("#", ...)
end

local function try_find_override(
	path: fspath.ValidPath,
	base: override.Identity,
	prefix: { string }?
): override.Identity?
	prefix = prefix or {}
	path = fspath.to_nav(path)

	local base_path = fspath.push(base.path, prefix)

	if fspath.starts_with(path, base_path) and fspath.equal(path, base_path) then
		return base
	end

	for _, child in base.children do
		local found = try_find_override(path, child, base_path)
		if found then
			return found
		end
	end

	return nil
end

local function require_constructor(requiring_path: string, result_cache: ResultCache?, override: override.Identity?)
	local result_cache: ResultCache = result_cache or {}
	local resolver = require_resolver.from_path(requiring_path)

	local function warmluau_require<Path>(target: Path, ...: never): unknown
		assert(select("#", ...) == 0, "exactly 1 argument expected")
		if typeof(target) ~= "string" then
			return (require)(target)
		end
		local nav = fspath.to_nav(target)

		if override and fspath.starts_with(nav, override.path) then
			local found = try_find_override(nav, override)
			if not found then
				print(`Could not resolve overrided path for {styled(found)}.`)
			else
				return found.value
			end
		elseif nav[1] == "@lute" then
			return (require)(target)
		end

		local resolved_path = resolver:resolve_path(target)

		do
			local cached = result_cache[resolved_path]
			if cached then
				return cached.value
			end
		end

		local resolved_contents = filesystem.readfiletostring(resolved_path)
		resolved_contents = loader_prepend(resolved_contents)
		local resolved_bytecode = luau.compile(resolved_contents)
		local loaded = luau.load(resolved_bytecode, resolved_path)

		local env_require = require_constructor(resolved_path, result_cache, override)

		local output, count = count_vararg(loaded(env_require))

		if count ~= 1 then
			error("module cannot be required, did not return exactly 1 value", 2) --
		end

		result_cache[resolved_path] = { value = output }

		return output
	end

	return warmluau_require
end

return {
	prepend = loader_prepend,
	new = require_constructor,
	override = override.new,
}


local fspath = require("@util/fspath")

local override = {}
local metatable = { __index = override }

type InternalIdentity = setmetatable<{
	children: { [string]: InternalIdentity },
	path: fspath.Nav,
	value: unknown?,
}, typeof(metatable)>

export type Identity<Children = { [string]: InternalIdentity }> = InternalIdentity & {
	children: Children,
}

local function constructor<Value, Children>(path: string, value: Value, children: Children?): Identity<Children>
	local self = setmetatable({
		children = children or {},
		path = fspath.to_nav(path),
		value = value,
	}, metatable)

	return self
end

return { new = constructor }


local pretty_print = require("@util/pretty_print")

export type Node = {
	name: string,
	filePaths: { string }?,
	children: { Node }?,
}

local function ASSERT<Value, Message>(value: Value, message: Message?)
	if value then
		return value
	end
	error(message, 2)
end

-- save repetitive allocations
local reused_nodes_list: { Node } = {}

local function validate<Value>(value: Value): Node
	local nodes = reused_nodes_list
	nodes[1] = value
	local nodes_count = 1

	local index = 0
	while index < nodes_count do
		index += 1
		local node = nodes[index]
		ASSERT(type(node) == "table", `node in rojo sourcemap was not a table: {pretty_print(value)}`)
		node = node :: { [string]: unknown }
		local name = node["name"]
		ASSERT(name, `node in rojo sourcemap does not have a name: {pretty_print(value)}`)
		ASSERT(type(name) == "string", `node's name in rojo sourcemap was not a string, node: {pretty_print(value)}`)
		local file_paths = node["filePaths"]
		if file_paths then
			ASSERT(
				type(file_paths) == "table",
				`node's filePaths in rojo sourcemap was not a table, node: {pretty_print(value)}`
			)
		end
		local children = node["children"]
		if children then
			ASSERT(
				type(children) == "table",
				`node's children in rojo sourcemap was not a table, node: {pretty_print(value)}`
			)
			children = children :: { Node }
			local children_count = #children
			table.move(children, 1, children_count, nodes_count + 1, nodes)
			nodes_count += children_count
		end
	end

	table.clear(nodes)

	return value
end

return {
	validate = validate,
}


local luau = require("@lute/luau")

export type UnaryOperator = "not" | "-" | "#"
export type BinaryOperator =
	| "^"
	| "*"
	| "/"
	| "//"
	| "%"
	| "+"
	| "-"
	| ".."
	| "<"
	| ">"
	| "<="
	| ">="
	| "~="
	| "=="
	| "and"
	| "or"

export type UnknownConstant = {
	read kind: "unknown",
	read data: nil,
}

export type NilConstant = {
	kind: "nil",
	data: nil,
}

export type BooleanConstant = {
	kind: "boolean",
	data: boolean,
}

export type NumberConstant = {
	kind: "number",
	data: number,
}

export type VectorConstant = {
	kind: "vector",
	data: vector,
}

export type StringConstant = {
	kind: "string",
	data: string,
}

export type TableConstant = {
	kind: "table",
	data: {
		record: { [ConstantData]: Constant }?,
		metatable: {
			__len: ((self: TableConstant) -> NumberConstant)?,
			__index: ((self: TableConstant, index: Constant) -> Constant)?,
			__call: ((self: boolean, ...Constant) -> ...Constant)?,
		}?,
		evaluate: (() -> luau.AstExpr)?,
	},
}

export type Constant =
	| UnknownConstant --
	| NilConstant
	| BooleanConstant
	| NumberConstant
	| VectorConstant
	| StringConstant
	| TableConstant

export type KnownConstant =
	| NilConstant --
	| BooleanConstant
	| NumberConstant
	| VectorConstant
	| StringConstant
	| TableConstant

export type SimpleConstant =
	| UnknownConstant --
	| NilConstant
	| BooleanConstant
	| NumberConstant
	| VectorConstant
	| StringConstant

export type ConstantData = index<SimpleConstant, "data">

export type Variable = {
	writes: number,
	init: Constant?,
	init_expr: luau.AstExpr?,
}

export type Tracked = {
	globals: { [string]: Variable },
	locals: { [luau.AstLocal]: Variable },
}

return nil