/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */

#include "C4Include.h"
#include "script/C4AulCompiler.h"

#include "script/C4Aul.h"
#include "script/C4AulParse.h"
#include "script/C4AulScriptFunc.h"
#include "script/C4ScriptHost.h"

#include <cinttypes>

#define C4AUL_Inherited     "inherited"
#define C4AUL_SafeInherited "_inherited"
#define C4AUL_DebugBreak    "__debugbreak"

static std::string FormatCodePosition(const C4ScriptHost *source_host, const char *pos, const C4ScriptHost *target_host = nullptr, const C4AulScriptFunc *func = nullptr)
{
	std::string s;
	if (func && func->GetFullName())
	{
		s += strprintf(" (in %s", func->GetFullName().getData());
		if (source_host && pos)
			s += ", ";
		else
			s += ")";
	}
	if (source_host && pos)
	{
		if (!func || !func->GetFullName())
			s += " (";

		int line = SGetLine(source_host->GetScript(), pos);
		int col = SLineGetCharacters(source_host->GetScript(), pos);

		s += strprintf("%s:%d:%d)",
			source_host->GetFilePath(),
			line, col
		);
	}
	if (target_host && source_host != target_host)
	{
		s += strprintf(" (as #appendto/#include to %s)", target_host->ScriptName.getData());
	}
	return s;
}

template<class... T>
static void Warn(const C4ScriptHost *target_host, const C4ScriptHost *host, const char *SPos, const C4AulScriptFunc *func, C4AulWarningId warning, T &&...args)
{
	if (!host)
	{
		// Without a script host, just fall back to the default settings
#define DIAG(id, msg, enabled) if (warning == C4AulWarningId::id && !enabled) return;
#include "C4AulWarnings.h"
#undef DIAG
	}
	else if (!host->IsWarningEnabled(SPos, warning))
	{
		return;
	}
	const char *msg = C4AulWarningMessages[static_cast<size_t>(warning)];
	std::string message = sizeof...(T) > 0 ? strprintf(msg, std::forward<T>(args)...) : msg;
	message += FormatCodePosition(host, SPos, target_host, func);

	message += " [";
	message += C4AulWarningIDs[static_cast<size_t>(warning)];
	message += ']';

	::ScriptEngine.GetErrorHandler()->OnWarning(message.c_str());
}

template<class... T>
static void Warn(const C4ScriptHost *target_host, const C4ScriptHost *host, const ::aul::ast::Node *n, const C4AulScriptFunc *func, C4AulWarningId warning, T &&...args)
{
	return Warn(target_host, host, n->loc, func, warning, std::forward<T>(args)...);
}
template<class... T>
static void Warn(const C4ScriptHost *target_host, const C4ScriptHost *host, const std::nullptr_t &, const C4AulScriptFunc *func, C4AulWarningId warning, T &&...args)
{
	return Warn(target_host, host, static_cast<const char*>(nullptr), func, warning, std::forward<T>(args)...);
}

template<class... T>
static C4AulParseError Error(const C4ScriptHost *target_host, const C4ScriptHost *host, const char *SPos, const C4AulScriptFunc *func, const char *msg, T &&...args)
{
	std::string message = sizeof...(T) > 0 ? strprintf(msg, std::forward<T>(args)...) : msg;

	message += FormatCodePosition(host, SPos, target_host, func);
	return C4AulParseError(static_cast<C4ScriptHost*>(nullptr), message.c_str());
}

template<class... T>
static C4AulParseError Error(const C4ScriptHost *target_host, const C4ScriptHost *host, const ::aul::ast::Node *n, const C4AulScriptFunc *func, const char *msg, T &&...args)
{
	return Error(target_host, host, n->loc, func, msg, std::forward<T>(args)...);
}
template<class... T>
static C4AulParseError Error(const C4ScriptHost *target_host, const C4ScriptHost *host, const std::nullptr_t &, const C4AulScriptFunc *func, const char *msg, T &&...args)
{
	return Error(target_host, host, static_cast<const char*>(nullptr), func, msg, std::forward<T>(args)...);
}

class C4AulCompiler::PreparseAstVisitor : public ::aul::DefaultRecursiveVisitor
{
	// target_host: The C4ScriptHost on which compilation is done
	C4ScriptHost *target_host = nullptr;
	// host: The C4ScriptHost where the script actually resides in
	C4ScriptHost *host = nullptr;
	// Fn: The C4AulScriptFunc that is currently getting parsed
	C4AulScriptFunc *Fn = nullptr;

public:
	PreparseAstVisitor(C4ScriptHost *host, C4ScriptHost *source_host, C4AulScriptFunc *func = nullptr) : target_host(host), host(source_host), Fn(func) {}
	explicit PreparseAstVisitor(C4AulScriptFunc *func) : Fn(func), target_host(func->pOrgScript), host(target_host) {}

	~PreparseAstVisitor() override = default;

	using DefaultRecursiveVisitor::visit;
	void visit(const ::aul::ast::RangeLoop *n) override;
	void visit(const ::aul::ast::VarDecl *n) override;
	void visit(const ::aul::ast::FunctionDecl *n) override;
	void visit(const ::aul::ast::CallExpr *n) override;
	void visit(const ::aul::ast::ParExpr *n) override;
	void visit(const ::aul::ast::AppendtoPragma *n) override;
	void visit(const ::aul::ast::IncludePragma *n) override;
	void visit(const ::aul::ast::Script *n) override;
};

class C4AulCompiler::CodegenAstVisitor : public ::aul::DefaultRecursiveVisitor
{
	C4AulScriptFunc *Fn = nullptr;
	// target_host: The C4ScriptHost on which compilation is done
	C4ScriptHost *target_host = nullptr;
	// host: The C4ScriptHost where the script actually resides in
	C4ScriptHost *host = nullptr;

	int32_t stack_height = 0;
	bool at_jump_target = false;

	struct Loop
	{
		explicit Loop(int stack_height) : stack_height(stack_height) {}

		int stack_height = 0;
		std::vector<int> continues;
		std::vector<int> breaks;

		enum class Control
		{
			Continue,
			Break
		};
	};

	std::stack<Loop> active_loops;

	// The type of the variable on top of the value stack. C4V_Any if unknown.
	C4V_Type type_of_stack_top = C4V_Any;
	
	constexpr static bool IsJump(C4AulBCCType t)
	{
		return t == AB_JUMP || t == AB_JUMPAND || t == AB_JUMPOR || t == AB_JUMPNNIL || t == AB_CONDN || t == AB_COND;
	}

	int AddJumpTarget();
	void AddJumpTo(const char *loc, C4AulBCCType type, int target);
	void UpdateJump(int jump, int target);
	void PushLoop();
	void PopLoop(int continue_target);
	void AddLoopControl(const char *loc, Loop::Control c);

	int AddVarAccess(const char *TokenSPos, C4AulBCCType eType, intptr_t varnum);
	int AddBCC(const char *TokenSPos, C4AulBCCType eType, intptr_t X = 0);
	int AddBCC(const char *SPos, const C4AulBCC &bcc);

	template<class T>
	void MaybePopValueOf(const std::unique_ptr<T> &n)
	{
		if (!n) return;
		if (!n->has_value()) return;
		AddBCC(n->loc, AB_STACK, -1);
	}

	static int GetStackValue(C4AulBCCType eType, intptr_t X);
	void RemoveLastBCC();
	C4AulBCC MakeSetter(const char *SPos, bool fLeaveValue);

	void HandleError(const C4AulError &e)
	{
		if (Fn)
		{
			AddBCC(nullptr, AB_ERR, (intptr_t)::Strings.RegString(e.what()));
		}
		if (target_host) // target_host may be nullptr for DirectExec scripts
		{
			target_host->Engine->ErrorHandler->OnError(e.what());
		}
	}

	template<class T>
	bool SafeVisit(const T &node)
	{
		// Swallows exceptions during evaluation of node. Use if you want to
		// keep doing syntax checks for subsequent children. (Generated code
		// will cause a runtime error if executed.)
		try
		{
			node->accept(this);
			return true;
		}
		catch (C4AulParseError &e)
		{
			HandleError(e);
			return false;
		}
	}

	class StackGuard
	{
		// Ensures that the Aul value stack ends up at the expected height
		CodegenAstVisitor *parent;
		const int32_t target_stack_height;
	public:
		explicit StackGuard(CodegenAstVisitor *parent, int32_t offset = 0) : parent(parent), target_stack_height(parent->stack_height + offset)
		{}
		~StackGuard()
		{
			assert(parent->stack_height == target_stack_height);
			if (parent->stack_height != target_stack_height)
			{
				parent->HandleError(Error(parent->target_host, parent->host, nullptr, parent->Fn, "internal error: value stack left unbalanced"));
				parent->AddBCC(nullptr, AB_STACK, target_stack_height - parent->stack_height);
			}
		}
	};

public:
	CodegenAstVisitor(C4ScriptHost *host, C4ScriptHost *source_host) : target_host(host), host(source_host) {}
	explicit CodegenAstVisitor(C4AulScriptFunc *func) : Fn(func), target_host(func->pOrgScript), host(target_host) {}

	~CodegenAstVisitor() override = default;

	using DefaultRecursiveVisitor::visit;
	void visit(const ::aul::ast::Noop *) override;
	void visit(const ::aul::ast::StringLit *n) override;
	void visit(const ::aul::ast::IntLit *n) override;
	void visit(const ::aul::ast::BoolLit *n) override;
	void visit(const ::aul::ast::ArrayLit *n) override;
	void visit(const ::aul::ast::ProplistLit *n) override;
	void visit(const ::aul::ast::NilLit *n) override;
	void visit(const ::aul::ast::ThisLit *n) override;
	void visit(const ::aul::ast::VarExpr *n) override;
	void visit(const ::aul::ast::UnOpExpr *n) override;
	void visit(const ::aul::ast::BinOpExpr *n) override;
	void visit(const ::aul::ast::AssignmentExpr *n) override;
	void visit(const ::aul::ast::SubscriptExpr *n) override;
	void visit(const ::aul::ast::SliceExpr *n) override;
	void visit(const ::aul::ast::CallExpr *n) override;
	void visit(const ::aul::ast::ParExpr *n) override;
	void visit(const ::aul::ast::Block *n) override;
	void visit(const ::aul::ast::Return *n) override;
	void visit(const ::aul::ast::ForLoop *n) override;
	void visit(const ::aul::ast::RangeLoop *n) override;
	void visit(const ::aul::ast::DoLoop *n) override;
	void visit(const ::aul::ast::WhileLoop *n) override;
	void visit(const ::aul::ast::Break *n) override;
	void visit(const ::aul::ast::Continue *n) override;
	void visit(const ::aul::ast::If *n) override;
	void visit(const ::aul::ast::VarDecl *n) override;
	void visit(const ::aul::ast::FunctionDecl *n) override;
	void visit(const ::aul::ast::FunctionExpr *n) override;
	void visit(const ::aul::ast::Script *n) override;

	template<class T>
	void EmitFunctionCode(const T *n)
	{
		// This dynamic_cast resolves the problem where we have a Function*
		// and want to emit code to it. All classes derived from Function
		// are also ultimately derived from Node, so this call is fine
		// without any additional checking.
		EmitFunctionCode(n, dynamic_cast<const ::aul::ast::Node*>(n));
	}

private:
	void EmitFunctionCode(const ::aul::ast::Function *f, const ::aul::ast::Node *n);
};

class C4AulCompiler::ConstexprEvaluator : public ::aul::AstVisitor
{
public:
	enum EvalFlag
	{
		// If this flag is set, ConstexprEvaluator will assume unset values
		// are nil. If it is not set, evaluation of unset values will send an
		// ExpressionNotConstant to the error handler.
		IgnoreUnset = 1<<0,
		// If this flag is set, ConstexprEvaluator will not send exceptions to
		// the error handler (so it doesn't report them twice: once from the
		// preparsing step, then again from the compile step).
		SuppressErrors = 1<<1
	};
	typedef int EvalFlags;

	// Evaluates constant AST subtrees and returns the final C4Value.
	// Flags ExpressionNotConstant if evaluation fails.
	static C4Value eval(C4ScriptHost *host, const ::aul::ast::Expr *e, EvalFlags flags = 0);
	static C4Value eval_static(C4ScriptHost *host, C4PropListStatic *parent, const std::string &parent_key, const ::aul::ast::Expr *e, EvalFlags flags = 0);

private:
	C4ScriptHost *host = nullptr;
	C4Value v;
	bool ignore_unset_values = false;
	bool quiet = false;

	struct ProplistMagic
	{
		bool active = false;
		C4PropListStatic *parent = nullptr;
		std::string key;

		ProplistMagic() = default;
		ProplistMagic(bool active, C4PropListStatic *parent, std::string key) : active(active), parent(parent), key(std::move(key)) {}
	} proplist_magic;

	explicit ConstexprEvaluator(C4ScriptHost *host) : host(host) {}

	template<typename... T>
	NORETURN void nonconst(const ::aul::ast::Node *n, const char *msg, T&&...args) const
	{
		throw ExpressionNotConstant(host, n, msg, std::forward<T>(args)...);
	}

	void AssertValueType(const C4Value &v, C4V_Type Type1, const char *opname, const ::aul::ast::Node *n)
	{
		// Typecheck parameter
		if (!v.CheckParConversion(Type1))
			throw Error(host, host, n, nullptr, R"(operator "%s": got %s, but expected %s)", opname, v.GetTypeName(), GetC4VName(Type1));
	}
public:
	class ExpressionNotConstant : public C4AulParseError
	{
	public:
		template<typename... T>
		ExpressionNotConstant(const C4ScriptHost *host, const ::aul::ast::Node *n, const char *reason, T&&...args) :
			C4AulParseError(Error(host, host, n, nullptr, reason, std::forward<T>(args)...)) {}
	};

	using AstVisitor::visit;
	void visit(const ::aul::ast::StringLit *n) override;
	void visit(const ::aul::ast::IntLit *n) override;
	void visit(const ::aul::ast::BoolLit *n) override;
	void visit(const ::aul::ast::ArrayLit *n) override;
	void visit(const ::aul::ast::ProplistLit *n) override;
	void visit(const ::aul::ast::NilLit *) override;
	void visit(const ::aul::ast::ThisLit *n) override;
	void visit(const ::aul::ast::VarExpr *n) override;
	void visit(const ::aul::ast::UnOpExpr *n) override;
	void visit(const ::aul::ast::BinOpExpr *n) override;
	void visit(const ::aul::ast::AssignmentExpr *n) override;
	void visit(const ::aul::ast::SubscriptExpr *n) override;
	void visit(const ::aul::ast::SliceExpr *n) override;
	void visit(const ::aul::ast::CallExpr *n) override;
	void visit(const ::aul::ast::FunctionExpr *n) override;
};

class C4AulCompiler::ConstantResolver : public ::aul::DefaultRecursiveVisitor
{
	C4ScriptHost *host;
	bool quiet = false;
	explicit ConstantResolver(C4ScriptHost *host) : host(host) {}

public:
	static void resolve_quiet(C4ScriptHost *host, const ::aul::ast::Script *script)
	{
		// Does the same as resolve, but doesn't emit errors/warnings
		// (because we'll emit them again later).
		ConstantResolver r(host);
		r.quiet = true;
		r.visit(script);
	}
	static void resolve(C4ScriptHost *host, const ::aul::ast::Script *script)
	{
		// We resolve constants *twice*; this allows people to create circular
		// references in proplists or arrays.
		// Unfortunately it also results in unexpected behaviour in code like
		// this:
		//     static const c1 = c2, c2 = c3, c3 = 1;
		// which will set c1 to nil, and both c2 and c3 to 1.
		// While this is unlikely to happen often, we should fix that so it
		// resolves all three constants to 1.
		ConstantResolver r(host);
		r.visit(script);
	}
	~ConstantResolver() override = default;

	using DefaultRecursiveVisitor::visit;
	void visit(const ::aul::ast::Script *n) override;
	void visit(const ::aul::ast::VarDecl *n) override;
};

void C4AulCompiler::Preparse(C4ScriptHost *host, C4ScriptHost *source_host, const ::aul::ast::Script *script)
{
	PreparseAstVisitor v(host, source_host);
	v.visit(script);

	ConstantResolver::resolve_quiet(host, script);
}

void C4AulCompiler::Compile(C4ScriptHost *host, C4ScriptHost *source_host, const ::aul::ast::Script *script)
{
	ConstantResolver::resolve(host, script);

	CodegenAstVisitor v(host, source_host);
	v.visit(script);
}

void C4AulCompiler::Compile(C4AulScriptFunc *func, const ::aul::ast::Function *def)
{
	{
		// Don't visit the whole definition here; that would create a new function
		// and we don't want that.
		PreparseAstVisitor v(func);
		def->body->accept(&v);
	}
	{
		CodegenAstVisitor v(func);
		v.EmitFunctionCode(def);
	}
}

#define ENSURE_COND(cond, failmsg) do { if (!(cond)) throw Error(target_host, host, n, Fn, failmsg); } while (0)

void C4AulCompiler::PreparseAstVisitor::visit(const ::aul::ast::RangeLoop *n)
{
	const char *cname = n->var.c_str();
	if (n->scoped_var)
	{
		Fn->VarNamed.AddName(cname);
	}
	else
	{
		// Loop variable not explicitly declared here. Look it up in
		// the function and warn if it hasn't been declared at all.
		if (Fn->VarNamed.GetItemNr(cname) == -1)
		{
			Warn(target_host, host, n, Fn, C4AulWarningId::implicit_range_loop_var_decl, cname);
			Fn->VarNamed.AddName(cname);
		}
	}
	DefaultRecursiveVisitor::visit(n);
}

void C4AulCompiler::PreparseAstVisitor::visit(const ::aul::ast::VarDecl *n)
{
	if (n->constant && n->scope != ::aul::ast::VarDecl::Scope::Global)
	{
		Warn(target_host, host, n, Fn, C4AulWarningId::non_global_var_is_never_const);
	}
	for (const auto &var : n->decls)
	{
		const char *cname = var.name.c_str();
		switch (n->scope)
		{
		case ::aul::ast::VarDecl::Scope::Func:
			{
				assert(Fn && "function-local var declaration outside of function");
				if (!Fn)
					throw Error(target_host, host, n, Fn, "internal error: function-local var declaration outside of function");

				if (target_host)
				{
					// if target_host is unset, we're parsing this func for direct execution,
					// in which case we don't want to warn about variable hiding.
					if (target_host->Engine->GlobalNamedNames.GetItemNr(cname) >= 0 || target_host->Engine->GlobalConstNames.GetItemNr(cname) >= 0)
						Warn(target_host, host, n, Fn, C4AulWarningId::variable_shadows_variable, "local variable", cname, "global variable");
					C4String *s = ::Strings.FindString(cname);
					if (s && target_host->GetPropList()->HasProperty(s))
						Warn(target_host, host, n, Fn, C4AulWarningId::variable_shadows_variable, "local variable", cname, "object-local variable");
					if (Fn->ParNamed.GetItemNr(cname) != -1)
					{
						// The parameter order of this warning is correct:
						// Aul looks up parameters before local variables, so
						// the parameter actually shadows the local variable.
						// This doesn't make a whole lot of sense and should
						// probably be changed.
						Warn(target_host, host, n, Fn, C4AulWarningId::variable_shadows_variable, "parameter", cname, "local variable");
					}
				}
				Fn->VarNamed.AddName(cname);
				break;
			}
		case ::aul::ast::VarDecl::Scope::Object:
			{
				if (host->Engine->GlobalNamedNames.GetItemNr(cname) >= 0 || host->Engine->GlobalConstNames.GetItemNr(cname) >= 0)
					Warn(target_host, host, n, Fn, C4AulWarningId::variable_shadows_variable, "object-local variable", cname, "global variable");
				C4String *s = ::Strings.RegString(cname);
				if (target_host->GetPropList()->HasProperty(s))
					Warn(target_host, host, n, Fn, C4AulWarningId::redeclaration, "object-local variable", cname);
				else
					target_host->GetPropList()->SetPropertyByS(s, C4VNull);
				break;
			}
		case ::aul::ast::VarDecl::Scope::Global:
			assert(!Fn && "global var declaration inside function");
			if (Fn)
				throw Error(target_host, host, n, Fn, "internal error: global var declaration inside function");

			if (host->Engine->GlobalNamedNames.GetItemNr(cname) >= 0 || host->Engine->GlobalConstNames.GetItemNr(cname) >= 0)
				Warn(target_host, host, n, Fn, C4AulWarningId::redeclaration, "global variable", cname);
			if (n->constant)
				host->Engine->GlobalConstNames.AddName(cname);
			else
				host->Engine->GlobalNamedNames.AddName(cname);
			break;
		}
	}

	if (n->scope == ::aul::ast::VarDecl::Scope::Func)
	{
		// only func-scoped variables can potentially have initializers we care
		// about in the pre-parsing stage: they may have calls that pass
		// unnamed parameters
		DefaultRecursiveVisitor::visit(n);
	}
}

void C4AulCompiler::PreparseAstVisitor::visit(const ::aul::ast::FunctionDecl *n)
{
	// create script fn
	C4PropListStatic *Parent = n->is_global ? target_host->Engine->GetPropList() : target_host->GetPropList();
	const char *cname = n->name.c_str();

	assert(!Fn);

	// Look up the overloaded function before adding the overloading one
	C4AulFunc *parent_func = Parent->GetFunc(cname);

	Fn = new C4AulScriptFunc(Parent, target_host, cname, n->loc);
	host->ownedFunctions.push_back(C4VFunction(Fn));
	for (const auto &param : n->params)
	{
		Fn->AddPar(param.name.c_str(), param.type);
	}
	if (n->has_unnamed_params)
		Fn->ParCount = C4AUL_MAX_Par;

	// Add function to def/engine
	Fn->SetOverloaded(parent_func);
	Parent->SetPropertyByS(Fn->Name, C4VFunction(Fn));

	try
	{
		DefaultRecursiveVisitor::visit(n);
		Fn = nullptr;
	}
	catch (...)
	{
		Fn = nullptr;
		throw;
	}
}

void C4AulCompiler::PreparseAstVisitor::visit(const ::aul::ast::CallExpr *n)
{
	if (n->append_unnamed_pars && Fn->ParCount != C4AUL_MAX_Par)
	{
		Fn->ParCount = C4AUL_MAX_Par;
	}
	DefaultRecursiveVisitor::visit(n);
}

void C4AulCompiler::PreparseAstVisitor::visit(const ::aul::ast::ParExpr *n)
{
	if (Fn->ParCount != C4AUL_MAX_Par)
	{
		Warn(target_host, host, n, Fn, C4AulWarningId::undeclared_varargs, "Par()");
		Fn->ParCount = C4AUL_MAX_Par;
	}
	DefaultRecursiveVisitor::visit(n);
}

void C4AulCompiler::PreparseAstVisitor::visit(const ::aul::ast::AppendtoPragma *n)
{
	if (n->what.empty())
		host->Appends.emplace_back("*");
	else
		host->Appends.emplace_back(n->what.c_str());
}

void C4AulCompiler::PreparseAstVisitor::visit(const ::aul::ast::IncludePragma *n)
{
	host->Includes.emplace_back(n->what.c_str());
}

void C4AulCompiler::PreparseAstVisitor::visit(const::aul::ast::Script * n)
{
	for (const auto &d : n->declarations)
	{
		try
		{
			d->accept(this);
		}
		catch (C4AulParseError &e)
		{
			target_host->Engine->GetErrorHandler()->OnError(e.what());
		}
	}
}

int C4AulCompiler::CodegenAstVisitor::GetStackValue(C4AulBCCType eType, intptr_t X)
{
	switch (eType)
	{
	case AB_INT:
	case AB_BOOL:
	case AB_STRING:
	case AB_CPROPLIST:
	case AB_CARRAY:
	case AB_CFUNCTION:
	case AB_NIL:
	case AB_LOCALN:
	case AB_GLOBALN:
	case AB_DUP:
	case AB_DUP_CONTEXT:
	case AB_THIS:
		return 1;

	case AB_Pow:
	case AB_Div:
	case AB_Mul:
	case AB_Mod:
	case AB_Sub:
	case AB_Sum:
	case AB_LeftShift:
	case AB_RightShift:
	case AB_LessThan:
	case AB_LessThanEqual:
	case AB_GreaterThan:
	case AB_GreaterThanEqual:
	case AB_Equal:
	case AB_NotEqual:
	case AB_BitAnd:
	case AB_BitXOr:
	case AB_BitOr:
	case AB_PROP_SET:
	case AB_ARRAYA:
	case AB_CONDN:
	case AB_COND:
	case AB_POP_TO:
	case AB_RETURN:
		// JUMPAND/JUMPOR/JUMPNNIL are special: They either jump over instructions adding one to the stack
		// or decrement the stack. Thus, for stack counting purposes, they decrement.
	case AB_JUMPAND:
	case AB_JUMPOR:
	case AB_JUMPNNIL:
		return -1;

	case AB_FUNC:
		return -reinterpret_cast<C4AulFunc *>(X)->GetParCount() + 1;

	case AB_CALL:
	case AB_CALLFS:
		return -C4AUL_MAX_Par;

	case AB_STACK_SET:
	case AB_LOCALN_SET:
	case AB_PROP:
	case AB_GLOBALN_SET:
	case AB_Inc:
	case AB_Dec:
	case AB_BitNot:
	case AB_Not:
	case AB_Neg:
	case AB_PAR:
	case AB_FOREACH_NEXT:
	case AB_ERR:
	case AB_EOFN:
	case AB_JUMP:
	case AB_DEBUG:
		return 0;

	case AB_STACK:
		return X;

	case AB_NEW_ARRAY:
		return -X + 1;

	case AB_NEW_PROPLIST:
		return -X * 2 + 1;

	case AB_ARRAYA_SET:
	case AB_ARRAY_SLICE:
		return -2;

	case AB_ARRAY_SLICE_SET:
		return -3;
	}
	assert(0 && "GetStackValue: unexpected bytecode not handled");
	return 0;
}

int C4AulCompiler::CodegenAstVisitor::AddVarAccess(const char *TokenSPos, C4AulBCCType eType, intptr_t varnum)
{
	return AddBCC(TokenSPos, eType, 1 + varnum - (stack_height + Fn->VarNamed.iSize));
}

int C4AulCompiler::CodegenAstVisitor::AddBCC(const char *TokenSPos, C4AulBCCType eType, intptr_t X)
{
	// Track stack size
	stack_height += GetStackValue(eType, X);

	// Use stack operation instead of 0-Any (enable optimization)
	if (eType == AB_NIL)
	{
		eType = AB_STACK;
		X = 1;
	}

	assert(eType != AB_STACK || X != 0);

	// Join checks only if it's not a jump target
	if (!at_jump_target && Fn->GetLastCode())
	{
		C4AulBCC *pCPos1 = Fn->GetLastCode();

		// Skip noop stack operation
		if (eType == AB_STACK && X == 0)
		{
			return Fn->GetCodePos() - 1;
		}

		// Join together stack operations
		if (eType == AB_STACK && pCPos1->bccType == AB_STACK &&
			(X <= 0 || pCPos1->Par.i >= 0))
		{
			pCPos1->Par.i += X;
			// Empty? Remove it. This relies on the parser not issuing
			// multiple negative stack operations consecutively, as
			// that could result in removing a jump target bytecode.
			if (!pCPos1->Par.i)
				Fn->RemoveLastBCC();
			return Fn->GetCodePos() - 1;
		}

		// Prune unneeded Incs / Decs
		if (eType == AB_STACK && X < 0 && (pCPos1->bccType == AB_Inc || pCPos1->bccType == AB_Dec))
		{
			if (!pCPos1->Par.X)
			{
				pCPos1->bccType = eType;
				pCPos1->Par.i = X;
				return Fn->GetCodePos() - 1;
			}
			else
			{
				// If it was a result modifier, we can safely remove it knowing that it was neither
				// the first chunk nor a jump target. We can therefore apply additional optimizations.
				Fn->RemoveLastBCC();
				pCPos1--;
			}
		}

		// Join STACK_SET + STACK -1 to POP_TO (equivalent)
		if (eType == AB_STACK && X == -1 && pCPos1->bccType == AB_STACK_SET)
		{
			pCPos1->bccType = AB_POP_TO;
			return Fn->GetCodePos() - 1;
		}

		// Join POP_TO + DUP to AB_STACK_SET if both target the same slot
		if (eType == AB_DUP && pCPos1->bccType == AB_POP_TO && X == pCPos1->Par.i + 1)
		{
			pCPos1->bccType = AB_STACK_SET;
			return Fn->GetCodePos() - 1;
		}

		// Reduce some constructs like SUM + INT 1 to INC or DEC
		if ((eType == AB_Sum || eType == AB_Sub) &&
			pCPos1->bccType == AB_INT &&
			(pCPos1->Par.i == 1 || pCPos1->Par.i == -1))
		{
			if ((pCPos1->Par.i > 0) == (eType == AB_Sum))
				pCPos1->bccType = AB_Inc;
			else
				pCPos1->bccType = AB_Dec;
			pCPos1->Par.i = X;
			return Fn->GetCodePos() - 1;
		}

		// Reduce Not + CONDN to COND, Not + COND to CONDN
		if ((eType == AB_CONDN || eType == AB_COND) && pCPos1->bccType == AB_Not)
		{
			pCPos1->bccType = eType == AB_CONDN ? AB_COND : AB_CONDN;
			pCPos1->Par.i = X + 1;
			return Fn->GetCodePos() - 1;
		}

		// Join AB_STRING + AB_ARRAYA to AB_PROP
		if (eType == AB_ARRAYA && pCPos1->bccType == AB_STRING)
		{
			pCPos1->bccType = AB_PROP;
			return Fn->GetCodePos() - 1;
		}

		// Join AB_INT + AB_Neg to AB_INT
		if (eType == AB_Neg && pCPos1->bccType == AB_INT)
		{
			pCPos1->Par.i *= -1;
			return Fn->GetCodePos() - 1;
		}
	}

	// Add
	Fn->AddBCC(eType, X, TokenSPos);

	// Reset jump flag
	at_jump_target = false;

	return Fn->GetCodePos() - 1;
}

void C4AulCompiler::CodegenAstVisitor::RemoveLastBCC()
{
	// Security: This is unsafe on anything that might get optimized away
	C4AulBCC *pBCC = Fn->GetLastCode();
	assert(pBCC->bccType != AB_STACK && pBCC->bccType != AB_STACK_SET && pBCC->bccType != AB_POP_TO);
	// Correct stack
	stack_height -= GetStackValue(pBCC->bccType, pBCC->Par.X);
	// Remove
	Fn->RemoveLastBCC();
}

int C4AulCompiler::CodegenAstVisitor::AddBCC(const char *SPos, const C4AulBCC &bcc)
{
	return AddBCC(SPos, bcc.bccType, bcc.Par.X);
}

C4AulBCC C4AulCompiler::CodegenAstVisitor::MakeSetter(const char *SPos, bool fLeaveValue)
{
	assert(Fn);
	C4AulBCC Value = *(Fn->GetLastCode()), Setter = Value;
	// Check type
	switch (Value.bccType)
	{
	case AB_ARRAYA: Setter.bccType = AB_ARRAYA_SET; break;
	case AB_ARRAY_SLICE: Setter.bccType = AB_ARRAY_SLICE_SET; break;
	case AB_DUP:
		Setter.bccType = AB_STACK_SET;
		// the setter additionally has the new value on the stack
		--Setter.Par.i;
		break;
	case AB_STACK_SET: Setter.bccType = AB_STACK_SET; break;
	case AB_LOCALN:
		Setter.bccType = AB_LOCALN_SET;
		break;
	case AB_PROP:
		Setter.bccType = AB_PROP_SET;
		break;
	case AB_GLOBALN: Setter.bccType = AB_GLOBALN_SET; break;
	default:
		throw Error(target_host, host, SPos, Fn, "assignment to a constant");
	}
	// If the new value is produced using the old one, the parameters to get the old one need to be duplicated.
	// Otherwise, the setter can just use the parameters originally meant for the getter.
	// All getters push one value, so the parameter count is one more than the values they pop from the stack.
	int iParCount = 1 - GetStackValue(Value.bccType, Value.Par.X);
	if (Value.bccType == AB_STACK_SET)
	{
		// STACK_SET has a side effect, so it can't be simply removed.
		// Discard the unused value the usual way instead.
		if (!fLeaveValue)
			AddBCC(SPos, AB_STACK, -1);
		// The original parameter isn't needed anymore, since in contrast to the other getters
		// it does not indicate a position.
		iParCount = 0;
	}
	else if (!fLeaveValue || iParCount)
	{
		RemoveLastBCC();
		at_jump_target = true; // In case the original BCC was a jump target
	}
	if (fLeaveValue && iParCount)
	{
		for (int i = 0; i < iParCount; i++)
			AddBCC(SPos, AB_DUP, 1 - iParCount);
		// Finally re-add original BCC
		AddBCC(SPos, Value.bccType, Value.Par.X);
	}
	// Done. The returned BCC should be added later once the value to be set was pushed on top.
	assert(iParCount == -GetStackValue(Setter.bccType, Setter.Par.X));
	return Setter;
}

int C4AulCompiler::CodegenAstVisitor::AddJumpTarget()
{
	assert(Fn && "Jump target outside of function");
	if (!Fn)
		throw C4AulParseError(host, "internal error: jump target outside of function");

	at_jump_target = true;
	return Fn->GetCodePos();
}

void C4AulCompiler::CodegenAstVisitor::UpdateJump(int jump, int target)
{
	C4AulBCC *code = Fn->GetCodeByPos(jump);
	assert(IsJump(code->bccType));
	code->Par.i = target - jump;
}

void C4AulCompiler::CodegenAstVisitor::AddJumpTo(const char *loc, C4AulBCCType type, int target)
{
	AddBCC(loc, type, target - Fn->GetCodePos());
}

void C4AulCompiler::CodegenAstVisitor::PushLoop()
{
	active_loops.emplace(stack_height);
}

void C4AulCompiler::CodegenAstVisitor::PopLoop(int continue_target)
{
	assert(!active_loops.empty());
	assert(stack_height == active_loops.top().stack_height);
	// Update all loop control jumps
	const auto &loop = active_loops.top();
	for (auto &c : loop.continues)
		UpdateJump(c, continue_target);
	int loop_exit = AddJumpTarget();
	for (auto &b : loop.breaks)
		UpdateJump(b, loop_exit);
	active_loops.pop();
}

void C4AulCompiler::CodegenAstVisitor::AddLoopControl(const char *loc, Loop::Control c)
{
	assert(!active_loops.empty());
	if (active_loops.empty())
		throw C4AulParseError(host, "internal error: loop control code emitted outside of loop");
	// Clear stack
	assert(active_loops.top().stack_height == stack_height);
	if (active_loops.top().stack_height - stack_height > 0)
		AddBCC(loc, AB_STACK, active_loops.top().stack_height - stack_height);
	int jump = AddBCC(loc, AB_JUMP, 0);
	switch (c)
	{
	case Loop::Control::Continue:
		active_loops.top().continues.push_back(jump);
		break;
	case Loop::Control::Break:
		active_loops.top().breaks.push_back(jump);
		break;
	}
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::Noop *) {}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::StringLit *n)
{
	StackGuard g(this, 1);
	AddBCC(n->loc, AB_STRING, (intptr_t)::Strings.RegString(n->value.c_str()));
	type_of_stack_top = C4V_String;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::IntLit *n)
{
	StackGuard g(this, 1);
	AddBCC(n->loc, AB_INT, n->value);
	type_of_stack_top = C4V_Int;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::BoolLit *n)
{
	StackGuard g(this, 1);
	AddBCC(n->loc, AB_BOOL, n->value);
	type_of_stack_top = C4V_Bool;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::ArrayLit *n)
{
	StackGuard g(this, 1);
	for (const auto &e : n->values)
	{
		SafeVisit(e);
	}
	AddBCC(n->loc, AB_NEW_ARRAY, n->values.size());
	type_of_stack_top = C4V_Array;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::ProplistLit *n)
{
	StackGuard g(this, 1);
	for (const auto &e : n->values)
	{
		StackGuard g(this, 2);
		AddBCC(n->loc, AB_STRING, (intptr_t)::Strings.RegString(e.first.c_str()));
		SafeVisit(e.second);
	}
	AddBCC(n->loc, AB_NEW_PROPLIST, n->values.size());
	type_of_stack_top = C4V_PropList;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::NilLit *n)
{
	StackGuard g(this, 1);
	AddBCC(n->loc, AB_NIL);
	type_of_stack_top = C4V_Nil;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::ThisLit *n)
{
	StackGuard g(this, 1);
	AddBCC(n->loc, AB_THIS);
	type_of_stack_top = C4V_PropList;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::VarExpr *n)
{
	StackGuard g(this, 1);
	assert(Fn);
	C4Value dummy;
	const char *cname = n->identifier.c_str();
	C4String *interned = ::Strings.FindString(cname);

	// Reset known type of top of value stack so we don't keep the old one around
	type_of_stack_top = C4V_Any;

	// Lookup order: Parameters > var > local > global > global const
	// Why parameters are considered before function-scoped variables
	// you ask? I've no idea, but that's how it was before I started
	// changing things.
	// NOTE: If you change this, remember to also change the warning
	// (variable_shadows_variable) in PreparseAstVisitor.
	if (Fn->ParNamed.GetItemNr(cname) != -1)
	{
		int pos = Fn->ParNamed.GetItemNr(cname);
		AddVarAccess(n->loc, AB_DUP, -Fn->GetParCount() + pos);
		type_of_stack_top = Fn->GetParType()[pos];
	}
	else if (Fn->VarNamed.GetItemNr(cname) != -1)
	{
		AddVarAccess(n->loc, AB_DUP, Fn->VarNamed.GetItemNr(cname));
	}
	// Can't use Fn->Parent->HasProperty here because that only returns true
	// for immediate properties, while we also want to interrogate prototypes
	else if (Fn->Parent && interned && Fn->Parent->GetPropertyByS(interned, &dummy))
	{
		AddBCC(n->loc, AB_LOCALN, (intptr_t)interned);
	}
	else if (ScriptEngine.GlobalNamedNames.GetItemNr(cname) != -1)
	{
		AddBCC(n->loc, AB_GLOBALN, ScriptEngine.GlobalNamedNames.GetItemNr(cname));
	}
	else if (ScriptEngine.GlobalConstNames.GetItemNr(cname) != -1)
	{
		C4Value v;
		ENSURE_COND(ScriptEngine.GetGlobalConstant(cname, &v), "internal error: global constant not retrievable");
		switch (v.GetType())
		{
		case C4V_Nil:
			AddBCC(n->loc, AB_NIL);
			break;
		case C4V_Int:
			AddBCC(n->loc, AB_INT, v._getInt());
			break;
		case C4V_Bool:
			AddBCC(n->loc, AB_BOOL, v._getBool());
			break;
		case C4V_PropList:
			AddBCC(n->loc, AB_CPROPLIST, reinterpret_cast<intptr_t>(v._getPropList()));
			break;
		case C4V_String:
			AddBCC(n->loc, AB_STRING, reinterpret_cast<intptr_t>(v._getStr()));
			break;
		case C4V_Array:
			AddBCC(n->loc, AB_CARRAY, reinterpret_cast<intptr_t>(v._getArray()));
			break;
		case C4V_Function:
			AddBCC(n->loc, AB_CFUNCTION, reinterpret_cast<intptr_t>(v._getFunction()));
		default:
			AddBCC(n->loc, AB_NIL);
			throw Error(target_host, host, n, Fn, "internal error: global constant of unexpected type: %s (of type %s)", cname, v.GetTypeName());
		}
		type_of_stack_top = v.GetType();
	}
	else
	{
		AddBCC(n->loc, AB_NIL);
		throw Error(target_host, host, n, Fn, "symbol not found in any symbol table: %s", cname);
	}
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::UnOpExpr *n)
{
	StackGuard g(this, 1);

	n->operand->accept(this);
	const auto &op = C4ScriptOpMap[n->op];
	if (op.Changer)
	{
		C4AulBCC setter = MakeSetter(n->loc, true);
		AddBCC(n->loc, op.Code, 0);
		AddBCC(n->loc, setter);
		// On postfix inc/dec, regenerate the previous value
		if (op.Postfix && (op.Code == AB_Inc || op.Code == AB_Dec))
		{
			AddBCC(n->loc, op.Code == AB_Inc ? AB_Dec : AB_Inc, 1);
		}
	}
	else
	{
		AddBCC(n->loc, op.Code);
	}
	type_of_stack_top = op.RetType;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::BinOpExpr *n)
{
	StackGuard g(this, 1);
	
	SafeVisit(n->lhs);

	const auto &op = C4ScriptOpMap[n->op];
	if (op.Code == AB_JUMPAND || op.Code == AB_JUMPOR || op.Code == AB_JUMPNNIL)
	{
		// Short-circuiting operators. These are slightly more complex
		// because we don't want to evaluate their rhs operand when the
		// lhs one already decided the result
		int jump = AddBCC(n->loc, op.Code);
		SafeVisit(n->rhs);
		UpdateJump(jump, AddJumpTarget());
	}
	else if (op.Changer)
	{
		try
		{
			C4AulBCC setter = MakeSetter(n->loc, true);
			SafeVisit(n->rhs);
			AddBCC(n->loc, op.Code);
			AddBCC(n->loc, setter);
		}
		catch (C4AulParseError &e)
		{
			HandleError(e);
		}
	}
	else
	{
		SafeVisit(n->rhs);
		AddBCC(n->loc, op.Code, 0);
	}

	type_of_stack_top = op.RetType;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::AssignmentExpr *n)
{
	StackGuard g(this, 1);
	SafeVisit(n->lhs);
	try
	{
		C4AulBCC setter = MakeSetter(n->loc, false);
		SafeVisit(n->rhs);
		AddBCC(n->loc, setter);
	}
	catch (C4AulParseError &e)
	{
		HandleError(e);
	}
	// Assignment does not change the type of the variable
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::SubscriptExpr *n)
{
	StackGuard g(this, 1);
	SafeVisit(n->object);
	SafeVisit(n->index);
	AddBCC(n->loc, AB_ARRAYA);

	// FIXME: Check if the subscripted object is a literal and if so, retrieve type
	type_of_stack_top = C4V_Any;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::SliceExpr *n)
{
	StackGuard g(this, 1);
	SafeVisit(n->object);
	SafeVisit(n->start);
	SafeVisit(n->end);
	AddBCC(n->loc, AB_ARRAY_SLICE);

	type_of_stack_top = C4V_Array;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::CallExpr *n)
{
	const char *cname = n->callee.c_str();

	if (n->callee == C4AUL_DebugBreak)
	{
		if (n->context)
			throw Error(target_host, host, n, Fn, R"("%s" can't be called in a different context)", cname);
		if (!n->args.empty())
			throw Error(target_host, host, n, Fn, R"("%s" must not have any arguments)", cname);

		AddBCC(n->loc, AB_DEBUG);
		// Add a pseudo-nil to keep the stack balanced
		AddBCC(n->loc, AB_NIL);
		type_of_stack_top = C4V_Nil;
		return;
	}

	if (n->callee == C4AUL_Inherited || n->callee == C4AUL_SafeInherited)
	{
		// inherited can only be called within the same context
		if (n->context)
		{
			throw Error(target_host, host, n, Fn, R"("%s" can't be called in a different context)", cname);
		}
	}

	if (n->callee == C4AUL_Inherited && !Fn->OwnerOverloaded)
	{
		throw Error(target_host, host, n, Fn, "inherited function not found (use " C4AUL_SafeInherited " to disable this message)");
	}

	const auto pre_call_stack = stack_height;

	if (n->context)
		SafeVisit(n->context);

	std::vector<C4V_Type> known_par_types;
	known_par_types.reserve(n->args.size());

	for (const auto &arg : n->args)
	{
		SafeVisit(arg);
		known_par_types.push_back(type_of_stack_top);
	}

	C4AulFunc *callee = nullptr;

	// Special handling for the overload chain
	if (n->callee == C4AUL_Inherited || n->callee == C4AUL_SafeInherited)
	{
		callee = Fn->OwnerOverloaded;
	}

	size_t fn_argc = C4AUL_MAX_Par;
	if (!n->context)
	{
		// if this is a function without explicit context, we resolve it
		if (!callee)
			callee = Fn->Parent->GetFunc(cname);
		if (!callee && target_host)
			callee = target_host->Engine->GetFunc(cname);

		if (callee)
		{
			fn_argc = callee->GetParCount();
		}
		else
		{
			// pop all args off the stack
			if (!n->args.empty())
				AddBCC(n->loc, AB_STACK, -(intptr_t)n->args.size());
			// and "return" nil
			AddBCC(n->loc, AB_NIL);
			type_of_stack_top = C4V_Nil;

			if (n->callee != C4AUL_SafeInherited)
			{
				HandleError(Error(target_host, host, n, Fn, "called function not found: %s", cname));
			}
			return;
		}
	}

	if (n->args.size() > fn_argc)
	{
		// Pop off any args that are over the limit
		Warn(target_host, host, n->args[fn_argc].get(), Fn, C4AulWarningId::arg_count_mismatch,
			cname, (unsigned)n->args.size(), fn_argc);
		AddBCC(n->loc, AB_STACK, fn_argc - n->args.size());
	}
	else if (n->args.size() < fn_argc)
	{
		if (n->append_unnamed_pars)
		{
			assert(Fn->GetParCount() == C4AUL_MAX_Par);
			int missing_par_count = fn_argc - n->args.size();
			int available_par_count = Fn->GetParCount() - Fn->ParNamed.iSize;
			for (int i = 0; i < std::min(missing_par_count, available_par_count); ++i)
			{
				AddVarAccess(n->loc, AB_DUP, -Fn->GetParCount() + Fn->ParNamed.iSize + i);
			}
			// Fill up remaining, unsettable parameters with nil
			if (available_par_count < missing_par_count)
				AddBCC(n->loc, AB_STACK, missing_par_count - available_par_count);
		}
		else if (fn_argc > n->args.size())
		{
			// Add nil for each missing parameter
			AddBCC(n->loc, AB_STACK, fn_argc - n->args.size());
		}
	}

	// Check passed parameters for this call (as far as possible)
	std::vector<C4V_Type> expected_par_types;
	if (n->context)
	{
		AddBCC(n->loc, n->safe_call ? AB_CALLFS : AB_CALL, (intptr_t)::Strings.RegString(cname));
		// Since we don't know the context in which this call will happen at
		// runtime, we'll check whether all available functions with the same
		// name agree on their parameters.
		const C4AulFunc *candidate = target_host ? target_host->Engine->GetFirstFunc(cname) : nullptr;
		if (candidate)
		{
			expected_par_types.assign(candidate->GetParType(), candidate->GetParType() + candidate->GetParCount());
			while ((candidate = target_host->Engine->GetNextSNFunc(candidate)) != nullptr)
			{
				if (candidate->GetParCount() > expected_par_types.size())
				{
					expected_par_types.resize(candidate->GetParCount(), C4V_Any);
				}
				for (size_t i = 0; i < expected_par_types.size(); ++i)
				{
					C4V_Type a = expected_par_types[i];
					C4V_Type b = candidate->GetParType()[i];
					// If we can convert one of the types into the other
					// without a warning, use the wider one
					bool implicit_a_to_b = !C4Value::WarnAboutConversion(a, b);
					bool implicit_b_to_a = !C4Value::WarnAboutConversion(b, a);
					if (implicit_a_to_b && !implicit_b_to_a)
						expected_par_types[i] = b;
					else if (implicit_b_to_a && !implicit_a_to_b)
						expected_par_types[i] = a;
					// but if we can convert neither of the types into the
					// other, give up and assume the user will do the right
					// thing
					else if (!implicit_a_to_b && !implicit_b_to_a)
						expected_par_types[i] = C4V_Any;
				}
			}
		}
		type_of_stack_top = C4V_Any;
	}
	else
	{
		assert(callee);
		AddBCC(n->loc, AB_FUNC, (intptr_t)callee);
		expected_par_types.assign(callee->GetParType(), callee->GetParType() + callee->GetParCount());
		type_of_stack_top = callee->GetRetType();
	}

	// Check parameters
	for (size_t i = 0; i < std::min(known_par_types.size(), expected_par_types.size()); ++i)
	{
		C4V_Type from = known_par_types[i];
		C4V_Type to = expected_par_types[i];
		if (C4Value::WarnAboutConversion(from, to))
		{
			Warn(target_host, host, n->args[i].get(), Fn, C4AulWarningId::arg_type_mismatch, (unsigned)i, cname, GetC4VName(from), GetC4VName(to));
		}
	}

	// We leave one value (the return value) on the stack
	assert(pre_call_stack + 1 == stack_height);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::ParExpr *n)
{
	StackGuard g(this, 1);

	SafeVisit(n->arg);
	AddBCC(n->loc, AB_PAR);
	type_of_stack_top = C4V_Any;
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::Block *n)
{
	for (const auto &s : n->children)
	{
		StackGuard g(this, 0);
		if (SafeVisit(s))
		{
			// If the statement has left a stack value, pop it off
			MaybePopValueOf(s);
		}
	}
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::Return *n)
{
	StackGuard g(this, 0);

	SafeVisit(n->value);
	AddBCC(n->loc, AB_RETURN);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::ForLoop *n)
{
	// Bytecode arranged like this:
	//        initializer
	//  cond: condition
	//        CONDN exit
	//  body: body
	//  incr: incrementor
	//        JUMP cond
	//  exit:
	//
	// continue jumps to incr
	// break jumps to exit

	if (n->init)
	{
		if (SafeVisit(n->init))
			MaybePopValueOf(n->init);
	}
	int cond = -1, condition_jump = -1;
	PushLoop();
	if (n->cond)
	{
		cond = AddJumpTarget();
		SafeVisit(n->cond);
		active_loops.top().breaks.push_back(AddBCC(n->cond->loc, AB_CONDN));
	}

	int body = AddJumpTarget();
	if (!n->cond)
		cond = body;
	if (SafeVisit(n->body))
		MaybePopValueOf(n->body);

	int incr = -1;
	if (n->incr)
	{
		incr = AddJumpTarget();
		if (SafeVisit(n->incr))
			MaybePopValueOf(n->incr);
	}
	else
	{
		// If no incrementor exists, just jump straight to the condition
		incr = cond;
	}
	// start the next iteration of the loop
	AddJumpTo(n->loc, AB_JUMP, cond);
	PopLoop(incr);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::RangeLoop *n)
{
	// Bytecode arranged like this:
	//        condition (aka iterated array)
	//        INT 0 (the loop index variable)
	//  cond: FOREACH_NEXT
	//        JUMP exit
	//  body: body
	//        JUMP cond
	//  exit: STACK -2 (to clean the iteration variables)
	//
	// continue jumps to cond
	// break jumps to exit

	const char *cname = n->var.c_str();
	int var_id = Fn->VarNamed.GetItemNr(cname);
	assert(var_id != -1 && "CodegenAstVisitor: unable to find variable in foreach");
	if (var_id == -1)
		throw Error(target_host, host, n, Fn, "internal error: unable to find variable in foreach: %s", cname);
	// Emit code for array
	SafeVisit(n->cond);
	// Emit code for iteration
	AddBCC(n->loc, AB_INT, 0);
	int cond = AddJumpTarget();
	PushLoop();
	AddVarAccess(n->loc, AB_FOREACH_NEXT, var_id);
	AddLoopControl(n->loc, Loop::Control::Break); // Will be skipped by AB_FOREACH_NEXT as long as more entries exist

	// Emit body
	if (SafeVisit(n->body))
		MaybePopValueOf(n->body);
	// continue starts the next iteration of the loop
	AddLoopControl(n->loc, Loop::Control::Continue);
	PopLoop(cond);
	// Pop off iterator and array
	AddBCC(n->loc, AB_STACK, -2);
}

void C4AulCompiler::CodegenAstVisitor::EmitFunctionCode(const ::aul::ast::Function *f, const ::aul::ast::Node *n)
{
	assert(Fn != nullptr);

	Fn->ClearCode();

	// Reserve var stack space
	if (Fn->VarNamed.iSize > 0)
		AddBCC(n->loc, AB_STACK, Fn->VarNamed.iSize);
	stack_height = 0;

	try
	{
		f->body->accept(this);
	}
	catch (C4AulParseError &e)
	{
		AddBCC(nullptr, AB_ERR, (intptr_t)::Strings.RegString(e.what()));
		throw;
	}

	if (f->body->children.empty() || !dynamic_cast<::aul::ast::Return*>(f->body->children.rbegin()->get()))
	{
		// If the last statement isn't a return, add one to the byte
		// code. We're not doing CFA because the worst thing that might
		// happen is we insert two instructions that never get executed.
		AddBCC(n->loc, AB_NIL);
		AddBCC(n->loc, AB_RETURN);
	}
	Fn->DumpByteCode();
	// This instruction should never be reached but we'll add it just in
	// case.
	AddBCC(n->loc, AB_EOFN);
	assert(stack_height == 0);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::DoLoop *n)
{
	int body = AddJumpTarget();
	PushLoop();
	if (SafeVisit(n->body))
		MaybePopValueOf(n->body);
	int cond = AddJumpTarget();
	SafeVisit(n->cond);
	AddJumpTo(n->loc, AB_COND, body);
	PopLoop(cond);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::WhileLoop *n)
{
	int cond = AddJumpTarget();
	PushLoop();
	SafeVisit(n->cond);
	active_loops.top().breaks.push_back(AddBCC(n->cond->loc, AB_CONDN));
	if (SafeVisit(n->body))
		MaybePopValueOf(n->body);
	// continue starts the next iteration of the loop
	AddLoopControl(n->loc, Loop::Control::Continue);
	PopLoop(cond);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::Break *n)
{
	ENSURE_COND(!active_loops.empty(), "'break' outside loop");
	AddLoopControl(n->loc, Loop::Control::Break);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::Continue *n)
{
	ENSURE_COND(!active_loops.empty(), "'continue' outside loop");
	AddLoopControl(n->loc, Loop::Control::Continue);
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::If *n)
{
	SafeVisit(n->cond);
	int jump = AddBCC(n->loc, AB_CONDN);
	// Warn if we're controlling a no-op ("if (...);")
	if (dynamic_cast<::aul::ast::Noop*>(n->iftrue.get()))
	{
		Warn(target_host, host, n->iftrue->loc, Fn, C4AulWarningId::empty_if);
	}
	if (SafeVisit(n->iftrue))
		MaybePopValueOf(n->iftrue);

	if (dynamic_cast<::aul::ast::Noop*>(n->iffalse.get()))
	{
		Warn(target_host, host, n->iffalse->loc, Fn, C4AulWarningId::empty_if);
	}

	if (n->iffalse)
	{
		int jumpout = AddBCC(n->loc, AB_JUMP);
		UpdateJump(jump, AddJumpTarget());
		jump = jumpout;
		if (SafeVisit(n->iffalse))
			MaybePopValueOf(n->iffalse);
	}
	UpdateJump(jump, AddJumpTarget());
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::VarDecl *n)
{
	for (const auto &dec : n->decls)
	{
		const char *cname = dec.name.c_str();
		switch (n->scope)
		{
		case ::aul::ast::VarDecl::Scope::Func:
			if (dec.init)
			{
				// Emit code for the initializer
				SafeVisit(dec.init);
				int var_idx = Fn->VarNamed.GetItemNr(cname);
				assert(var_idx >= 0 && "CodegenAstVisitor: var not found in variable table");
				if (var_idx < 0)
				{
					AddBCC(n->loc, AB_STACK, -1);
					throw Error(target_host, host, n, Fn, "internal error: var not found in variable table: %s", cname);
				}
				AddVarAccess(n->loc, AB_POP_TO, var_idx);
			}
			break;
		case ::aul::ast::VarDecl::Scope::Object:
		case ::aul::ast::VarDecl::Scope::Global:
			// Object-local and global constants are handled by ConstantResolver.
			break;
		}
	}
}

void C4AulCompiler::CodegenAstVisitor::visit(const ::aul::ast::FunctionDecl *n)
{
	assert(!Fn && "CodegenAstVisitor: function declaration encountered within active function");
	if (Fn)
		throw Error(target_host, host, n, Fn, "internal error: function declaration for '%s' encountered within active function", n->name.c_str());

	C4PropListStatic *Parent = n->is_global ? target_host->Engine->GetPropList() : target_host->GetPropList();

	C4String *name = ::Strings.FindString(n->name.c_str());
	C4AulFunc *f = Parent->GetFunc(name);
	while (f)
	{
		if (f->SFunc() && f->SFunc()->pOrgScript == host && f->Parent == Parent)
		{
			if (Fn)
				Warn(target_host, host, n, Fn, C4AulWarningId::redeclaration, "function", f->GetName());
			Fn = f->SFunc();
		}
		f = f->SFunc() ? f->SFunc()->OwnerOverloaded : nullptr;
	}

	if (!Fn && Parent->HasProperty(name))
	{
		throw Error(target_host, host, n, Fn, "declaration of '%s': cannot override local variable via 'func %s'", n->name.c_str(), n->name.c_str());
	}

	assert(Fn && "CodegenAstVisitor: unable to find function definition");
	if (!Fn)
		throw Error(target_host, host, n, Fn, "internal error: unable to find function definition for %s", n->name.c_str());

	// If this isn't a global function, but there is a global one with
	// the same name, and this function isn't overloading a different
	// one, add the global function to the overload chain
	if (!n->is_global && !Fn->OwnerOverloaded)
	{
		C4AulFunc *global_parent = target_host->Engine->GetFunc(Fn->GetName());
		if (global_parent)
			Fn->SetOverloaded(global_parent);
	}

	try
	{
		EmitFunctionCode(n);
		Fn = nullptr;
	}
	catch (...)
	{
		Fn = nullptr;
		throw;
	}
}

void C4AulCompiler::CodegenAstVisitor::visit(const::aul::ast::FunctionExpr * n)
{
	AddBCC(n->loc, AB_NIL);
	throw Error(target_host, host, n, Fn, "can't define a function in a function-scoped proplist");
}

void C4AulCompiler::CodegenAstVisitor::visit(const::aul::ast::Script * n)
{
	for (const auto &d : n->declarations)
	{
		SafeVisit(d);
	}
}

#undef ENSURE_COND
#define ENSURE_COND(cond, failmsg) do { if (!(cond)) throw Error(host, host, n, nullptr, failmsg); } while (0)

// Evaluates constant AST subtrees and returns the final C4Value.
// Throws ExpressionNotConstant if evaluation fails.

C4Value C4AulCompiler::ConstexprEvaluator::eval(C4ScriptHost *host, const ::aul::ast::Expr *e, EvalFlags flags)
{
	ConstexprEvaluator ce(host);
	ce.ignore_unset_values = (flags & IgnoreUnset) == IgnoreUnset;
	try
	{
		e->accept(&ce);
		return ce.v;
	}
	catch (C4AulParseError &e)
	{
		if ((flags & SuppressErrors) == 0)
			host->Engine->ErrorHandler->OnError(e.what());
		return C4VNull;
	}
}

C4Value C4AulCompiler::ConstexprEvaluator::eval_static(C4ScriptHost *host, C4PropListStatic *parent, const std::string &parent_key, const ::aul::ast::Expr *e, EvalFlags flags)
{
	ConstexprEvaluator ce(host);
	ce.proplist_magic = ConstexprEvaluator::ProplistMagic{ true, parent, parent_key };
	ce.ignore_unset_values = (flags & IgnoreUnset) == IgnoreUnset;
	try
	{
		e->accept(&ce);
		return ce.v;
	}
	catch (C4AulParseError &e)
	{
		if ((flags & SuppressErrors) == 0)
			host->Engine->ErrorHandler->OnError(e.what());
		return C4VNull;
	}
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::StringLit *n) { v = C4VString(n->value.c_str()); }

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::IntLit *n) { v = C4VInt(n->value); }

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::BoolLit *n) { v = C4VBool(n->value); }

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::ArrayLit *n)
{
	auto a = std::make_unique<C4ValueArray>(n->values.size());
	for (size_t i = 0; i < n->values.size(); ++i)
	{
		n->values[i]->accept(this);
		a->SetItem(i, v);
	}
	v = C4VArray(a.release());
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::ProplistLit *n)
{
	std::unique_ptr<C4PropList> new_proplist;
	C4PropList *p = nullptr;

	bool first_pass = true;

	if (proplist_magic.active)
	{
		// Check if there's already a proplist available
		C4String *key = ::Strings.RegString(proplist_magic.key.c_str());
		C4Value old;
		if (proplist_magic.parent)
		{
			proplist_magic.parent->GetPropertyByS(key, &old);
		}
		else
		{
			// If proplist_magic.parent is nullptr, we're handling a global constant.
			host->Engine->GetGlobalConstant(key->GetCStr(), &old);
		}
		if (old.getPropList())
		{
			p = old.getPropList();
			first_pass = false;
		}
		else
		{
			p = C4PropList::NewStatic(nullptr, proplist_magic.parent, key);
			new_proplist.reset(p);
		}
	}
	else
	{
		p = C4PropList::New();
		new_proplist.reset(p);
	}

	// Since the values may be functions that refer to other values in the
	// proplist, pre-populate the new proplist with dummy values until the
	// real ones are set
	if (first_pass)
	{
		for (const auto &kv : n->values)
		{
			p->SetPropertyByS(::Strings.RegString(kv.first.c_str()), C4VNull);
		}
	}

	auto saved_magic = std::move(proplist_magic);
	for (const auto &kv : n->values)
	{
		proplist_magic = ProplistMagic { saved_magic.active, p->IsStatic(), kv.first };
		kv.second->accept(this);
		p->SetPropertyByS(::Strings.RegString(kv.first.c_str()), v);
	}
	proplist_magic = std::move(saved_magic);
	v = C4VPropList(p);
	new_proplist.release();
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::NilLit *) { v = C4VNull; }

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::ThisLit *n) { nonconst(n, "\"this\" is not a global constant"); }

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::VarExpr *n)
{
	const char *cname = n->identifier.c_str();
	C4String *interned = ::Strings.FindString(cname);
	if (interned && host->GetPropList()->GetPropertyByS(interned, &v))
		return;
	if (host->Engine->GetGlobalConstant(cname, &v))
		return;

	if (ignore_unset_values)
	{
		v = C4VNull;
		return;
	}

	nonconst(n, "the variable \"%s\" is not a global constant", cname);
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::UnOpExpr *n)
{
	n->operand->accept(this);
	assert(n->op > 0);
	const auto &op = C4ScriptOpMap[n->op];
	if (op.Changer)
		nonconst(n, "unary operator %s is applied in a non-const fashion", op.Identifier);
	AssertValueType(v, op.Type1, op.Identifier, n);
	switch (op.Code)
	{
	case AB_BitNot:
		v.SetInt(~v._getInt());
		break;
	case AB_Not:
		v.SetBool(!v.getBool());
		break;
	case AB_Neg:
		v.SetInt(-v._getInt());
		break;
	default:
		assert(!"ConstexprEvaluator: Unexpected unary operator");
		throw Error(host, host, n, nullptr, "internal error: unary operator not found in operator table");
	}
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::BinOpExpr *n)
{
	assert(n->op > 0);
	const auto &op = C4ScriptOpMap[n->op];
	if (op.Changer)
		nonconst(n, "binary operator %s is applied in a non-const fashion", op.Identifier);

	n->lhs->accept(this);
	C4Value lhs = v;
	// Evaluate the short-circuiting operators here
	if ((op.Code == AB_JUMPAND && !lhs) || (op.Code == AB_JUMPOR && lhs) || (op.Code == AB_JUMPNNIL && lhs.GetType() != C4V_Nil))
	{
		v = lhs;
		return;
	}
	n->rhs->accept(this);
	C4Value &rhs = v;

	AssertValueType(lhs, op.Type1, op.Identifier, n);
	AssertValueType(rhs, op.Type2, op.Identifier, n);

	switch (op.Code)
	{
	case AB_Pow:
		v.SetInt(Pow(lhs._getInt(), rhs._getInt()));
		break;
	case AB_Div:
		ENSURE_COND(rhs._getInt() != 0, "division by zero");
		ENSURE_COND(lhs._getInt() != INT32_MIN || rhs._getInt() != -1, "division overflow");
		v.SetInt(lhs._getInt() / rhs._getInt());
		break;
	case AB_Mul:
		v.SetInt(lhs._getInt() * rhs._getInt());
		break;
	case AB_Mod:
		ENSURE_COND(rhs._getInt() != 0, "division by zero");
		ENSURE_COND(lhs._getInt() != INT32_MIN || rhs._getInt() != -1, "division overflow");
		v.SetInt(lhs._getInt() / rhs._getInt());
		break;
#define INT_BINOP(code, op) case code: v.SetInt(lhs._getInt() op rhs._getInt()); break
		INT_BINOP(AB_Sum, +);
		INT_BINOP(AB_Sub, -);
		INT_BINOP(AB_LeftShift, << );
		INT_BINOP(AB_RightShift, >> );
		INT_BINOP(AB_BitAnd, &);
		INT_BINOP(AB_BitXOr, ^);
		INT_BINOP(AB_BitOr, | );
#undef INT_BINOP
#define BOOL_BINOP(code, op) case code: v.SetBool(lhs._getInt() op rhs._getInt()); break
		BOOL_BINOP(AB_LessThan, <);
		BOOL_BINOP(AB_LessThanEqual, <= );
		BOOL_BINOP(AB_GreaterThan, >);
		BOOL_BINOP(AB_GreaterThanEqual, >= );
#undef BOOL_BINOP
	case AB_Equal:
		v.SetBool(lhs.IsIdenticalTo(rhs));
		break;
	case AB_NotEqual:
		v.SetBool(!lhs.IsIdenticalTo(rhs));
		break;
	case AB_JUMPAND:
	case AB_JUMPOR:
	case AB_JUMPNNIL:
		// If we hit this, then the short-circuit above failed
		v = rhs;
		break;
	default:
		assert(!"ConstexprEvaluator: Unexpected binary operator");
		throw Error(host, host, n, nullptr, "internal error: binary operator not found in operator table");
		break;
	}
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::AssignmentExpr *n)
{
	nonconst(n, "updating assignment used in a non-const fashion");
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::SubscriptExpr *n)
{
	n->object->accept(this);
	C4Value obj = v;
	n->index->accept(this);
	C4Value &index = v;

	if (obj.CheckConversion(C4V_Array))
	{
		ENSURE_COND(index.CheckConversion(C4V_Int), FormatString("array access: index of type %s, but expected int", index.GetTypeName()).getData());
		v = obj.getArray()->GetItem(index.getInt());
	}
	else if (obj.CheckConversion(C4V_PropList))
	{
		ENSURE_COND(index.CheckConversion(C4V_String), FormatString("proplist access: index of type %s, but expected string", index.GetTypeName()).getData());
		if (!obj.getPropList()->GetPropertyByS(index.getStr(), &v))
			v.Set0();
	}
	else
	{
		ENSURE_COND(false, FormatString("can't access %s as array or proplist", obj.GetTypeName()).getData());
	}
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::SliceExpr *n)
{
	n->object->accept(this);
	C4Value obj = v;
	n->start->accept(this);
	C4Value start = v;
	n->end->accept(this);
	C4Value &end = v;

	ENSURE_COND(obj.CheckConversion(C4V_Array), FormatString("array slice: can't access %s as an array", obj.GetTypeName()).getData());
	ENSURE_COND(start.CheckConversion(C4V_Int), FormatString("array slice: start index of type %s, int expected", start.GetTypeName()).getData());
	ENSURE_COND(end.CheckConversion(C4V_Int), FormatString("array slice: end index of type %s, int expected", end.GetTypeName()).getData());

	v.SetArray(obj.getArray()->GetSlice(start.getInt(), end.getInt()));
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::CallExpr *n)
{
	// TODO: allow side-effect-free calls here
	nonconst(n, "call to function (%s) not supported in constant expressions", n->callee.c_str());
}

void C4AulCompiler::ConstexprEvaluator::visit(const ::aul::ast::FunctionExpr *n)
{
	// Function expressions can only occur inside static proplists.
	ENSURE_COND(proplist_magic.active, "internal error: function expression outside of static proplist");

	C4AulScriptFunc *sfunc = nullptr;
	bool first_pass = true;

	if (auto func = proplist_magic.parent->GetFunc(proplist_magic.key.c_str()))
	{
		sfunc = func->SFunc();
		first_pass = false;
	}
	else
	{
		sfunc = new C4AulScriptFunc(proplist_magic.parent, host, proplist_magic.key.c_str(), n->loc);
	}

	ENSURE_COND(sfunc != nullptr, "internal error: function expression target resolved to non-function value");

	if (first_pass)
	{
		for (const auto &param : n->params)
		{
			sfunc->AddPar(param.name.c_str());
		}
		if (n->has_unnamed_params)
			sfunc->ParCount = C4AUL_MAX_Par;

		PreparseAstVisitor preparser(host, host, sfunc);
		preparser.visit(n->body.get());
	}
	else
	{
		CodegenAstVisitor cg(sfunc);
		cg.EmitFunctionCode(n);
	}

	v.SetFunction(sfunc);
}

void C4AulCompiler::ConstantResolver::visit(const::aul::ast::Script *n)
{
	for (const auto &d : n->declarations)
	{
		try
		{
			d->accept(this);
		}
		catch (C4AulParseError &e)
		{
			host->Engine->GetErrorHandler()->OnError(e.what());
		}
	}
}

void C4AulCompiler::ConstantResolver::visit(const ::aul::ast::VarDecl *n)
{
	const int quiet_flag = quiet ? ConstexprEvaluator::SuppressErrors : 0;
	for (const auto &dec : n->decls)
	{
		const char *cname = dec.name.c_str();
		C4RefCntPointer<C4String> name = ::Strings.RegString(cname);
		switch (n->scope)
		{
		case ::aul::ast::VarDecl::Scope::Func:
			// Function-scoped declarations and their initializers are handled by CodegenAstVisitor.
			break;
		case ::aul::ast::VarDecl::Scope::Object:
			if (!host->GetPropList()->HasProperty(name))
				host->GetPropList()->SetPropertyByS(name, C4VNull);
			if (dec.init)
			{
				assert(host->GetPropList()->IsStatic());
				try
				{
					C4Value v = ConstexprEvaluator::eval_static(host, host->GetPropList()->IsStatic(), dec.name, dec.init.get(), ConstexprEvaluator::IgnoreUnset | quiet_flag);
					host->GetPropList()->SetPropertyByS(name, v);
				}
				catch (C4AulParseError &e)
				{
					if (!quiet)
						host->Engine->ErrorHandler->OnError(e.what());
				}
			}
			break;
		case ::aul::ast::VarDecl::Scope::Global:
			if ((dec.init != nullptr) != n->constant)
			{
				if (!quiet)
					host->Engine->ErrorHandler->OnError(Error(host, host, n->loc, nullptr, "global variable must be either constant or uninitialized: %s", cname).what());
			}
			else if (dec.init)
			{
				try
				{
					assert(n->constant && "CodegenAstVisitor: initialized global variable isn't const");
					C4Value *v = host->Engine->GlobalConsts.GetItem(cname);
					assert(v && "CodegenAstVisitor: global constant not found in variable table");
					if (!v)
						throw Error(host, host, n->loc, nullptr, "internal error: global constant not found in variable table: %s", cname);
					*v = ConstexprEvaluator::eval_static(host, nullptr, dec.name, dec.init.get(), ConstexprEvaluator::IgnoreUnset | quiet_flag);
				}
				catch (C4AulParseError &e)
				{
					if (!quiet)
						host->Engine->ErrorHandler->OnError(e.what());
				}
			}
			break;
		}
	}
}
