~sircmpwn/hare-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
5 5

[PATCH harec] Add C++ templates to Hare

Details
Message ID
<20240401074550.22680-1-sebastian@sebsite.pw>
DKIM signature
pass
Download raw message
Patch: +7492 -829
This patch adds a long awaited feature to Hare: C++ templates.

The basics of them work roughly as you would expect:

	template<T: typename> fn sort(items: []T) void = {
		// very efficient sorting algorithm
		for (let i = 0z; i < len(items) - 1; i += 1) {
			let idx = i;
			for (let j = i + 1; j < len(items); j += 1) {
				if (cmp(items[j], items[idx]) < 0) {
					idx = j;
				};
			};

			const tmp = items[idx];
			items[idx] = items[i];
			items[i] = tmp;
		};
	};

	template<T: typename> fn cmp(a: T, b: T) int = {
		return if (a < b) -1
			else if (a > b) 1
			else 0;
	};

	// explicit specialization (special case for rune template argument)
	template<> fn cmp<>(a: rune, b: rune) int = cmp(a: u32, b: u32);

	// explicit specialization (special case for str template argument)
	template<> fn cmp<>(a: str, b: str) int = {
		const a = *(&a: *[]u8);
		const b = *(&b: *[]u8);

		let ln = if (len(a) < len(b)) (len(a), -1) else (len(b), 1);
		for (let i = 0z; i < ln.0; i += 1) {
			if (a[i] != b[i]) {
				return a[i]: int - b[i]: int;
			};
		};
		return if (len(a) == len(b)) 0 else ln.1;
	};

	@test fn sort() void = {
		let x: []int = [73, 43, 49, 67, 54, 19, 9, 31, 3, 31];
		sort<int>(x); // can pass template argument explicitly ...
		for (let i = 1z; i < len(x); i += 1) {
			assert(x[i] >= x[i - 1]);
		};

		let x: []rune = [
			'嶴', '3', '㱙', '~', '퍛', '<', '햴', 'w', 'Ḕ', 'J',
			'둵', 'W', '磌', '@', '驚', 'K', 'ꏤ', 'f', '仾', '=',
		];
		sort(x); // ... or can omit it and let it be deduced
		for (let i = 1z; i < len(x); i += 1) {
			assert(x[i]: u32 >= x[i - 1]: u32);
		};
	};

Unlike in C++, the order of declarations doesn't matter. Template
declarations and/or explicit specializations can even span multiple
subunits without issue.

	template<T: typename> let x<5, *T> = "special case";
	template<N: int, T: typename> let x: [N]T = [0...];

The parser is resilient:

	template<int = 0> type shadowed = void;
	template<> type shadowed<1> = int;
	template<> type shadowed<2> = bool;
	fn f(shadowed: int) shadowed<2> = {
		return shadowed < 1 >> 0;
	};

	template<N: int> def X = N * 3;
	static assert(X<1>>2);

Templates can also be used on constant declarations, which is very
powerful. For example, here's an implementation of compile-time
factorial and power "functions":

	template<> def FACTORIAL<0> = 1u;
	template<N: uint> def FACTORIAL = N * FACTORIAL<N - 1>;
	static assert(FACTORIAL<6> == 720);

	template<BASE: int> def POW<BASE, 0>: int = 1;
	template<BASE: int, EXP: uint> def POW = BASE * POW<BASE, EXP - 1>;
	static assert(POW<9, 3> == 729);

Ever wanted a generic "default value" operator in Hare? Now you can make
your own!

	template<T: typename> def DEFAULT: T = _default<T>{ ... }.default;
	template<T: typename> type _default = struct { default: T };

	static assert(DEFAULT<int> == 0);
	static assert(DEFAULT<str> == "");
	static assert(DEFAULT<nullable *opaque> == null);

Templates can also be imported from other modules:

	use sort;

	@test fn sort() void = {
		let x: []int = [73, 43, 49, 67, 54, 19, 9, 31, 3, 31];
		sort::sort<int>(x);
		assert(sort::sorted(x));
	};

The Itanium C++ ABI is used for mangling the names of instantiated
templates. This means that the generated mangled names are compatible
with GCC and Clang, as well as some other C++ implementations. Because
of this, templates declared in Hare can be called from C++, and vice
versa:

example.ha:
	export template<N: uint, T: typename> fn bar(
		x: *T,
	) nullable *opaque = {
		static let arr: [N]T = [0...];
		arr[..] = [*x...];
		return &arr;
	};

	// mangled as _Z3barILj4EhEPvRT0_
	export template fn bar<4>(x: *u8) nullable *opaque;

example.cxx:
	#include <cassert>

	template<unsigned int N, typename T> void *bar(T &x);

	int main()
	{
		unsigned char c = 'c';
		unsigned char *x = (unsigned char *)bar<4>(c);
		for (int i = 0; i < 4; i++) {
			assert(x[i] == c);
		}
	}

Here's another example with a much more complex template. this time
function definition is written in C++ and called from Hare:

example.cxx:
	#include <iostream>

	namespace f {
		template<typename ...T, typename TT> void g(float *a,
			TT &b, void *c, void *d, float *&(&e)(T *...),
			void (&f)(T *&...), void (&g)(T *&...),
			const TT *h, const TT &i)
		{
			std::cout << "hi lol\n";
		}

		template void g(float *, float *&, void *, void *,
			float *&(&)(float *, signed char *, unsigned long long *),
			void (&)(float *&, signed char *&, unsigned long long *&),
			void (&)(float *&, signed char *&, unsigned long long *&),
			float *const *, float *const &);
	}

example.ha:
	template<T: typename..., TT: typename> fn f::g(
		a: nullable *f32,
		b: *TT,
		c: nullable *opaque,
		d: nullable *opaque,
		e: *fn(nullable *T...) *nullable *f32,
		f: *fn(*nullable *T...) void,
		g: *fn(*nullable *T...) void,
		h: nullable *const TT,
		i: *const TT,
	) void;

	export @symbol("main") fn main() void = {
		// mangled as _ZN1f1gIJfayEPfEEvS1_RT0_PvS4_RFRS1_DpPT_ERFvDpRS7_ESE_PKS2_RSF_
		f::g(&0f32, &(null: nullable *f32), &0, &0, &float_func,
			&void_func, &void_func, null,
			&(null: nullable *f32));
	};

	fn float_func(
		a: nullable *f32,
		b: nullable *i8,
		c: nullable *u64,
	) *nullable *f32 = null: *nullable *f32;

	fn void_func(
		a: *nullable *f32,
		b: *nullable *i8,
		c: *nullable *u64,
	) void = void;

Variadic templates are implemented using parameter packs:

	template<T: typename...> type tagged_with_ptrs = (*T... |);
	type t = tagged_with_ptrs<int, str, f64>; // (*int | *str | *f64)

The syntax for expanding a parameter pack is suffixing it with ellipsis.

	template<S: int...> fn f(n: int) bool = {
		switch (n) {
		case 1, S..., 3 =>
			return true;
		case =>
			return false;
		};
	};

Unfortunately, this introduces a parsing ambiguity when used in a
function prototype or call expression:

	type t = int;
	fn f(x: t...) void = void;
	template<T: typename...> fn g(x: T...) void = void;

	template<S: int...> fn h() void = {
		let s: []t = [1, 2, 3];
		f(s...);
		g(S...);
	};

In the above example, f is a Hare-style variadic function, whereas g is
a non-variadic function whose parameters are expanded from the parameter
pack T. Likewise, the function call to f uses the slice s as variadic
arguments, whereas the call to g expands the parameter pack S and passes
in the arguments normally. The rule used to disambiguate these cases is
that if the type or expression being expanded contains an unexpanded
parameter pack, then it's treated as a pack expansion.

Various expressions have been introduced to perform computations on/with
parameter packs. The new `size...(X)` expression gives the length of a
parameter pack:

	template<S: int...> def PACK_LEN = size...(S);

Folding expressions have also been introduced. These let you perform
binary arithmetic on all items in a parameter pack:

	// unary folds
	template<S: int...> def LSUB = (... - S);
	template<S: int...> def RSUB = (S - ...);

Given the parameter list <1, 2, 3, 4>, LSUB expands to
(((1 - 2) - 3) - 4), and RSUB expands to (1 - (2 - (3 - 4))). Binary
folds are supported as well:

	// binary folds
	template<N: int, S: int...> def BLSUB = (N - ... - S);
	template<N: int, S: int...> def BRSUB = (S - ... - N);

Given the same parameter list (where N=1, and S=<2, 3, 4>), BLSUB
expands to (((1 - 2) - 3) - 4), and BRSUB expands to
(2 - (3 - (4 - 1))). Any binary operator can be used for any form of
folding expression:

	template<S: int...> def X = (... < S);

The constant X is only valid if S contains 1 or 2 items; any more items
and you end up comparing a bool to an int, causing translation to fail.

If a parameter pack in a unary fold expression expands to only one item,
the result is that item. If a parameter pack in a binary fold expression
expands to zero items, the result is the non-pack operand. If a
parameter pack in a unary fold expression expands to zero items, the
compiler will usually error out, with two exceptions, demonstrated
below:

	template<S: bool...> def AND = (... && S);
	template<S: bool...> def OR = (... || S);
	static assert(AND && !OR);

These rules are all taken from the C++ standard.

To those concerned with backwards compatibility, don't worry: this
commit is almost entirely backwards compatible. Although 3 new keywords
are added (class, template, typename), they're only lexed as keywords
where permitted by the syntax, so they can still be used as names:

	use strings::template;
	template<> let template: template::template = [];

There's one place where this commit technically introduces a breaking
change. All mangled names begin with `_Z`, which is fine in C++ since
identifiers beginning with underscores are reserved, but Hare doesn't
have reserved identifiers. So instead, all names in the program which
begin with `_Z` are "adjusted" by suffixing them with `$`. This makes
them valid identifiers in the assembler, but not within Hare, so unless
the programmer uses @symbol, there will never be a name conflict. This
is only a breaking change for code which relies on the symbol emitted
for identifiers beginning with `_Z`.

Signed-off-by: Sebastian <sebastian@sebsite.pw>
---
Happy april fools! :3

i spent so long on this, if the ci somehow fails i'm gonna cry

 Makefile              |    5 +
 include/ast.h         |   65 +-
 include/check.h       |   56 +-
 include/eval.h        |    3 +
 include/expr.h        |    1 +
 include/identifier.h  |    9 +
 include/lex.h         |    6 +
 include/mod.h         |    2 +
 include/parse.h       |   17 +-
 include/scope.h       |   23 +-
 include/template.h    |   22 +
 include/typedef.h     |    8 +-
 include/types.h       |   14 +
 include/util.h        |    2 +-
 makefiles/freebsd.mk  |    1 +
 makefiles/linux.mk    |    1 +
 makefiles/netbsd.mk   |    1 +
 makefiles/openbsd.mk  |    3 +-
 makefiles/tests.mk    |   22 +-
 rt/test_helper.ha     |    2 +
 src/ast.c             |  862 ++++++++++++
 src/check.c           | 3125 +++++++++++++++++++++++++++++++++++++----
 src/eval.c            |   66 +-
 src/gen.c             |    9 +-
 src/identifier.c      |  818 ++++++++++-
 src/lex.c             |   16 +-
 src/main.c            |   67 +-
 src/mod.c             |   58 +-
 src/parse.c           | 1414 ++++++++++++++-----
 src/scope.c           |   17 +-
 src/template.c        |  163 +++
 src/type_store.c      |  548 ++++++--
 src/typedef.c         |  128 +-
 src/types.c           |  133 +-
 src/util.c            |   12 +-
 testmod/templates.ha  |   48 +
 testmod/templates2.ha |    6 +
 tests/30-reduction.c  |    6 +-
 tests/37-templates.ha |  562 ++++++++
 39 files changed, 7492 insertions(+), 829 deletions(-)
 create mode 100644 include/template.h
 create mode 100644 rt/test_helper.ha
 create mode 100644 src/ast.c
 create mode 100644 src/template.c
 create mode 100644 testmod/templates.ha
 create mode 100644 testmod/templates2.ha
 create mode 100644 tests/37-templates.ha

diff --git a/Makefile b/Makefile
index 92dc093..71b3956 100644
--- a/Makefile
+++ b/Makefile
@@ -25,6 +25,7 @@ headers = \
	include/parse.h \
	include/qbe.h \
	include/scope.h \
	include/template.h \
	include/type_store.h \
	include/typedef.h \
	include/types.h \
@@ -32,6 +33,7 @@ headers = \
	include/util.h

harec_objects = \
	src/ast.o \
	src/check.o \
	src/emit.o \
	src/eval.o \
@@ -46,6 +48,7 @@ harec_objects = \
	src/qinstr.o \
	src/qtype.o \
	src/scope.o \
	src/template.o \
	src/type_store.o \
	src/typedef.o \
	src/types.o \
@@ -60,6 +63,7 @@ $(BINOUT)/harec: $(harec_objects)
.SUFFIXES:
.SUFFIXES: .ha .ssa .td .c .o .s .scd .1 .5

src/ast.o: $(headers)
src/check.o: $(headers)
src/emit.o: $(headers)
src/eval.o: $(headers)
@@ -74,6 +78,7 @@ src/qbe.o: $(headers)
src/qinstr.o: $(headers)
src/qtype.o: $(headers)
src/scope.o: $(headers)
src/template.o: $(headers)
src/type_store.o: $(headers)
src/typedef.o: $(headers)
src/types.o: $(headers)
diff --git a/include/ast.h b/include/ast.h
index e8ac029..b337f2f 100644
--- a/include/ast.h
+++ b/include/ast.h
@@ -2,9 +2,12 @@
#define HARE_AST_H
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "expr.h"
#include "identifier.h"
#include "lex.h"
#include "scope.h"
#include "template.h"
#include "types.h"

struct ast_type;
@@ -52,15 +55,15 @@ struct ast_enum_type {

struct ast_function_parameters {
	struct location loc;
	bool variadic;
	char *name;
	struct ast_type *type;
	struct ast_type *type; // NULL if C-style variadism
	struct ast_function_parameters *next;
};

struct ast_function_type {
	struct ast_type *result;
	struct ast_function_parameters *params;
	enum variadism variadism;
};

struct ast_pointer_type {
@@ -69,11 +72,13 @@ struct ast_pointer_type {
};

struct ast_tagged_union_type {
	bool variadic;
	struct ast_type *type;
	struct ast_tagged_union_type *next;
};

struct ast_tuple_type {
	bool variadic;
	struct ast_type *type;
	struct ast_tuple_type *next;
};
@@ -104,6 +109,7 @@ struct ast_type {
		struct ast_tuple_type tuple;
		struct {
			struct identifier alias;
			struct ast_template_argument *tmpl_args;
			union {
				struct ast_enum_type _enum;
				bool unwrap;
@@ -122,10 +128,25 @@ struct ast_expression_list {
	struct ast_expression_list *next;
};

struct ast_template_argument {
	bool variadic;
	// expr/type are NULL only if the argument list is empty (<>)
	// this is necessary to distinguish between an absent argument list and
	// an empty argument list
	union {
		struct ast_expression *expr;
		struct ast_type *type;
	};
	struct ast_template_argument *next;
};

struct ast_expression_access {
	enum access_type type;
	union {
		struct identifier ident;
		struct {
			struct identifier ident;
			struct ast_template_argument *tmpl_args;
		};
		struct {
			struct ast_expression *array;
			struct ast_expression *index;
@@ -167,6 +188,7 @@ struct ast_expression_assign {

struct ast_expression_binarithm {
	enum binarithm_operator op;
	bool fold;
	struct ast_expression *lvalue, *rvalue;
};

@@ -278,6 +300,7 @@ enum measure_operator {
	M_ALIGN,
	M_LEN,
	M_SIZE,
	M_SIZEPACK,
	M_OFFSET,
};

@@ -286,7 +309,7 @@ struct ast_expression_measure {
	union {
		struct ast_expression *value;
		struct ast_type *type;
		// TODO: Field selection
		struct identifier ident;
	};
};

@@ -305,6 +328,7 @@ struct ast_expression_slice {
};

struct ast_case_option {
	bool variadic;
	struct ast_expression *value;
	struct ast_case_option *next;
};
@@ -331,10 +355,12 @@ struct ast_field_value {
struct ast_expression_struct {
	bool autofill;
	struct identifier type;
	struct ast_template_argument *tmpl_args;
	struct ast_field_value *fields;
};

struct ast_expression_tuple {
	bool variadic;
	struct ast_expression *expr;
	struct ast_expression_tuple *next;
};
@@ -419,10 +445,29 @@ enum ast_decl_type {
	ADECL_ASSERT,
};

struct ast_template_parameters {
	struct location loc;
	bool is_pack; // variadic
	bool template; // template template parameter
	struct ast_template_parameters *tmpl_params; // for template template parameter
	char *name;
	struct ast_type *type; // NULL if "class" or "typename"
	union {
		struct ast_expression *init;
		struct ast_type *default_type;
	};
	struct ast_template_parameters *next;
};

struct ast_decl {
	struct location loc;
	enum ast_decl_type decl_type;
	bool exported;
	// the template field is true if a template parameter list is present.
	// it isn't set for explicit template instantiations
	bool template;
	struct ast_template_parameters *tmpl_params;
	struct ast_template_argument *specialization;
	union {
		struct ast_global_decl global;
		struct ast_global_decl constant;
@@ -448,4 +493,16 @@ struct ast_unit {
	struct ast_subunit subunits;
};

void emit_ast_template_args(const struct ast_template_argument *args,
	const struct template *tmpl, struct scope *scope,
	FILE *out, templates tmpls);
void emit_ast_template_params(const struct ast_template_parameters *params,
	struct scope *scope, FILE *out, templates tmpls);
void emit_ast_prototype(const struct ast_function_type *func,
	struct scope *scope, FILE *out, templates tmpls);
void emit_ast_type(const struct ast_type *type, struct scope *scope,
	FILE *out, templates tmpls);
void emit_ast_expr(const struct ast_expression *expr,
	struct scope *scope, FILE *out, templates tmpls);

#endif
diff --git a/include/check.h b/include/check.h
index 7226228..359b69f 100644
--- a/include/check.h
+++ b/include/check.h
@@ -1,8 +1,10 @@
#ifndef HARE_CHECK_H
#define HARE_CHECK_H
#include <stdbool.h>
#include <stddef.h>
#include <stdnoreturn.h>
#include "ast.h"
#include "expr.h"
#include "identifier.h"
#include "scope.h"
#include "types.h"
@@ -24,6 +26,11 @@ struct errors {
	struct errors *next;
};

struct error_context {
	struct location loc;
	struct error_context *next;
};

struct context {
	type_store *store;
	struct modcache **modcache;
@@ -37,6 +44,7 @@ struct context {
	int id;
	struct errors *errors;
	struct errors **next;
	struct error_context *errctx;
	struct declarations *decls;
	struct ast_types *unresolved;
};
@@ -59,11 +67,36 @@ struct global_decl {
	bool threadlocal;
};

struct template_specialization {
	struct scope *imports; // the scope of this specialization's subunit
	struct ast_template_parameters *params;
	struct ast_template_argument *args;
	struct location loc;
	bool threadlocal;
	struct ast_type *type;
	struct ast_expression *expr;
	struct template_specialization *next;
};

struct template_decl {
	struct scope *imports; // the scope of the unspecialized declaration's subunit
	struct scope *unit; // the scope of the template's unit
	struct ast_template_parameters *params;
	struct template_specialization *specializations;
	enum ast_decl_type decl_type;
	bool exported;
	bool threadlocal;
	struct location loc;
	struct ast_type *type;
	struct ast_expression *expr;
};

enum decl_type {
	DECL_FUNC,
	DECL_TYPE,
	DECL_GLOBAL,
	DECL_CONST,
	DECL_TEMPLATE,
};

struct declaration {
@@ -77,6 +110,7 @@ struct declaration {
		struct function_decl func;
		struct global_decl global;
		const struct type *type;
		struct template_decl template;
	};
};

@@ -102,6 +136,12 @@ struct incomplete_enum_field {
	struct scope *enum_scope;
};

struct incomplete_specialization {
	struct ast_decl decl;
	struct scope *imports; // the scope of this specialization's subunit
	struct incomplete_specialization *next;
};

// Keeps track of context required to resolve a declaration or an enum field
// Extends the scope_object struct so it can be inserted into a scope
struct incomplete_declaration {
@@ -110,10 +150,12 @@ struct incomplete_declaration {
	enum idecl_type type;
	bool in_progress;
	bool dealias_in_progress;
	bool has_unspecialized; // used by template declarations
	union {
		struct ast_decl decl;
		struct incomplete_enum_field *field;
	};
	struct incomplete_specialization *next; // used by template declarations
};

void mkident(struct context *ctx, struct identifier *out,
@@ -126,10 +168,18 @@ char *gen_typename(const struct type *type);
typedef void (*resolvefn)(struct context *,
		struct incomplete_declaration *idecl);

struct scope_object *specialize(struct context *ctx,
	const struct scope_object *obj,
	const struct ast_template_argument *tmpl_args,
	const struct types *fn_params);

size_t type_pack_expand(struct context *ctx, struct scope *scope,
	const struct ast_type *atype, size_t n);

void resolve_dimensions(struct context *ctx,
		struct incomplete_declaration *idecl);

void resolve_type(struct context *ctx,
void resolve_decl(struct context *ctx,
		struct incomplete_declaration *idecl);

void wrap_resolver(struct context *ctx,
@@ -158,4 +208,8 @@ void check_expression(struct context *ctx,

void error(struct context *ctx, struct location loc,
	struct expression *expr, const char *fmt, ...);

void push_error_context(struct context *ctx, struct location loc);

void pop_error_context(struct context *ctx);
#endif
diff --git a/include/eval.h b/include/eval.h
index 2078457..ed442e7 100644
--- a/include/eval.h
+++ b/include/eval.h
@@ -5,6 +5,9 @@
struct expression;
struct context;

bool literal_eq(struct context *ctx, const struct expression *a,
	const struct expression *b);

// Evaluates an expression at compile time.
bool eval_expr(struct context *ctx, const struct expression *in,
	struct expression *out);
diff --git a/include/expr.h b/include/expr.h
index 2218283..6741c94 100644
--- a/include/expr.h
+++ b/include/expr.h
@@ -345,6 +345,7 @@ struct expression_vaarg {
struct expression {
	const struct type *result;
	enum expr_type type;
	int tmpl_param_idx;
	struct location loc; // For fixed aborts
	union {
		struct expression_access access;
diff --git a/include/identifier.h b/include/identifier.h
index de0fe71..6dfcd7b 100644
--- a/include/identifier.h
+++ b/include/identifier.h
@@ -4,6 +4,10 @@
#include <stdbool.h>
#include <stdint.h>

struct ast_template_parameters;
struct scope;
struct type_func;

// Maximum length of an identifier, as the sum of the lengths (excluding NUL
// terminators) of its parts plus one for each namespace deliniation.
//
@@ -24,6 +28,11 @@ struct identifiers {
	struct identifiers *next;
};

char *mangle(const struct identifier *ident, struct scope *scope,
	const struct ast_template_parameters *params,
	const struct type_func *prototype);
void adjust_reserved_ident(struct identifier *ident);
void tmpl_param_scope_ident(struct identifier *out, const char *name, int idx);
uint32_t identifier_hash(uint32_t init, const struct identifier *ident);
char *identifier_unparse(const struct identifier *ident);
int identifier_unparse_static(const struct identifier *ident, char *buf);
diff --git a/include/lex.h b/include/lex.h
index bf0cac0..bd9ae2a 100644
--- a/include/lex.h
+++ b/include/lex.h
@@ -26,6 +26,7 @@ enum lexical_token {
	T_BOOL,
	T_BREAK,
	T_CASE,
	T_CLASS,
	T_CONST,
	T_CONTINUE,
	T_DEF,
@@ -63,8 +64,10 @@ enum lexical_token {
	T_STR,
	T_STRUCT,
	T_SWITCH,
	T_TEMPLATE,
	T_TRUE,
	T_TYPE,
	T_TYPENAME,
	T_U16,
	T_U32,
	T_U64,
@@ -173,6 +176,9 @@ struct lexer {
	struct token un;
	struct location loc;
	bool require_int;
	// if template_compat is true, `class`, `template`, and `typename` are
	// lexed as names rather than keywords, for backwards compatibility
	bool template_compat;
};

void lex_init(struct lexer *lexer, FILE *f, int fileid);
diff --git a/include/mod.h b/include/mod.h
index c32cff2..c32557d 100644
--- a/include/mod.h
+++ b/include/mod.h
@@ -2,9 +2,11 @@
#define HARE_MOD_H
#include "identifier.h"
#include "scope.h"
#include "template.h"

struct ast_global_decl;
struct context;
void scan_mod_templates(const struct identifier *ident, templates out);
struct scope *module_resolve(struct context *ctx,
	const struct ast_global_decl *defines,
	const struct identifier *ident);
diff --git a/include/parse.h b/include/parse.h
index 8f7f812..b8510e7 100644
--- a/include/parse.h
+++ b/include/parse.h
@@ -1,15 +1,14 @@
#ifndef HAREC_PARSE_H
#define HAREC_PARSE_H
#include <stdio.h>
#include "ast.h"
#include "identifier.h"
#include "lex.h"
#include "template.h"

struct ast_expression;
struct ast_subunit;
struct ast_type;
struct lexer;

void parse(struct lexer *lexer, struct ast_subunit *unit);
void parse_imports(struct lexer *lexer, struct ast_subunit *subunit);
void parse_decls(struct lexer *lexer, templates tmpls, struct ast_decls **decls);
bool parse_identifier(struct lexer *lexer, struct identifier *ident, bool trailing);
struct ast_type *parse_type(struct lexer *lexer);
struct ast_expression *parse_expression(struct lexer *lexer);
struct ast_type *parse_type(struct lexer *lexer, templates tmpls);
struct ast_expression *parse_expression(struct lexer *lexer, templates tmpls);

#endif
diff --git a/include/scope.h b/include/scope.h
index 97e461c..02db412 100644
--- a/include/scope.h
+++ b/include/scope.h
@@ -2,28 +2,46 @@
#define HAREC_SCOPE_H
#include "expr.h"
#include "identifier.h"
#include "types.h"

#define SCOPE_BUCKETS 4096

struct template_decl;

enum object_type {
	O_BIND,
	O_CONST,
	O_DECL,
	O_PACK,
	O_SCAN,
	O_TEMPLATE,
	O_TYPE,
};

struct scope_object {
	enum object_type otype;
	bool threadlocal;
	// name is the name of the object within this scope (for lookups)
	// ident is the global identifier (these may be different in some cases)
	struct identifier name, ident;
	bool threadlocal;

	union {
		const struct type *type;
		struct expression *value; // For O_CONST
		struct template_decl *template; // For O_TEMPLATE
	};

	union {
		struct types *pack_types;
		struct expressions *pack_values;
	};
	// pack_pos is used when deducing a parameter pack: parameters may be
	// "deduced" multiple times, but we don't want them to be duplicated at
	// the end of the pack; we want them to be skipped. so pack_pos is set
	// to 0 before deducing, and then incremented everytime something is
	// "deduced". if there's already something present at pack_pos, nothing
	// is added to the pack
	size_t pack_pos;

	struct scope_object *lnext; // Linked list
	struct scope_object *mnext; // Hash map
@@ -93,6 +111,9 @@ struct scope_object *scope_insert(
	const struct identifier *ident, const struct identifier *name,
	const struct type *type, struct expression *value);

struct scope_object *scope_lookup_noparent(struct scope *scope,
	const struct identifier *ident);

struct scope_object *scope_lookup(struct scope *scope,
	const struct identifier *ident);

diff --git a/include/template.h b/include/template.h
new file mode 100644
index 0000000..8eef237
--- /dev/null
+++ b/include/template.h
@@ -0,0 +1,22 @@
#ifndef HAREC_TEMPLATE_H
#define HAREC_TEMPLATE_H
#include <stddef.h>
#include <stdint.h>
#include "lex.h"
#include "identifier.h"

#define TEMPLATE_BUCKETS 1024

typedef struct template *templates[TEMPLATE_BUCKETS];

struct template {
	struct identifier ident;
	struct template *next;
	size_t nparams;
	uint64_t type_params[]; // bit map
};

const struct template *get_template(const struct identifier *ident, templates tmpls);
void scan_templates(struct lexer *lexer, templates out);

#endif
diff --git a/include/typedef.h b/include/typedef.h
index c44f2be..7065cb8 100644
--- a/include/typedef.h
+++ b/include/typedef.h
@@ -1,11 +1,15 @@
#ifndef HARE_TYPEDEF_H
#define HARE_TYPEDEF_H
#include <stdio.h>
#include "scope.h"
#include "template.h"
#include "types.h"

struct type;
struct unit;

const char *storage_to_suffix(enum type_storage storage);
void emit_type(const struct type *type, FILE *out);
void emit_typedefs(struct unit *unit, FILE *out);
void emit_typedefs(struct unit *unit, struct scope *scope,
	FILE *out, templates tmpls);

#endif
diff --git a/include/types.h b/include/types.h
index d7c2003..67eb227 100644
--- a/include/types.h
+++ b/include/types.h
@@ -50,6 +50,11 @@ enum type_storage {
struct context;
struct type;

struct types {
	const struct type *type;
	struct types *next;
};

#define SIZE_UNDEFINED ((size_t)-1)
#define ALIGN_UNDEFINED ((size_t)-1)

@@ -137,12 +142,15 @@ struct type_tagged_union {
enum type_flags {
	TYPE_CONST = 1 << 0,
	TYPE_ERROR = 1 << 1,
	// For internal use only
	TYPE_EXPAND = 1 << 2, // only used when mangling function prototypes
};

struct type {
	enum type_storage storage;
	uint32_t id;
	unsigned int flags;
	int tmpl_param_idx;
	size_t size, align;
	union {
		struct {
@@ -186,6 +194,9 @@ bool type_has_error(struct context *ctx, const struct type *type);

uint32_t type_hash(const struct type *type);

bool tmpl_param_idx_eq(const struct type *a, const struct type *b);
uint32_t tmpl_param_idx_hash(const struct type *type);

const struct type *promote_flexible(struct context *ctx,
	const struct type *a, const struct type *b);
bool type_is_assignable(struct context *ctx,
@@ -251,4 +262,7 @@ extern struct type
	builtin_type_const_str,
	builtin_type_valist;

extern unsigned int short_size;
extern unsigned int long_size;

#endif
diff --git a/include/util.h b/include/util.h
index e0bd321..dbd11fc 100644
--- a/include/util.h
+++ b/include/util.h
@@ -47,6 +47,6 @@ int xvfprintf(FILE *restrict f, const char *restrict fmt, va_list ap) FORMAT(0);

char *gen_name(int *id, const char *fmt);

void errline(struct location loc);
void errline(struct location loc, int color);

#endif
diff --git a/makefiles/freebsd.mk b/makefiles/freebsd.mk
index 7ab69bd..2051839 100644
--- a/makefiles/freebsd.mk
+++ b/makefiles/freebsd.mk
@@ -2,6 +2,7 @@ RTSCRIPT = rt/hare.sc

_rt_ha = \
	rt/malloc.ha \
	rt/test_helper.ha \
	rt/+$(PLATFORM)/syscallno.ha \
	rt/+$(PLATFORM)/segmalloc.ha

diff --git a/makefiles/linux.mk b/makefiles/linux.mk
index 69add7b..6bf7ce5 100644
--- a/makefiles/linux.mk
+++ b/makefiles/linux.mk
@@ -2,6 +2,7 @@ RTSCRIPT = rt/hare.sc

_rt_ha = \
	rt/malloc.ha \
	rt/test_helper.ha \
	rt/+$(PLATFORM)/syscallno+$(ARCH).ha \
	rt/+$(PLATFORM)/segmalloc.ha

diff --git a/makefiles/netbsd.mk b/makefiles/netbsd.mk
index f8bf4f6..0b61079 100644
--- a/makefiles/netbsd.mk
+++ b/makefiles/netbsd.mk
@@ -2,6 +2,7 @@ RTSCRIPT = rt/hare+$(PLATFORM).sc

_rt_ha = \
	rt/malloc.ha \
	rt/test_helper.ha \
	rt/+$(PLATFORM)/syscallno.ha \
	rt/+$(PLATFORM)/segmalloc.ha

diff --git a/makefiles/openbsd.mk b/makefiles/openbsd.mk
index 0f43bd5..2c4df31 100644
--- a/makefiles/openbsd.mk
+++ b/makefiles/openbsd.mk
@@ -1,7 +1,8 @@
RTSCRIPT = rt/hare+$(PLATFORM).sc

_rt_ha = \
	rt/malloc+libc.ha
	rt/malloc+libc.ha \
	rt/test_helper.ha

_rt_s = \
	rt/+openbsd/platformstart.s
diff --git a/makefiles/tests.mk b/makefiles/tests.mk
index 8c01307..bb0e104 100644
--- a/makefiles/tests.mk
+++ b/makefiles/tests.mk
@@ -11,9 +11,11 @@ test_objects = \
	src/utf8.o \
	src/eval.o \
	src/typedef.o \
	src/mod.o
	src/mod.o \
	src/ast.o \
	src/template.o \

testmod_ha = testmod/measurement.ha testmod/testmod.ha
testmod_ha = testmod/measurement.ha testmod/templates.ha testmod/templates2.ha testmod/testmod.ha
$(HARECACHE)/testmod.ssa: $(testmod_ha) $(HARECACHE)/rt.td $(BINOUT)/harec
	@mkdir -p -- $(HARECACHE)
	@printf 'HAREC\t%s\n' '$@'
@@ -81,7 +83,8 @@ tests = \
	tests/33-yield \
	tests/34-declarations \
	tests/35-floats \
	tests/36-defines
	tests/36-defines \
	tests/37-templates


tests/00-literals: $(HARECACHE)/rt.o $(HARECACHE)/testmod.o $(HARECACHE)/tests_00_literals.o
@@ -89,7 +92,7 @@ tests/00-literals: $(HARECACHE)/rt.o $(HARECACHE)/testmod.o $(HARECACHE)/tests_0
	@$(LD) $(LDLINKFLAGS) -T $(RTSCRIPT) -o $@ $(HARECACHE)/rt.o $(HARECACHE)/testmod.o $(HARECACHE)/tests_00_literals.o

tests_00_literals_ha = tests/00-literals.ha
$(HARECACHE)/tests_00_literals.ssa: $(tests_00_literals_ha) $(HARECACHE)/rt.td $(BINOUT)/harec
$(HARECACHE)/tests_00_literals.ssa: $(tests_00_literals_ha) $(HARECACHE)/rt.td $(HARECACHE)/testmod.td $(BINOUT)/harec
	@mkdir -p -- $(HARECACHE)
	@printf 'HAREC\t%s\n' '$@'
	@$(TDENV) $(BINOUT)/harec $(HARECFLAGS) -o $@ $(tests_00_literals_ha)
@@ -483,3 +486,14 @@ $(HARECACHE)/tests_36_defines.ssa: $(tests_36_defines_ha) $(HARECACHE)/rt.td $(B
	@mkdir -p -- $(HARECACHE)
	@printf 'HAREC\t%s\n' '$@'
	@$(TDENV) $(BINOUT)/harec $(HARECFLAGS) -o $@ $(tests_36_defines_ha)


tests/37-templates: $(HARECACHE)/rt.o $(HARECACHE)/testmod.o $(HARECACHE)/tests_37_templates.o
	@printf 'LD\t%s\t\n' '$@'
	@$(LD) $(LDLINKFLAGS) -T $(RTSCRIPT) -o $@ $(HARECACHE)/rt.o $(HARECACHE)/testmod.o $(HARECACHE)/tests_37_templates.o

tests_37_templates_ha = tests/37-templates.ha
$(HARECACHE)/tests_37_templates.ssa: $(tests_37_templates_ha) $(HARECACHE)/rt.td $(HARECACHE)/testmod.td $(BINOUT)/harec
	@mkdir -p -- $(HARECACHE)
	@printf 'HAREC\t%s\n' '$@'
	@$(TDENV) $(BINOUT)/harec $(HARECFLAGS) -o $@ $(tests_37_templates_ha)
diff --git a/rt/test_helper.ha b/rt/test_helper.ha
new file mode 100644
index 0000000..eac5705
--- /dev/null
+++ b/rt/test_helper.ha
@@ -0,0 +1,2 @@
// used by testmod to test explicit template specializations across subunits
export def SUCCESS = 1;
diff --git a/src/ast.c b/src/ast.c
new file mode 100644
index 0000000..b669dfe
--- /dev/null
+++ b/src/ast.c
@@ -0,0 +1,862 @@
#include <assert.h>
#include <ctype.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "ast.h"
#include "expr.h"
#include "identifier.h"
#include "scope.h"
#include "template.h"
#include "typedef.h"
#include "types.h"
#include "util.h"

void
emit_ast_template_args(const struct ast_template_argument *args,
	const struct template *tmpl,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	xfprintf(out, "<");
	for (size_t i = 0; args && args->expr; args = args->next) {
		if ((tmpl->type_params[i / 64] >> (i % 64) & 1) == 1) {
			emit_ast_type(args->type, scope, out, tmpls);
		} else {
			emit_ast_expr(args->expr, scope, out, tmpls);
		}
		if (args->variadic) {
			xfprintf(out, "...");
		}
		if (args->next) {
			xfprintf(out, ", ");
		}
		if (i < tmpl->nparams) {
			i++;
		}
	}
	xfprintf(out, ">");
}

void
emit_ast_template_params(const struct ast_template_parameters *params,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	xfprintf(out, "<");
	for (const struct ast_template_parameters *param = params;
			param; param = param->next) {
		xfprintf(out, "%s: ", param->name);
		if (param->type) {
			emit_ast_type(param->type, scope, out, tmpls);
			if (param->init) {
				xfprintf(out, " = ");
				emit_ast_expr(param->init, scope, out, tmpls);
			}
		} else {
			xfprintf(out, "class");
			if (param->default_type) {
				xfprintf(out, " = ");
				emit_ast_type(param->default_type,
					scope, out, tmpls);
			}
		}
		if (param->is_pack) {
			xfprintf(out, "...");
		}
		if (param->next) {
			xfprintf(out, ", ");
		}
	}
	xfprintf(out, ">");
}

static void
emit_ast_identifier(const struct identifier *ident,
	const struct ast_template_argument *tmpl_args,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	struct identifier adjusted = *ident;
	adjust_reserved_ident(&adjusted);
	if (scope) {
		const struct scope_object *obj = scope_lookup(scope, &adjusted);
		if (obj) {
			adjusted = obj->ident; // get fully qualified name
			adjust_reserved_ident(&adjusted);
		}
	}
	char *identstr = identifier_unparse(&adjusted);
	xfprintf(out, "%s", identstr);
	free(identstr);
	if (tmpl_args) {
		const struct template *tmpl = get_template(ident, tmpls);
		assert(tmpl);
		emit_ast_template_args(tmpl_args, tmpl, scope, out, tmpls);
	}
}

static void
emit_ast_struct(const struct ast_type *type,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	xfprintf(out, "%s %s{ ",
		type->storage == STORAGE_STRUCT ? "struct" : "union",
		type->struct_union.packed ? "@packed " : "");
	for (const struct ast_struct_union_field *f = &type->struct_union.fields;
			f; f = f->next) {
		if (f->offset) {
			xfprintf(out, "@offset(");
			emit_ast_expr(f->offset, scope, out, tmpls);
			xfprintf(out, ") ");
		}
		if (f->name) {
			xfprintf(out, "%s: ", f->name);
		}
		emit_ast_type(f->type, scope, out, tmpls);
		xfprintf(out, ", ");
	}
	xfprintf(out, "}");
}

void
emit_ast_prototype(const struct ast_function_type *func,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	xfprintf(out, "(");
	for (const struct ast_function_parameters *param = func->params;
			param; param = param->next) {
		if (param->name) {
			xfprintf(out, "%s: ", param->name);
			assert(param->type);
		}
		if (param->type) {
			emit_ast_type(param->type, scope, out, tmpls);
		}
		if (param->variadic) {
			xfprintf(out, "...");
		}
		if (param->next) {
			xfprintf(out, ", ");
		}
	}
	xfprintf(out, ") ");
	emit_ast_type(func->result, scope, out, tmpls);
}

void
emit_ast_type(const struct ast_type *type,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	if (type->flags & TYPE_CONST) {
		xfprintf(out, "const ");
	}
	if (type->flags & TYPE_ERROR) {
		xfprintf(out, "!");
	}

	switch (type->storage) {
	case STORAGE_BOOL:
	case STORAGE_F32:
	case STORAGE_F64:
	case STORAGE_I16:
	case STORAGE_I32:
	case STORAGE_I64:
	case STORAGE_I8:
	case STORAGE_INT:
	case STORAGE_NEVER:
	case STORAGE_NULL:
	case STORAGE_OPAQUE:
	case STORAGE_RUNE:
	case STORAGE_SIZE:
	case STORAGE_STRING:
	case STORAGE_U16:
	case STORAGE_U32:
	case STORAGE_U64:
	case STORAGE_U8:
	case STORAGE_UINT:
	case STORAGE_UINTPTR:
	case STORAGE_VALIST:
	case STORAGE_VOID:
		xfprintf(out, "%s", type_storage_unparse(type->storage));
		break;
	case STORAGE_ALIAS:
		if (type->unwrap) {
			xfprintf(out, "...");
		}
		emit_ast_identifier(&type->alias, type->tmpl_args,
			scope, out, tmpls);
		break;
	case STORAGE_ARRAY:
		if (type->array.length) {
			xfprintf(out, "[");
			emit_ast_expr(type->array.length, scope, out, tmpls);
			xfprintf(out, "]");
		} else if (type->array.contextual) {
			xfprintf(out, "[_]");
		} else {
			xfprintf(out, "[*]");
		}
		emit_ast_type(type->array.members, scope, out, tmpls);
		break;
	case STORAGE_FUNCTION:
		xfprintf(out, "fn");
		emit_ast_prototype(&type->func, scope, out, tmpls);
		break;
	case STORAGE_POINTER:
		if (type->pointer.flags & PTR_NULLABLE) {
			xfprintf(out, "nullable ");
		}
		xfprintf(out, "*");
		emit_ast_type(type->pointer.referent, scope, out, tmpls);
		break;
	case STORAGE_SLICE:
		xfprintf(out, "[]");
		emit_ast_type(type->slice.members, scope, out, tmpls);
		break;
	case STORAGE_STRUCT:
	case STORAGE_UNION:
		emit_ast_struct(type, scope, out, tmpls);
		break;
	case STORAGE_TAGGED:
		xfprintf(out, "(");
		for (const struct ast_tagged_union_type *tu = &type->tagged;
				tu; tu = tu->next) {
			emit_ast_type(tu->type, scope, out, tmpls);
			if (tu->variadic) {
				xfprintf(out, "...");
			}
			if (tu->next || (tu->variadic && tu == &type->tagged)) {
				xfprintf(out, " | ");
			}
		}
		xfprintf(out, ")");
		break;
	case STORAGE_TUPLE:
		xfprintf(out, "(");
		for (const struct ast_tagged_union_type *tuple = &type->tagged;
				tuple; tuple = tuple->next) {
			emit_ast_type(tuple->type, scope, out, tmpls);
			if (tuple->variadic) {
				xfprintf(out, "...");
			}
			if (tuple->next) {
				xfprintf(out, ", ");
			}
		}
		xfprintf(out, ")");
		break;
	case STORAGE_ENUM:
	case STORAGE_ERROR:
	case STORAGE_FCONST:
	case STORAGE_ICONST:
	case STORAGE_RCONST:
		assert(0); // unreachable
	}
}

static const char *
binop_unparse(enum binarithm_operator op)
{
	switch (op) {
	case BIN_BAND:
		return "&";
	case BIN_BOR:
		return "|";
	case BIN_BXOR:
		return "^";
	case BIN_DIV:
		return "/";
	case BIN_GREATER:
		return ">";
	case BIN_GREATEREQ:
		return ">=";
	case BIN_LAND:
		return "&&";
	case BIN_LEQUAL:
		return "==";
	case BIN_LESS:
		return "<";
	case BIN_LESSEQ:
		return "<=";
	case BIN_LOR:
		return "||";
	case BIN_LSHIFT:
		return "<<";
	case BIN_LXOR:
		return "^^";
	case BIN_MINUS:
		return "-";
	case BIN_MODULO:
		return "%";
	case BIN_NEQUAL:
		return "!=";
	case BIN_PLUS:
		return "+";
	case BIN_RSHIFT:
		return ">>";
	case BIN_TIMES:
		return "*";
	}

	assert(0); // unreachable
}

static const char *
unop_unparse(enum unarithm_operator op)
{
	switch (op) {
	case UN_ADDRESS:
		return "&";
	case UN_BNOT:
		return "~";
	case UN_DEREF:
		return "*";
	case UN_LNOT:
		return "!";
	case UN_MINUS:
		return "-";
	}

	assert(0); // unreachable
}

static void
emit_ast_binding(const struct ast_expression_binding *binding,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	if (binding->unpack) {
		xfprintf(out, "(");
		for (const struct ast_binding_unpack *u = binding->unpack;
				u; u = u->next) {
			xfprintf(out, "%s", u->name);
			if (u->next) {
				xfprintf(out, ", ");
			}
		}
		xfprintf(out, ")");
	} else {
		xfprintf(out, "%s", binding->name);
	}
	if (binding->type) {
		xfprintf(out, ": ");
		emit_ast_type(binding->type, scope, out, tmpls);
	}
	xfprintf(out, " = ");
	emit_ast_expr(binding->initializer, scope, out, tmpls);
}

static void
emit_ast_expr_list(const struct ast_expression_list *exprs,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	while (exprs) {
		emit_ast_expr(exprs->expr, scope, out, tmpls);
		xfprintf(out, "; ");
		exprs = exprs->next;
	}
}

void
emit_ast_expr(const struct ast_expression *expr,
	struct scope *scope,
	FILE *out,
	templates tmpls)
{
	switch (expr->type) {
	case EXPR_ACCESS:
		switch (expr->access.type) {
		case ACCESS_IDENTIFIER:
			emit_ast_identifier(&expr->access.ident,
				expr->access.tmpl_args, scope, out, tmpls);
			break;
		case ACCESS_INDEX:
			emit_ast_expr(expr->access.array, scope, out, tmpls);
			xfprintf(out, "[");
			emit_ast_expr(expr->access.index, scope, out, tmpls);
			xfprintf(out, "]");
			break;
		case ACCESS_FIELD:
			emit_ast_expr(expr->access._struct, scope, out, tmpls);
			xfprintf(out, ".%s", expr->access.field);
			break;
		case ACCESS_TUPLE:
			emit_ast_expr(expr->access.tuple, scope, out, tmpls);
			xfprintf(out, ".");
			emit_ast_expr(expr->access.value, scope, out, tmpls);
			break;
		}
		break;
	case EXPR_ALLOC:
		xfprintf(out, "alloc(");
		emit_ast_expr(expr->alloc.init, scope, out, tmpls);
		switch (expr->alloc.kind) {
		case ALLOC_OBJECT:
			xfprintf(out, ")");
			break;
		case ALLOC_WITH_CAP:
		case ALLOC_WITH_LEN:
			xfprintf(out, ", ");
			emit_ast_expr(expr->alloc.cap, scope, out, tmpls);
			xfprintf(out, ")");
			break;
		case ALLOC_COPY:
			xfprintf(out, "...)");
			break;
		}
		break;
	case EXPR_APPEND:
	case EXPR_INSERT:
		xfprintf(out, "%s%s(", expr->append.is_static ? "static " : "",
			expr->type == EXPR_APPEND ? "append" : "insert");
		emit_ast_expr(expr->append.object, scope, out, tmpls);
		xfprintf(out, ", ");
		emit_ast_expr(expr->append.value, scope, out, tmpls);
		if (expr->append.is_multi) {
			xfprintf(out, "...");
		}
		if (expr->append.length) {
			xfprintf(out, ", ");
			emit_ast_expr(expr->append.length, scope, out, tmpls);
		}
		xfprintf(out, ")");
		break;
	case EXPR_ASSERT:
		if (expr->assert.is_static) {
			xfprintf(out, "static ");
		}
		if (expr->assert.cond) {
			xfprintf(out, "assert(");
			emit_ast_expr(expr->assert.cond, scope, out, tmpls);
			if (expr->assert.message) {
				xfprintf(out, ",");
			}
		} else {
			xfprintf(out, "abort(");
		}
		if (expr->assert.message) {
			emit_ast_expr(expr->assert.message, scope, out, tmpls);
		}
		xfprintf(out, ")");
		break;
	case EXPR_ASSIGN:
		xfprintf(out, "(");
		emit_ast_expr(expr->assign.object, scope, out, tmpls);
		xfprintf(out, " ");
		if (expr->assign.op != BIN_LEQUAL) {
			xfprintf(out, "%s", binop_unparse(expr->assign.op));
		}
		xfprintf(out, "= ");
		emit_ast_expr(expr->assign.value, scope, out, tmpls);
		xfprintf(out, ")");
		break;
	case EXPR_BINARITHM:
		xfprintf(out, "(");
		if (expr->binarithm.lvalue) {
			xfprintf(out, "(");
			emit_ast_expr(expr->binarithm.lvalue,
				scope, out, tmpls);
			xfprintf(out, ")");
			if (expr->binarithm.rvalue && expr->binarithm.fold) {
				xfprintf(out, " %s ...",
					binop_unparse(expr->binarithm.op));
			}
		} else {
			xfprintf(out, "...");
		}
		xfprintf(out, " %s ", binop_unparse(expr->binarithm.op));
		if (expr->binarithm.rvalue) {
			xfprintf(out, "(");
			emit_ast_expr(expr->binarithm.rvalue,
				scope, out, tmpls);
			xfprintf(out, ")");
		} else {
			xfprintf(out, "...");
		}
		xfprintf(out, ")");
		break;
	case EXPR_BINDING:
	case EXPR_DEFINE:
		xfprintf(out, "%s%s ",
			expr->binding.is_static ? "static " : "",
			expr->type == EXPR_BINDING ? "let" : "def");
		for (const struct ast_expression_binding *binding = &expr->binding;
				binding; binding = binding->next) {
			emit_ast_binding(binding, scope, out, tmpls);
			if (binding->next) {
				xfprintf(out, ", ");
			}
		}
		break;
	case EXPR_BREAK:
	case EXPR_CONTINUE:
		xfprintf(out, expr->type == EXPR_BREAK ? "break" : "continue");
		if (expr->control.label) {
			xfprintf(out, " :%s", expr->control.label);
		}
		break;
	case EXPR_CALL:
		emit_ast_expr(expr->call.lvalue, scope, out, tmpls);
		xfprintf(out, "(");
		for (const struct ast_call_argument *arg = expr->call.args;
				arg; arg = arg->next) {
			emit_ast_expr(arg->value, scope, out, tmpls);
			if (arg->variadic) {
				xfprintf(out, "...");
			}
			if (arg->next) {
				xfprintf(out, ", ");
			}
		}
		xfprintf(out, ")");
		break;
	case EXPR_CAST:
		xfprintf(out, "(");
		emit_ast_expr(expr->cast.value, scope, out, tmpls);
		switch (expr->cast.kind) {
		case C_CAST:
			xfprintf(out, ": ");
			break;
		case C_ASSERTION:
			xfprintf(out, "as ");
			break;
		case C_TEST:
			xfprintf(out, "is ");
			break;
		}
		emit_ast_type(expr->cast.type, scope, out, tmpls);
		xfprintf(out, ")");
		break;
	case EXPR_COMPOUND:
		if (expr->compound.label) {
			xfprintf(out, ":%s ", expr->compound.label);
		}
		xfprintf(out, "{ ");
		emit_ast_expr_list(&expr->compound.list, scope, out, tmpls);
		xfprintf(out, "}");
		break;
	case EXPR_DEFER:
		xfprintf(out, "defer ");
		emit_ast_expr(expr->defer.deferred, scope, out, tmpls);
		break;
	case EXPR_DELETE:
		xfprintf(out, "%sdelete(",
			expr->delete.is_static ? "static " : "");
		emit_ast_expr(expr->delete.expr, scope, out, tmpls);
		xfprintf(out, ")");
		break;
	case EXPR_FOR:
		xfprintf(out, "for ");
		if (expr->_for.label) {
			xfprintf(out, ":%s ", expr->_for.label);
		}
		xfprintf(out, "(");
		if (expr->_for.bindings) {
			emit_ast_expr(expr->_for.bindings, scope, out, tmpls);
			xfprintf(out, "; ");
		}
		emit_ast_expr(expr->_for.cond, scope, out, tmpls);
		if (expr->_for.afterthought) {
			xfprintf(out, "; ");
			emit_ast_expr(expr->_for.afterthought,
				scope, out, tmpls);
		}
		xfprintf(out, ") ");
		emit_ast_expr(expr->_for.body, scope, out, tmpls);
		break;
	case EXPR_FREE:
		xfprintf(out, "free(");
		emit_ast_expr(expr->free.expr, scope, out, tmpls);
		xfprintf(out, ")");
		break;
	case EXPR_IF:
		xfprintf(out, "if (");
		emit_ast_expr(expr->_if.cond, scope, out, tmpls);
		xfprintf(out, ") ");
		emit_ast_expr(expr->_if.true_branch, scope, out, tmpls);
		if (expr->_if.false_branch) {
			xfprintf(out, " else ");
			emit_ast_expr(expr->_if.false_branch,
				scope, out, tmpls);
		}
		break;
	case EXPR_MEASURE:
		switch (expr->measure.op) {
		case M_ALIGN:
			xfprintf(out, "align(");
			emit_ast_type(expr->measure.type, scope, out, tmpls);
			break;
		case M_LEN:
			xfprintf(out, "len(");
			emit_ast_expr(expr->measure.value, scope, out, tmpls);
			break;
		case M_SIZE:
			xfprintf(out, "size(");
			emit_ast_type(expr->measure.type, scope, out, tmpls);
			break;
		case M_SIZEPACK:
			xfprintf(out, "size...(");
			emit_ast_identifier(&expr->measure.ident,
				NULL, scope, out, tmpls);
			break;
		case M_OFFSET:
			xfprintf(out, "offset(");
			emit_ast_expr(expr->measure.value, scope, out, tmpls);
			break;
		}
		xfprintf(out, ")");
		break;
	case EXPR_LITERAL:
		switch (expr->literal.storage) {
		case STORAGE_BOOL:
			xfprintf(out, "%s",
				expr->literal.bval ? "true" : "false");
			break;
		case STORAGE_F32:
		case STORAGE_F64:
		case STORAGE_FCONST:
			xfprintf(out, "%a%s", expr->literal.fval,
				storage_to_suffix(expr->literal.storage));
			break;
		case STORAGE_I16:
		case STORAGE_I32:
		case STORAGE_I64:
		case STORAGE_I8:
		case STORAGE_ICONST:
		case STORAGE_INT:
			xfprintf(out, "%" PRIi64 "%s", expr->literal.ival,
				storage_to_suffix(expr->literal.storage));
			break;
		case STORAGE_NULL:
			xfprintf(out, "null");
			break;
		case STORAGE_SIZE:
		case STORAGE_U16:
		case STORAGE_U32:
		case STORAGE_U64:
		case STORAGE_U8:
		case STORAGE_UINT:
			xfprintf(out, "%" PRIu64 "%s", expr->literal.uval,
				storage_to_suffix(expr->literal.storage));
			break;
		case STORAGE_VOID:
			xfprintf(out, "void");
			break;
		case STORAGE_RCONST:
		case STORAGE_RUNE:
			xfprintf(out, "\'\\U%08" PRIx32 "\'",
				expr->literal.rune);
			break;
		case STORAGE_STRING:
			xfprintf(out, "\"");
			for (size_t i = 0; i < expr->literal.string.len; i++) {
				char c = expr->literal.string.value[i];
				if (isalnum((unsigned char)c)) {
					xfprintf(out, "%c", c);
				} else {
					xfprintf(out, "\\x%02X", c);
				}
			}
			xfprintf(out, "\"");
			break;
		case STORAGE_ARRAY:
			xfprintf(out, "[");
			for (const struct ast_array_literal *item = expr->literal.array;
					item; item = item->next) {
				emit_ast_expr(item->value, scope, out, tmpls);
				if (item->expand) {
					xfprintf(out, "...");
				}
				if (item->next) {
					xfprintf(out, ", ");
				}
			}
			xfprintf(out, "]");
			break;
		case STORAGE_ALIAS:
		case STORAGE_ENUM:
		case STORAGE_ERROR:
		case STORAGE_FUNCTION:
		case STORAGE_NEVER:
		case STORAGE_OPAQUE:
		case STORAGE_POINTER:
		case STORAGE_SLICE:
		case STORAGE_STRUCT:
		case STORAGE_TAGGED:
		case STORAGE_TUPLE:
		case STORAGE_UINTPTR:
		case STORAGE_UNION:
		case STORAGE_VALIST:
			assert(0); // unreachable
		}
		break;
	case EXPR_MATCH:
		xfprintf(out, "match ");
		if (expr->match.label) {
			xfprintf(out, ":%s ", expr->match.label);
		}
		xfprintf(out, "(");
		emit_ast_expr(expr->match.value, scope, out, tmpls);
		xfprintf(out, ") { ");
		for (const struct ast_match_case *c = expr->match.cases;
				c; c = c->next) {
			xfprintf(out, "case");
			if (c->name) {
				xfprintf(out, " let %s", c->name);
				if (c->type) {
					xfprintf(out, ":");
				}
			}
			if (c->type) {
				xfprintf(out, " ");
				emit_ast_type(c->type, scope, out, tmpls);
			}
			xfprintf(out, " => ");
			emit_ast_expr_list(&c->exprs, scope, out, tmpls);
		}
		xfprintf(out, "}");
		break;
	case EXPR_PROPAGATE:
		xfprintf(out, "(");
		emit_ast_expr(expr->propagate.value, scope, out, tmpls);
		xfprintf(out, ")%c", expr->propagate.abort ? '!' : '?');
		break;
	case EXPR_RETURN:
		xfprintf(out, "return");
		if (expr->_return.value) {
			xfprintf(out, " ");
			emit_ast_expr(expr->_return.value, scope, out, tmpls);
		}
		break;
	case EXPR_SLICE:
		emit_ast_expr(expr->slice.object, scope, out, tmpls);
		xfprintf(out, "[");
		if (expr->slice.start) {
			emit_ast_expr(expr->slice.start, scope, out, tmpls);
		}
		xfprintf(out, "..");
		if (expr->slice.end) {
			emit_ast_expr(expr->slice.end, scope, out, tmpls);
		}
		xfprintf(out, "]");
		break;
	case EXPR_STRUCT:
		if (expr->_struct.type.name) {
			emit_ast_identifier(&expr->_struct.type,
				expr->_struct.tmpl_args, scope, out, tmpls);
			xfprintf(out, " { ");
		} else {
			xfprintf(out, "struct { ");
		}
		for (const struct ast_field_value *f = expr->_struct.fields;
				f; f = f->next) {
			if (f->name) {
				xfprintf(out, "%s", f->name);
				if (f->type) {
					xfprintf(out, ": ");
				}
			}
			if (f->type) {
				emit_ast_type(f->type, scope, out, tmpls);
			}
			if (f->initializer) {
				xfprintf(out, " = ");
				emit_ast_expr(f->initializer,
					scope, out, tmpls);
			}
			xfprintf(out, ", ");
		}
		if (expr->_struct.autofill) {
			xfprintf(out, "... ");
		}
		xfprintf(out, "}");
		break;
	case EXPR_SWITCH:
		xfprintf(out, "switch ");
		if (expr->_switch.label) {
			xfprintf(out, ":%s ", expr->_switch.label);
		}
		xfprintf(out, "(");
		emit_ast_expr(expr->_switch.value, scope, out, tmpls);
		xfprintf(out, ") { ");
		for (const struct ast_switch_case *c = expr->_switch.cases;
				c; c = c->next) {
			xfprintf(out, "case");
			for (const struct ast_case_option *opt = c->options;
					opt; opt = opt->next) {
				xfprintf(out, " ");
				emit_ast_expr(opt->value, scope, out, tmpls);
				if (opt->variadic) {
					xfprintf(out, "...");
				}
				if (opt->next) {
					xfprintf(out, ",");
				}
			}
			xfprintf(out, " => ");
			emit_ast_expr_list(&c->exprs, scope, out, tmpls);
		}
		xfprintf(out, "}");
		break;
	case EXPR_TUPLE:
		xfprintf(out, "(");
		for (const struct ast_expression_tuple *tuple = &expr->tuple;
				tuple; tuple = tuple->next) {
			emit_ast_expr(tuple->expr, scope, out, tmpls);
			if (tuple->variadic) {
				xfprintf(out, "...");
			}
			if (tuple->next) {
				xfprintf(out, ", ");
			}
		}
		xfprintf(out, ")");
		break;
	case EXPR_UNARITHM:
		xfprintf(out, "(");
		xfprintf(out, "%s", unop_unparse(expr->unarithm.op));
		emit_ast_expr(expr->unarithm.operand, scope, out, tmpls);
		xfprintf(out, ")");
		break;
	case EXPR_VAARG:
		xfprintf(out, "vaarg(");
		emit_ast_expr(expr->vaarg.ap, scope, out, tmpls);
		xfprintf(out, ")");
		break;
	case EXPR_VAEND:
		xfprintf(out, "vaarg(");
		emit_ast_expr(expr->vaarg.ap, scope, out, tmpls);
		xfprintf(out, ")");
		break;
	case EXPR_VASTART:
		xfprintf(out, "vastart()");
		break;
	case EXPR_YIELD:
		xfprintf(out, "yield");
		if (expr->control.label) {
			xfprintf(out, " :%s", expr->control.label);
			if (expr->control.value) {
				xfprintf(out, ",");
			}
		}
		if (expr->control.value) {
			xfprintf(out, " ");
			emit_ast_expr(expr->control.value, scope, out, tmpls);
		}
		break;
	}
}
diff --git a/src/check.c b/src/check.c
index d6202e7..0297987 100644
--- a/src/check.c
+++ b/src/check.c
@@ -1,6 +1,7 @@
#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@@ -74,10 +75,16 @@ handle_errors(struct errors *errors)
{
	struct errors *error = errors;
	while (error) {
		xfprintf(stderr, "%s:%d:%d: error: %s\n", sources[error->loc.file],
			error->loc.lineno, error->loc.colno, error->msg);
		errline(error->loc);
		free(error->msg);
		xfprintf(stderr, "%s:%d:%d: ", sources[error->loc.file],
			error->loc.lineno, error->loc.colno);
		if (error->msg) {
			xfprintf(stderr, "error: %s\n", error->msg);
			errline(error->loc, 31);
			free(error->msg);
		} else {
			xfprintf(stderr, "note: specialized from here\n");
			errline(error->loc, 33);
		}
		struct errors *next = error->next;
		free(error);
		error = next;
@@ -112,6 +119,14 @@ verror(struct context *ctx, const struct location loc,
	next->loc = loc;
	next->msg = msg;
	ctx->next = &next->next;

	for (const struct error_context *errctx = ctx->errctx;
			errctx; errctx = errctx->next) {
		struct errors *next = *ctx->next =
			xcalloc(1, sizeof(struct errors));
		next->loc = errctx->loc;
		ctx->next = &next->next;
	}
}

void
@@ -139,6 +154,23 @@ error_norec(struct context *ctx, struct location loc, const char *fmt, ...)
	abort();
}

void
push_error_context(struct context *ctx, struct location loc)
{
	struct error_context *errctx = xcalloc(1, sizeof(struct error_context));
	errctx->loc = loc;
	errctx->next = ctx->errctx;
	ctx->errctx = errctx;
}

void
pop_error_context(struct context *ctx)
{
	struct error_context *errctx = ctx->errctx;
	ctx->errctx = ctx->errctx->next;
	free(errctx);
}

static struct expression *
lower_implicit_cast(struct context *ctx,
		const struct type *to, struct expression *expr)
@@ -154,19 +186,1257 @@ lower_implicit_cast(struct context *ctx,
			expr = lower_implicit_cast(ctx, interim, expr);
		}
	}

	struct expression *cast = xcalloc(1, sizeof(struct expression));
	cast->type = EXPR_CAST;
	cast->result = cast->cast.secondary = to;
	cast->cast.kind = C_CAST;
	cast->cast.value = expr;
	cast->cast.lowered = true;
	return cast;

	struct expression *cast = xcalloc(1, sizeof(struct expression));
	cast->type = EXPR_CAST;
	cast->result = cast->cast.secondary = to;
	cast->cast.kind = C_CAST;
	cast->cast.value = expr;
	cast->cast.lowered = true;
	return cast;
}

static void
resolve_unresolved(struct context *ctx)
{
	while (ctx->unresolved) {
		struct ast_types *unresolved = ctx->unresolved;
		ctx->unresolved = unresolved->next;
		type_store_lookup_atype(ctx, unresolved->type);
		free(unresolved);
	}
}

static void
reset_pack_positions(struct scope *scope)
{
	for (struct scope_object *obj = scope->objects; obj; obj = obj->lnext) {
		obj->pack_pos = 0;
	}
}

static void
deduce(struct context *ctx,
	struct scope *scope,
	const struct ast_template_parameters *tmpl_params,
	char *name,
	const struct type *type,
	struct expression *value)
{
	assert(!type != !value);
	struct identifier ident = {0};
	bool is_pack = false;
	for (const struct ast_template_parameters *p = tmpl_params;
			p; p = p->next) {
		if (!p->name) {
			continue;
		}
		if (strcmp(name, p->name) == 0) {
			if (!p->type == !value) {
				ident.name = name;
				is_pack = p->is_pack;
			}
			break;
		}
	}
	if (!ident.name) {
		return;
	}
	adjust_reserved_ident(&ident);

	struct scope_object *obj = scope_lookup_noparent(scope, &ident);
	if (!obj) {
		if (is_pack && type) {
			obj = scope_insert(scope, O_PACK, &ident,
				&ident, NULL, NULL);
			obj->pack_types = xcalloc(1, sizeof(struct types));
			obj->pack_types->type = lower_flexible(ctx, type, NULL);
			obj->pack_pos = 1;
		} else if (is_pack && !type) {
			obj = scope_insert(scope, O_PACK, &ident,
				&ident, NULL, NULL);
			obj->pack_values = xcalloc(1, sizeof(struct expressions));
			obj->pack_values->expr = value;
			obj->pack_pos = 1;
		} else {
			scope_insert(scope, type ? O_TYPE : O_CONST,
				&ident, &ident, type, value);
		}
	} else if (type) {
		switch (obj->otype) {
		case O_PACK:
			assert(!obj->type);
			// XXX: algorithm will be quadratic because of linked
			// list lol
			struct types **next = &obj->pack_types;
			for (size_t i = 0; *next && i < obj->pack_pos; i++) {
				next = &(*next)->next;
			}
			if (!*next) {
				*next = xcalloc(1, sizeof(struct types));
				(*next)->type = lower_flexible(ctx, type, NULL);
			}
			obj->pack_pos++;
			break;
		case O_TYPE:
			if (type_is_flexible(obj->type)) {
				const struct type *promoted =
					promote_flexible(ctx, obj->type, type);
				if (promoted) {
					obj->type = promoted;
				}
			}
			break;
		default:
			assert(0); // unreachable
		}
	}
}

static void
deduce_type(struct context *ctx,
	struct scope *scope,
	const struct ast_template_parameters *tmpl_params,
	const struct ast_type *param_type,
	const struct type *arg_type)
{
	switch (param_type->storage) {
	case STORAGE_ALIAS:
		// TODO: deduce template template parameters?
		if (param_type->tmpl_args == NULL && !param_type->unwrap
				&& param_type->alias.ns == NULL) {
			deduce(ctx, scope, tmpl_params, param_type->alias.name,
				arg_type, NULL);
		}
		break;
	case STORAGE_ARRAY:
		if (arg_type->storage != STORAGE_ARRAY) {
			break;
		}
		deduce_type(ctx, scope, tmpl_params, param_type->array.members,
			arg_type->array.members);

		// deduce expr from array length
		// (deduce_expr isn't used since the length is stored as size_t,
		// not struct expression or anything like that, so there's not
		// really a point. it doesn't really matter i guess idk)
		if (arg_type->array.length == SIZE_UNDEFINED
				|| param_type->array.length == NULL) {
			break;
		}
		const struct ast_expression *len = param_type->array.length;
		if (len->type != EXPR_ACCESS
				|| len->access.type != ACCESS_IDENTIFIER
				|| len->access.ident.ns != NULL
				|| len->access.tmpl_args != NULL) {
			break;
		}

		struct expression *expr = xcalloc(1, sizeof(struct expression));
		expr->type = EXPR_LITERAL;
		expr->loc = param_type->loc;
		expr->result = &builtin_type_size;
		expr->literal.uval = arg_type->array.length;
		deduce(ctx, scope, tmpl_params, len->access.ident.name,
			NULL, expr);
		break;
	case STORAGE_FUNCTION:
		reset_pack_positions(scope);
		if (arg_type->storage != STORAGE_FUNCTION) {
			break;
		}
		deduce_type(ctx, scope, tmpl_params, param_type->func.result,
			arg_type->func.result);

		const struct ast_function_parameters *aparam =
			param_type->func.params;
		for (const struct type_func_param *param = arg_type->func.params;
				param && aparam && aparam->type; param = param->next) {
			const struct type *type;
			if (arg_type->func.variadism == VARIADISM_HARE
					&& aparam->variadic && param->next == NULL) {
				assert(param->type->storage == STORAGE_SLICE);
				type = param->type->array.members;
			} else {
				type = param->type;
			}

			deduce_type(ctx, scope, tmpl_params,
				aparam->type, type);
			if (!aparam->variadic) {
				aparam = aparam->next;
			}
		}
		break;
	case STORAGE_POINTER:
		if (arg_type->storage == STORAGE_POINTER) {
			deduce_type(ctx, scope, tmpl_params,
				param_type->pointer.referent,
				arg_type->pointer.referent);
		}
		break;
	case STORAGE_SLICE:
		switch (arg_type->storage) {
		case STORAGE_POINTER:
			arg_type = arg_type->pointer.referent;
			if (arg_type->storage != STORAGE_ARRAY) {
				return;
			}
			break;
		case STORAGE_ARRAY:
		case STORAGE_SLICE:
			break;
		default:
			return;
		}

		deduce_type(ctx, scope, tmpl_params, param_type->slice.members,
			arg_type->array.members);
		break;
	case STORAGE_STRUCT:
	case STORAGE_TAGGED:
	case STORAGE_TUPLE:
	case STORAGE_UNION:
		break; // TODO?
	default:
		break;
	}
}

static void
deduce_expr(struct context *ctx,
	struct scope *scope,
	const struct ast_template_parameters *tmpl_params,
	const struct ast_expression *param_expr,
	struct expression *arg_expr)
{
	if (param_expr->type == EXPR_ACCESS
			&& param_expr->access.type == ACCESS_IDENTIFIER
			&& param_expr->access.ident.ns == NULL
			&& param_expr->access.tmpl_args == NULL) {
		deduce(ctx, scope, tmpl_params, param_expr->access.ident.name,
			NULL, arg_expr);
	}
}

union pack {
	struct types **types;
	struct expressions **values;
};

// returns true on success, false on error
static bool
init_tmpl_arg_explicit(struct context *ctx,
	struct identifier *ident,
	const struct ast_template_parameters *p,
	const struct ast_template_argument *arg,
	union pack *pack_next,
	int idx)
{
	if (p->type != NULL) {
		const struct type *type = type_store_lookup_atype(ctx, p->type);
		struct expression *expr = xcalloc(1, sizeof(struct expression));
		check_expression(ctx, arg->expr, expr, type);

		if (!type_is_assignable(ctx, type, expr->result)) {
			char *argtypename = gen_typename(expr->result);
			char *paramtypename = gen_typename(type);
			error(ctx, expr->loc, NULL,
				"Argument type %s is not assignable to parameter type %s",
				argtypename, paramtypename);
			free(argtypename);
			free(paramtypename);
			return false;
		}
		expr = lower_implicit_cast(ctx, type, expr);

		struct expression *evaled = xcalloc(1,
			sizeof(struct expression));
		bool success = eval_expr(ctx, expr, evaled);
		free(expr);
		if (!success) {
			return false;
		}
		evaled->tmpl_param_idx = idx;
		if (pack_next->values == NULL) {
			adjust_reserved_ident(ident);
			scope_insert(ctx->scope, O_CONST, ident,
				ident, NULL, evaled);
		} else {
			// argument in parameter pack
			*pack_next->values = xcalloc(1,
				sizeof(struct expressions));
			(*pack_next->values)->expr = evaled;
			pack_next->values = &(*pack_next->values)->next;
		}
	} else {
		struct type *type = xcalloc(1, sizeof(struct type));
		*type = *type_store_lookup_atype(ctx, arg->type);
		type->tmpl_param_idx = idx;
		if (pack_next->types == NULL) {
			adjust_reserved_ident(ident);
			scope_insert(ctx->scope, O_TYPE,
				ident, ident, type, NULL);
		} else {
			// argument in parameter pack
			*pack_next->types = xcalloc(1, sizeof(struct types));
			(*pack_next->types)->type = type;
			pack_next->types = &(*pack_next->types)->next;
		}
	}

	return true;
}

// returns true on success, false on error
static bool
check_explicit_tmpl_arg(struct context *ctx,
	struct scope *scope,
	struct scope *imports,
	const struct ast_template_parameters **params,
	const struct ast_template_argument **args,
	union pack *pack_next,
	int *idx)
{
	struct scope *old_subunit = ctx->unit->parent;
	struct scope *old_scope = ctx->scope;

	const struct ast_template_parameters *p = *params;
	const struct ast_template_argument *arg = *args;
	struct identifier ident;
	tmpl_param_scope_ident(&ident, p->name, *idx);
	ident.name = xstrdup(ident.name); // extend lifetime

	if (p->is_pack && pack_next->values == NULL) {
		// start of parameter pack
		struct scope_object *new = scope_insert(scope,
			O_PACK, &ident, &ident, NULL, NULL);
		if (p->type != NULL) {
			ctx->unit->parent = imports;
			ctx->scope = scope;
			new->type = type_store_lookup_atype(ctx, p->type);
			ctx->scope = old_scope;
			ctx->unit->parent = old_subunit;
		};
		// `struct types` and `struct expressions` have the same
		// representation, so this works for both expression packs and
		// type packs
		pack_next->values = &new->pack_values;
	}

	ctx->unit->parent = imports;
	ctx->scope = scope;
	bool res = init_tmpl_arg_explicit(ctx, &ident, p, arg, pack_next, *idx);
	ctx->scope = old_scope;
	ctx->unit->parent = old_subunit;
	free(ident.name);
	if (!res) {
		return false;
	}

	if (pack_next->values == NULL || arg->next == NULL) {
		*params = p->next;
		++*idx;
	}
	*args = arg->next;
	return true;
}

static void
deduce_tmpl_args_from_func_params(struct context *ctx,
	struct scope *scope,
	const struct ast_template_parameters *p,
	const struct ast_function_parameters *fp,
	const struct types *params)
{
	reset_pack_positions(scope);
	while (fp != NULL && params != NULL) {
		// no need to do anything different if fp->variadic is true: if
		// anything can be deduced then it's guaranteed to be pack
		// expansion; not c-style variadism. so just iterate over the
		// args and add them to the pack (handled in deduce)
		deduce_type(ctx, scope, p, fp->type, params->type);
		if (!fp->variadic) {
			fp = fp->next;
		}
		params = params->next;
	}
}

static void
deduce_tmpl_args_from_ast_func_params(struct context *ctx,
	struct scope *scope,
	const struct ast_template_parameters *p,
	const struct ast_function_parameters *fp,
	const struct ast_function_parameters *params)
{
	reset_pack_positions(scope);

	size_t fp_count = 0, params_count = 0, params_max = 0;
	struct scope *old_scope = ctx->scope;
	struct scope *exp_scope = ctx->scope;
	scope_push(&exp_scope, SCOPE_DEFINES);

	while (fp != NULL && params != NULL) {
		if (fp->variadic && fp_count == 0) {
			ctx->scope = scope;
			size_t n = type_pack_expand(ctx, NULL, fp->type, -1);
			ctx->scope = old_scope;
			if (n == 0) {
				fp = fp->next;
				continue;
			} else if (n != (size_t)-1) {
				fp_count = n;
			}
		}
		if (params->variadic && params_count == 0) {
			size_t n = type_pack_expand(ctx, NULL, params->type, -1);
			if (n == 0) {
				params = params->next;
				continue;
			} else if (n != (size_t)-1) {
				params_count = params_max = n;
			}
		}

		if (params_count > 0) {
			type_pack_expand(ctx, exp_scope, params->type,
				params_max - params_count);
			ctx->scope = exp_scope;
		}
		const struct type *type =
			type_store_lookup_atype(ctx, params->type);
		ctx->scope = old_scope;
		deduce_type(ctx, scope, p, fp->type, type);

		fp_count--;
		if (fp_count == 0 || fp_count == (size_t)-1) {
			fp_count = 0;
			fp = fp->next;
		}

		params_count--;
		if (params_count == 0 || params_count == (size_t)-1) {
			params_count = 0;
			params = params->next;
		}
	}
}

static bool
tmpl_prototypes_compat(struct context *ctx,
	struct scope *scope,
	struct scope *sp_scope,
	struct scope *imports,
	struct scope *sp_imports,
	const struct ast_function_parameters *fp,
	const struct ast_function_parameters *params)
{
	// TODO: would be nice if some of this stuff errored out instead of just
	// saying they're incompatible (like, if the number of parameters in
	// each prototype is different, and stuff like that)

	size_t fp_count = 0, params_count = 0, fp_max = 0, params_max = 0;
	struct scope *exp_scope = scope;
	scope_push(&exp_scope, SCOPE_DEFINES);
	struct scope *exp_sp_scope = sp_scope;
	scope_push(&exp_sp_scope, SCOPE_DEFINES);

	while (fp != NULL && params != NULL) {
		if (fp->variadic && fp_count == 0) {
			ctx->unit->parent = imports;
			ctx->scope = scope;
			size_t n = type_pack_expand(ctx, NULL, fp->type, -1);
			if (n == 0) {
				fp = fp->next;
				continue;
			} else if (n != (size_t)-1) {
				fp_count = fp_max = n;
			}
		}
		if (params->variadic && params_count == 0) {
			ctx->unit->parent = sp_imports;
			ctx->scope = sp_scope;
			size_t n = type_pack_expand(ctx, NULL, params->type, -1);
			if (n == 0) {
				params = params->next;
				continue;
			} else if (n != (size_t)-1) {
				params_count = params_max = n;
			}
		}

		if (fp->variadic != params->variadic
				&& ((!fp_count && !params_count)
					|| !fp->variadic == !params_count)) {
			return false;
		}

		ctx->unit->parent = imports;
		ctx->scope = scope;
		if (fp_count > 0) {
			type_pack_expand(ctx, exp_scope, fp->type,
				fp_max - fp_count);
			ctx->scope = exp_scope;
		}
		const struct type *unspec =
			type_store_lookup_atype(ctx, fp->type);

		ctx->unit->parent = sp_imports;
		ctx->scope = sp_scope;
		if (params_count > 0) {
			type_pack_expand(ctx, exp_sp_scope, params->type,
				params_max - params_count);
			ctx->scope = exp_sp_scope;
		}
		const struct type *spec =
			type_store_lookup_atype(ctx, params->type);

		if (unspec->id != spec->id) {
			return false;
		}

		fp_count--;
		if (fp_count == 0 || fp_count == (size_t)-1) {
			fp_count = 0;
			fp = fp->next;
		}

		params_count--;
		if (params_count == 0 || params_count == (size_t)-1) {
			params_count = 0;
			params = params->next;
		}
	}

	return fp == NULL && params == NULL;
}

// returns true on success, false on error
// XXX: it might maybe be possible to deduplicate some of this stuff from
// init_tmpl_arg_explicit
static bool
init_tmpl_arg_default(struct context *ctx,
	const struct identifier *ident,
	const struct ast_template_parameters *p,
	int idx)
{
	if (p->type != NULL) {
		const struct type *type = type_store_lookup_atype(ctx, p->type);
		struct expression *expr = xcalloc(1, sizeof(struct expression));
		check_expression(ctx, p->init, expr, type);

		if (!type_is_assignable(ctx, type, expr->result)) {
			char *argtypename = gen_typename(expr->result);
			char *paramtypename = gen_typename(type);
			error(ctx, expr->loc, NULL,
				"Argument type %s is not assignable to parameter type %s",
				argtypename, paramtypename);
			free(argtypename);
			free(paramtypename);
			return false;
		}
		expr = lower_implicit_cast(ctx, type, expr);

		struct expression *evaled = xcalloc(1,
			sizeof(struct expression));
		bool success = eval_expr(ctx, expr, evaled);
		free(expr);
		if (!success) {
			return false;
		}
		evaled->tmpl_param_idx = idx;
		scope_insert(ctx->scope, O_CONST, ident, ident, NULL, evaled);
	} else {
		struct type *type = xcalloc(1, sizeof(struct type));
		*type = *type_store_lookup_atype(ctx, p->default_type);

		type->tmpl_param_idx = idx;
		scope_insert(ctx->scope, O_TYPE, ident, ident, type, NULL);
	}

	return true;
}

// returns true on success, false on error
static bool
check_deduced_tmpl_arg(struct context *ctx,
	struct scope *scope,
	struct scope *imports,
	const struct ast_template_parameters *p,
	int idx)
{
	struct scope *old_subunit = ctx->unit->parent;
	struct scope *old_scope = ctx->scope;

	struct identifier ident;
	tmpl_param_scope_ident(&ident, p->name, idx);
	ident.name = xstrdup(ident.name); // extend lifetime

	struct scope_object *obj = scope_lookup_noparent(scope, &ident);
	if (obj != NULL) {
		// parameter was deduced
		switch (obj->otype) {
		case O_CONST:
			assert(obj->value->tmpl_param_idx == 0);
			obj->value->tmpl_param_idx = idx;
			break;
		case O_TYPE:;
			struct type *type = xcalloc(1, sizeof(struct type));
			*type = *obj->type;
			type->tmpl_param_idx = idx;
			obj->type = lower_flexible(ctx, type, NULL);
			break;
		default:
			assert(obj->otype == O_PACK);
			break;
		}

		free(ident.name);
		return true;
	}

	if (p->is_pack) {
		// insert empty pack into scope
		ctx->unit->parent = imports;
		ctx->scope = scope;
		const struct type *type = NULL;
		if (p->type != NULL) {
			type = type_store_lookup_atype(ctx, p->type);
		}
		scope_insert(scope, O_PACK, &ident, &ident, type, NULL);

		ctx->scope = old_scope;
		ctx->unit->parent = old_subunit;
		free(ident.name);
		return true;
	}
	if (p->init == NULL) {
		error(ctx, p->loc, NULL, "Argument can't be deduced");
		free(ident.name);
		return false;
	}

	// initialize to default argument
	ctx->unit->parent = imports;
	ctx->scope = scope;
	bool res = init_tmpl_arg_default(ctx, &ident, p, idx);
	ctx->scope = old_scope;
	ctx->unit->parent = old_subunit;
	free(ident.name);
	return res;
}

static void
push_dummy_pack_scope(struct scope **scope,
	const struct ast_template_parameters *params)
{
	scope_push(scope, SCOPE_DEFINES);
	for (const struct scope_object *obj = (*scope)->parent->objects;
			obj; obj = obj->lnext) {
		if (obj->otype == O_PACK) {
			int idx = 1;
			const struct ast_template_parameters *p;
			for (p = params; p; p = p->next, idx++) {
				if (strcmp(obj->name.name, p->name) == 0) {
					break;
				}
			}
			assert(p != NULL);

			// int is just a dummy type. it just needs to be a type
			// with nonzero definite size
			struct type *type = xcalloc(1, sizeof(struct type));
			*type = builtin_type_int;
			type->tmpl_param_idx = idx;
			scope_insert(*scope, O_TYPE, &obj->ident,
				&obj->name, type, NULL);
		}
	}
}

static struct type_func get_prototype(struct context *ctx,
	const struct ast_function_type *atype);

// XXX: this duplicates some logic from type_store; it's dumb. it's necessary
// because function types are treated differently, but everything else is the
// same
static const struct type *
get_prototype_param(struct context *ctx, const struct ast_type *atype)
{
	const struct type *ts_type;
	if ((atype->flags & TYPE_ERROR) == 0) {
		ts_type = builtin_type_for_storage(atype->storage,
			(atype->flags & TYPE_CONST) != 0);
		if (ts_type != NULL) {
			return ts_type;
		}
	}
	ts_type = type_store_lookup_atype(ctx, atype);
	// XXX: this is leaky, but so is the rest of harec so it doesn't really matter
	struct type *new_type = xcalloc(1, sizeof(struct type));
	*new_type = *ts_type;

	switch (atype->storage) {
	case STORAGE_ARRAY:
		new_type->array.members = get_prototype_param(ctx,
			atype->array.members);
		break;
	case STORAGE_FUNCTION:
		new_type->func = get_prototype(ctx, &atype->func);
		break;
	case STORAGE_POINTER:
		new_type->pointer.referent = get_prototype_param(ctx,
			atype->pointer.referent);
		break;
	default:
		free(new_type);
		return ts_type;
	}

	return new_type;
}

static struct type_func
get_prototype(struct context *ctx, const struct ast_function_type *atype)
{
	struct type_func prototype = {
		.result = type_store_lookup_atype(ctx, atype->result),
	};

	struct type_func_param **next = &prototype.params;
	for (const struct ast_function_parameters *p = atype->params;
			p; p = p->next) {
		if (p->variadic && p->type == NULL) {
			prototype.variadism = VARIADISM_C;
			assert(p->next == NULL);
			break;
		}

		*next = xcalloc(1, sizeof(struct type_func_param));
		(*next)->type = get_prototype_param(ctx, p->type);
		if (p->variadic) {
			(*next)->type = type_store_lookup_with_flags(
				ctx, (*next)->type, TYPE_EXPAND);
		}

		next = &(*next)->next;
	}

	return prototype;
}

static void
deduce_specialized_param(struct context *ctx,
	struct scope *scope,
	const struct ast_template_parameters *params,
	const struct ast_template_parameters *p,
	const struct ast_template_argument **arg,
	int idx)
{
	struct identifier ident;
	tmpl_param_scope_ident(&ident, p->name, idx);
	struct scope_object *obj = scope_lookup_noparent(scope, &ident);
	assert(obj != NULL);

	switch (obj->otype) {
	case O_CONST:
		assert(p->type != NULL);
		deduce_expr(ctx, ctx->scope, params, (*arg)->expr, obj->value);
		deduce_type(ctx, ctx->scope, params, p->type, obj->value->result);
		*arg = (*arg)->next;
		break;
	case O_PACK:
		// `struct types` and `struct expressions` have the same
		// representation, so this works for both expression packs and
		// type packs
		for (const struct expressions *pack = obj->pack_values;
				pack != NULL && *arg != NULL;
				pack = pack->next, *arg = (*arg)->next) {
			if (obj->type != NULL) {
				deduce_expr(ctx, ctx->scope, params,
					(*arg)->expr, pack->expr);
				deduce_type(ctx, ctx->scope, params,
					p->type, pack->expr->result);
			} else {
				deduce_type(ctx, ctx->scope, params, (*arg)->type,
					((const struct types *)pack)->type);
			}
		}
		*arg = NULL;
		break;
	case O_TYPE:
		assert(p->type == NULL);
		deduce_type(ctx, ctx->scope, params, (*arg)->type, obj->type);
		*arg = (*arg)->next;
		break;
	default:
		assert(0); // unreachable
	}
}

// returns true on success, false on error
static bool
match_specialization_arg_expr(struct context *ctx,
	const struct ast_expression *arg_expr,
	const struct expression *obj_value)
{
	struct expression *expr = xcalloc(1, sizeof(struct expression));
	check_expression(ctx, arg_expr, expr, obj_value->result);
	if (!type_is_assignable(ctx, obj_value->result, expr->result)) {
		// XXX: can this even happen in a valid program? like, is this
		// branch reachable? and if so, should this error out or just
		// return?
		return false;
	}
	expr = lower_implicit_cast(ctx, obj_value->result, expr);

	struct expression *evaled = xcalloc(1, sizeof(struct expression));
	bool success = eval_expr(ctx, expr, evaled);
	if (!success) {
		// XXX: should this properly error out?
		return false;
	}

	return literal_eq(ctx, evaled, obj_value);
}

// returns true on success, false on error
static bool
match_specialization_arg_type(struct context *ctx,
	const struct ast_type *arg_type,
	const struct type *obj_type)
{
	const struct type *type = type_store_lookup_atype(ctx, arg_type);
	return type->id == obj_type->id;
}

// returns true on success, false on error
static bool
match_specialization_arg(struct context *ctx,
	struct scope *scope,
	const struct ast_template_parameters *p,
	const struct ast_template_argument *arg,
	int idx)
{
	struct identifier ident;
	tmpl_param_scope_ident(&ident, p->name, idx);

	struct scope_object *obj = scope_lookup_noparent(scope, &ident);
	assert(obj != NULL);

	switch (obj->otype) {
	case O_CONST:
		return match_specialization_arg_expr(ctx, arg->expr, obj->value);
	case O_PACK:
		// `struct types` and `struct expressions` have the same
		// representation, so this works for both expression packs and
		// type packs
		for (const struct expressions *pack = obj->pack_values;
				pack != NULL && arg != NULL;
				pack = pack->next, arg = arg->next) {
			if (obj->type != NULL) {
				if (!match_specialization_arg_expr(ctx,
						arg->expr, pack->expr)) {
					return false;
				}
			} else {
				const struct type *type =
					((const struct types *)pack)->type;
				if (!match_specialization_arg_type(ctx,
						arg->type, type)) {
					return false;
				}
			}
		}
		return true;
	case O_TYPE:
		return match_specialization_arg_type(ctx, arg->type, obj->type);
	default:
		assert(0); // unreachable
	}
}

static int
match_specialization(struct context *ctx,
	struct scope *scope,
	struct scope *sp_scope,
	struct scope *imports,
	const struct template_specialization *sp,
	const struct ast_template_parameters *tmpl_params,
	const struct ast_function_parameters *fn_params)
{
	struct scope *old_subunit = ctx->unit->parent;
	struct scope *old_scope = ctx->scope;

	ctx->unit->parent = sp->imports;
	ctx->scope = sp_scope;

	reset_pack_positions(scope);
	const struct ast_template_parameters *p = tmpl_params;
	const struct ast_template_argument *arg = sp->args;
	int idx = 1;
	while (arg != NULL && arg->expr != NULL) {
		assert(p != NULL);
		deduce_specialized_param(ctx, scope, sp->params, p, &arg, idx);
		p = p->next;
		idx++;
	}

	if (fn_params != NULL) {
		deduce_tmpl_args_from_ast_func_params(ctx, scope, tmpl_params,
			fn_params, sp->type->func.params);
	}

	int ret = 0;
	for (p = sp->params; p; p = p->next) {
		struct identifier ident = {
			.name = p->name,
		};
		struct scope_object *obj =
			scope_lookup_noparent(sp_scope, &ident);
		if (obj == NULL) {
			return -1;
		}
		// TODO: not entirely sure if the logic for ret is correct. i
		// assume it probably isn't tbh
		ret++;
	}

	if (fn_params != NULL) {
		bool compat = tmpl_prototypes_compat(ctx, scope, sp_scope,
			imports, sp->imports, fn_params, sp->type->func.params);
		if (!compat) {
			ctx->scope = old_scope;
			ctx->unit->parent = old_subunit;
			return -1;
		}
	}

	ctx->unit->parent = sp->imports;
	ctx->scope = sp_scope;

	arg = sp->args;
	for (p = tmpl_params, idx = 1; p; p = p->next, idx++) {
		if (arg == NULL || arg->expr == NULL) {
			// if it's in the scope, it was deduced, so things are
			// fine i think
			struct identifier ident;
			tmpl_param_scope_ident(&ident, p->name, idx);
			struct scope_object *obj =
				scope_lookup_noparent(scope, &ident);
			if (obj == NULL) {
				// XXX: should this properly error out?
				ret = -1;
				break;
			}
			continue;
		}

		bool match = match_specialization_arg(ctx, scope, p, arg, idx);
		if (!match) {
			ret = -1;
			break;
		}
		if (p->is_pack) {
			// parameter pack was already fully matched in
			// match_specialization_arg
			break;
		}
		arg = arg->next;
	}

	ctx->scope = old_scope;
	ctx->unit->parent = old_subunit;
	return ret;
}

static struct scope_object *
check_specialization(struct context *ctx,
	struct scope *scope,
	struct scope *imports,
	struct scope *unit,
	const struct identifier *ident,
	enum ast_decl_type decl_type,
	struct location loc,
	struct ast_type *type,
	struct ast_expression *expr,
	bool threadlocal)
{
	struct scope *old_unit = ctx->unit;
	ctx->unit = ctx->defines->parent = unit;
	// wrap_resolver sets the scope to ctx->defines, so we change
	// ctx->defines to include the template parameters
	struct scope *old_defines = ctx->defines;
	ctx->defines = scope;

	struct incomplete_declaration *idecl = xcalloc(1,
		sizeof(struct incomplete_declaration));
	idecl->imports = imports;
	idecl->type = IDECL_DECL;
	idecl->decl.decl_type = decl_type;
	idecl->decl.loc = loc;

	switch (decl_type) {
	case ADECL_GLOBAL:
		idecl->decl.global.symbol = ident->name;
		idecl->decl.global.threadlocal = threadlocal;
		// fallthrough
	case ADECL_CONST:
		idecl->decl.global.ident = *ident;
		idecl->decl.global.type = type;
		idecl->decl.global.init = expr;
		break;
	case ADECL_FUNC:
		idecl->decl.function.symbol = ident->name;
		idecl->decl.function.ident = *ident;
		idecl->decl.function.prototype = type->func;
		idecl->decl.function.body = expr;
		break;
	case ADECL_TYPE:
		idecl->decl.type.ident = *ident;
		idecl->decl.type.type = type;
		break;
	case ADECL_ASSERT:
		assert(0); // unreachable
	}

	scope_object_init(&idecl->obj, O_SCAN, ident, ident, NULL, NULL);
	scope_insert_from_object(ctx->unit, &idecl->obj);
	wrap_resolver(ctx, &idecl->obj, resolve_decl);
	ctx->defines = old_defines;
	ctx->unit = ctx->defines->parent = old_unit;
	if (ctx->unit != unit) {
		scope_insert_from_object(ctx->unit, &idecl->obj);
	}
	return &idecl->obj;
}

struct scope_object *
specialize(struct context *ctx,
	const struct scope_object *obj,
	const struct ast_template_argument *tmpl_args,
	const struct types *fn_params)
{
	struct scope *old_subunit = ctx->unit->parent;
	struct scope *old_scope = ctx->scope;
	struct scope *scope = ctx->defines;
	scope_push(&scope, SCOPE_DEFINES);

	int idx = 1;
	assert(obj->otype == O_TEMPLATE);
	const struct ast_template_parameters *p = obj->template->params;
	union pack pack_next = {0};
	while (p != NULL && tmpl_args != NULL && tmpl_args->expr != NULL) {
		bool res = check_explicit_tmpl_arg(ctx, scope,
			obj->template->imports, &p,
			&tmpl_args, &pack_next, &idx);
		if (!res) {
			return NULL;
		}
	}

	if (tmpl_args != NULL && tmpl_args->expr != NULL) {
		error(ctx, tmpl_args->expr->loc, NULL,
			"Too many arguments for template instantiation");
		return NULL;
	}

	// TODO: also take type hint into account maybe
	if (fn_params != NULL && obj->template->decl_type == ADECL_FUNC) {
		deduce_tmpl_args_from_func_params(ctx, scope,
			obj->template->params, obj->template->type->func.params,
			fn_params);
	}

	while (p != NULL) {
		bool success = check_deduced_tmpl_arg(ctx, scope,
			obj->template->imports, p, idx);
		if (!success) {
			return NULL;
		}
		p = p->next;
		idx++;
	}

	// the prototype is used in the mangled name of function templates
	struct type_func prototype;
	if (obj->template->decl_type == ADECL_FUNC) {
		ctx->unit->parent = obj->template->imports;
		ctx->scope = scope;
		push_dummy_pack_scope(&ctx->scope, obj->template->params);
		prototype = get_prototype(ctx, &obj->template->type->func);
	}
	ctx->unit->parent = obj->template->imports;
	ctx->scope = scope;
	resolve_unresolved(ctx);
	ctx->scope = old_scope;
	ctx->unit->parent = old_subunit;

	struct identifier ident = {0};
	ident.name = mangle(&obj->ident, scope, obj->template->params,
		obj->template->decl_type == ADECL_FUNC ? &prototype : NULL);
	struct scope_object *old = scope_lookup(ctx->scope, &ident);
	if (old != NULL) {
		// template was already specialized; return old specialization
		free(ident.name);
		return old;
	}

	const struct template_specialization *best_sp = NULL;
	struct scope *best_sp_scope = NULL;
	int best_n = -1, count = 0;
	for (const struct template_specialization *sp =
			obj->template->specializations; sp; sp = sp->next) {
		struct scope *sp_scope = ctx->defines;
		scope_push(&sp_scope, SCOPE_DEFINES);

		const struct ast_function_parameters *fn_params = NULL;
		if (obj->template->decl_type == ADECL_FUNC) {
			fn_params = obj->template->type->func.params;
		}

		int n = match_specialization(ctx, scope, sp_scope,
			obj->template->imports, sp, obj->template->params,
			fn_params);
		if (n == -1) {
			continue;
		}
		if (n == best_n) {
			count++;
		}
		if (n < best_n || best_n == -1) {
			best_sp = sp;
			best_sp_scope = sp_scope;
			best_n = n;
			count = 1;
		}
	}
	if (count > 1) {
		// XXX: the location for this error is kinda meh i think
		error(ctx, best_sp->loc, NULL,
			"Multiple specializations could be used (ambiguous)");
		free(ident.name);
		return NULL;
	}

	struct scope_object *ret;
	// XXX: this would be a lot nicer if, idk, template_decl used a
	// template_specialization field or something
	if (best_sp != NULL) {
		ret = check_specialization(ctx, best_sp_scope, best_sp->imports,
			obj->template->unit, &ident, obj->template->decl_type,
			best_sp->loc, best_sp->type, best_sp->expr,
			best_sp->threadlocal);
	} else {
		ret = check_specialization(ctx, scope, obj->template->imports,
			obj->template->unit, &ident, obj->template->decl_type,
			obj->template->loc, obj->template->type,
			obj->template->expr, obj->template->threadlocal);
	}
	free(ident.name);
	ctx->decls->decl.exported = obj->template->exported;
	return ret;
}

static struct types *
types_from_call_args(struct context *ctx, const struct call_argument *args)
{
	struct types *ret = NULL, **next = &ret;
	while (args != NULL) {
		struct types *cur = *next = xcalloc(1, sizeof(struct types));
		next = &cur->next;
		cur->type = args->value->result;
		args = args->next;
	}
	return ret;
}

static struct types *
types_from_ast_func_params(struct context *ctx,
	const struct ast_function_parameters *params)
{
	struct types *ret = NULL, **next = &ret;
	while (params != NULL) {
		// this function is called when instantiating full
		// specializations. since there's no template parameters, a
		// variadic parameter must actually be variadism; not a
		// parameter pack
		if (params->type == NULL) {
			assert(params->variadic);
			break;
		}
		struct types *cur = *next = xcalloc(1, sizeof(struct types));
		next = &cur->next;
		cur->type = type_store_lookup_atype(ctx, params->type);
		if (params->variadic) {
			cur->type = type_store_lookup_slice(ctx,
				params->loc, cur->type);
		}
		params = params->next;
	}
	return ret;
}

static void
check_access_identifier(struct context *ctx,
	const struct ast_expression *aexpr,
	struct expression *expr,
	struct call_argument *fn_args)
{
	struct identifier ident = aexpr->access.ident;
	adjust_reserved_ident(&ident);
	struct scope_object *obj = scope_lookup(ctx->scope, &ident);
	if (!obj) {
		char buf[IDENT_BUFSIZ];
		identifier_unparse_static(&aexpr->access.ident, buf);
		error(ctx, aexpr->loc, expr, "Unknown object '%s'", buf);
		return;
	}
	wrap_resolver(ctx, obj, resolve_decl);
	if (obj->otype == O_TEMPLATE) {
		push_error_context(ctx, aexpr->loc);
		struct types *fn_params = types_from_call_args(ctx, fn_args);
		obj = specialize(ctx, obj, aexpr->access.tmpl_args, fn_params);
		pop_error_context(ctx);
		if (!obj) {
			mkerror(aexpr->loc, expr);
			return;
		}
	}

	switch (obj->otype) {
	case O_CONST:
		// Lower flexible types
		*expr = *obj->value;
		flexible_reset_refs(expr->result);
		break;
	case O_BIND:
	case O_DECL:
		expr->result = obj->type;
		expr->access.object = obj;
		break;
	case O_PACK:
		error(ctx, aexpr->loc, expr, "Parameter pack isn't expanded");
		return;
	case O_TYPE:
		if (type_dealias(ctx, obj->type)->storage != STORAGE_VOID) {
			char *typename = gen_typename(obj->type);
			error(ctx, aexpr->loc, expr,
				"Cannot use non-void type %s as literal",
				typename);
			free(typename);
			return;
		}
		expr->type = EXPR_LITERAL;
		expr->result = obj->type;
		break;
	case O_SCAN:
	case O_TEMPLATE:
		assert(0); // handled above
	}
}

static void resolve_decl(struct context *ctx,
	struct incomplete_declaration *idecl);

static void
check_expr_access(struct context *ctx,
	const struct ast_expression *aexpr,
@@ -176,45 +1446,9 @@ check_expr_access(struct context *ctx,
	expr->type = EXPR_ACCESS;
	expr->access.type = aexpr->access.type;

	struct scope_object *obj = NULL;
	switch (expr->access.type) {
	case ACCESS_IDENTIFIER:
		obj = scope_lookup(ctx->scope, &aexpr->access.ident);
		if (!obj) {
			char buf[IDENT_BUFSIZ];
			identifier_unparse_static(&aexpr->access.ident, buf);
			error(ctx, aexpr->loc, expr,
				"Unknown object '%s'", buf);
			return;
		}
		wrap_resolver(ctx, obj, resolve_decl);

		switch (obj->otype) {
		case O_CONST:
			// Lower flexible types
			*expr = *obj->value;
			flexible_reset_refs(expr->result);
			break;
		case O_BIND:
		case O_DECL:
			expr->result = obj->type;
			expr->access.object = obj;
			break;
		case O_TYPE:
			if (type_dealias(ctx, obj->type)->storage != STORAGE_VOID) {
				char *ident = identifier_unparse(&obj->type->alias.ident);
				error(ctx, aexpr->loc, expr,
					"Cannot use non-void type alias '%s' as literal",
					ident);
				free(ident);
				return;
			}
			expr->type = EXPR_LITERAL;
			expr->result = obj->type;
			break;
		case O_SCAN:
			assert(0); // handled above
		}
		check_access_identifier(ctx, aexpr, expr, NULL);
		break;
	case ACCESS_INDEX:
		expr->access.array = xcalloc(1, sizeof(struct expression));
@@ -896,7 +2130,7 @@ type_promote(struct context *ctx, const struct type *a, const struct type *b)
	const struct type *da = type_store_lookup_with_flags(ctx, a, 0);
	const struct type *db = type_store_lookup_with_flags(ctx, b, 0);

	if (da == db) {
	if (da->id == db->id) {
		const struct type *base = type_store_lookup_with_flags(ctx, a,
			a->flags | b->flags);
		assert(base == a || base == b);
@@ -910,7 +2144,7 @@ type_promote(struct context *ctx, const struct type *a, const struct type *b)
	da = type_dealias(ctx, da);
	db = type_dealias(ctx, db);

	if (da == db) {
	if (da->id == db->id) {
		return a->storage == STORAGE_ALIAS ? a : b;
	}

@@ -1082,40 +2316,600 @@ type_has_default(struct context *ctx, const struct type *type)
			if (obj->otype == O_SCAN) {
				wrap_resolver(ctx, obj, resolve_enum_field);
			}
			assert(obj->otype == O_CONST);
			if (obj->value->literal.uval == 0) {
				return true;
			assert(obj->otype == O_CONST);
			if (obj->value->literal.uval == 0) {
				return true;
			}
		}
		return false;
	case STORAGE_POINTER:
		return type->pointer.flags & PTR_NULLABLE;
	case STORAGE_STRUCT:
	case STORAGE_UNION:
		for (struct struct_field *sf = type->struct_union.fields;
				sf != NULL; sf = sf->next) {
			if (!type_has_default(ctx, sf->type)) {
				return false;
			}
		}
		return true;
	case STORAGE_TUPLE:
		for (const struct type_tuple *t = &type->tuple;
				t != NULL; t = t->next) {
			if (!type_has_default(ctx, t->type)) {
				return false;
			}
		}
		return true;
	case STORAGE_ALIAS:
		return type_has_default(ctx, type_dealias(ctx, type));
	case STORAGE_FCONST:
	case STORAGE_ICONST:
	case STORAGE_NULL:
	case STORAGE_RCONST:
		abort(); // unreachable
	}
	abort(); // Unreachable
}

// returns false if the lengths are incompatible, true on success
static bool
pack_expand_update_len(size_t *len, size_t tmplen)
{
	if (*len == (size_t)-1) {
		*len = tmplen;
	}
	return *len == tmplen || tmplen == (size_t)-1;
}

static size_t expr_pack_expand(struct context *ctx, struct scope *scope,
	const struct ast_expression *aexpr, size_t n);

static size_t
ident_pack_expand(struct context *ctx,
	struct scope *scope,
	const struct identifier *ident,
	const struct ast_template_argument *tmpl_args,
	size_t n)
{
	const struct scope_object *obj = scope_lookup(ctx->scope, ident);
	if (obj == NULL) {
		return -1;
	}
	size_t len = -1, tmplen;

	switch (obj->otype) {
	case O_PACK:
		break;
	case O_TEMPLATE:;
		const struct ast_template_parameters *param =
			obj->template->params;
		if (param == NULL) {
			return -1;
		}
		for (const struct ast_template_argument *arg = tmpl_args;
				arg; arg = arg->next) {
			if (param->type == NULL) {
				tmplen = type_pack_expand(ctx, scope, arg->type, n);
			} else {
				tmplen = expr_pack_expand(ctx, scope, arg->expr, n);
			}
			if (!pack_expand_update_len(&len, tmplen)) {
				return -1;
			}
			if (param->next != NULL) {
				param = param->next;
			}
		}
		return len;
	case O_TYPE:
		if (ctx->scope->class == SCOPE_DEFINES) {
			// dummy scope; pack may be shadowed
			obj = scope_lookup(ctx->scope->parent, ident);
			if (obj != NULL && obj->otype == O_PACK) {
				break;
			}
		}
		return -1;
	default:
		return -1;
	}

	len = 0;
	// `struct types` and `struct expressions` have the same representation,
	// so this works for both expression packs and type packs
	for (const struct expressions *expr = obj->pack_values;
			expr; expr = expr->next) {
		if (len++ != n) {
			continue;
		}
		// put item in scope, overwriting any previous item if one
		// exists (from a previous call to pack_expand)
		struct scope_object *new = scope_lookup(scope, &obj->ident);
		if (new == obj) {
			new = xcalloc(1, sizeof(struct scope_object));
			// dummy values, will be overwritten below
			scope_object_init(new, O_PACK, &obj->ident,
				&obj->name, NULL, NULL);
			scope_insert_from_object(scope, new);
		}
		new->otype = obj->type != NULL ? O_CONST : O_TYPE;
		// new->type and new->value are a union
		new->value = expr->expr;
	}

	if (len <= n && n != (size_t)-1) {
		return -1;
	}
	return len;
}

// returns -1 if there's no packs to expand or if an error occurred (e.g.
// incompatible pack lengths), otherwise returns the length of the packs. on
// success, the identifiers referring to the packs are inserted into 'scope' but
// modified to instead refer to the item at index 'n', so the type can be
// checked for each pack item. if n is -1, the length is returned and the scope
// isn't modified (so it may be NULL in this case)
size_t
type_pack_expand(struct context *ctx,
	struct scope *scope,
	const struct ast_type *atype,
	size_t n)
{
	size_t len, tmplen;
	switch (atype->storage) {
	case STORAGE_ALIAS:
		len = ident_pack_expand(ctx, scope, &atype->alias,
			atype->tmpl_args, n);
		break;
	case STORAGE_ARRAY:
	case STORAGE_SLICE:
		len = type_pack_expand(ctx, scope, atype->array.members, n);
		if (atype->array.length != NULL) {
			tmplen = expr_pack_expand(ctx, scope, atype->array.length, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case STORAGE_FUNCTION:
		len = type_pack_expand(ctx, scope, atype->func.result, n);
		for (const struct ast_function_parameters *p = atype->func.params;
				p; p = p->next) {
			if (!p->variadic) {
				tmplen = type_pack_expand(ctx, scope, p->type, n);
				if (!pack_expand_update_len(&len, tmplen)) return -1;
			}
		}
		break;
	case STORAGE_POINTER:
		len = type_pack_expand(ctx, scope, atype->pointer.referent, n);
		break;
	case STORAGE_STRUCT:
	case STORAGE_UNION:
		len = -1;
		for (const struct ast_struct_union_field *f = &atype->struct_union.fields;
				f; f = f->next) {
			if (f->offset != NULL) {
				tmplen = expr_pack_expand(ctx, scope, f->offset, n);
				if (!pack_expand_update_len(&len, tmplen)) return -1;
			}
			tmplen = type_pack_expand(ctx, scope, f->type, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case STORAGE_TAGGED:
		len = -1;
		for (const struct ast_tagged_union_type *tu = &atype->tagged;
				tu; tu = tu->next) {
			if (tu->variadic) {
				continue;
			};
			tmplen = type_pack_expand(ctx, scope, tu->type, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case STORAGE_TUPLE:
		len = -1;
		for (const struct ast_tuple_type *t = &atype->tuple;
				t; t = t->next) {
			if (t->variadic) {
				continue;
			};
			tmplen = type_pack_expand(ctx, scope, t->type, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case STORAGE_NULL:
	case STORAGE_ENUM:
	case STORAGE_FCONST:
	case STORAGE_ICONST:
	case STORAGE_RCONST:
	case STORAGE_ERROR:
		assert(0); // unreachable
	default:
		return -1;
	}

	return len;
}

// returns -1 if there's no packs to expand or if an error occurred (e.g.
// incompatible pack lengths), otherwise returns the length of the packs. on
// success, the identifiers referring to the packs are inserted into 'scope' but
// modified to instead refer to the item at index 'n', so the expression can be
// checked for each pack item. if n is -1, the length is returned and the scope
// isn't modified (so it may be NULL in this case)
static size_t
expr_pack_expand(struct context *ctx,
	struct scope *scope,
	const struct ast_expression *aexpr,
	size_t n)
{
	size_t len, tmplen;
	switch (aexpr->type) {
	case EXPR_ACCESS:
		switch (aexpr->access.type) {
		case ACCESS_IDENTIFIER:
			len = ident_pack_expand(ctx, scope, &aexpr->access.ident,
				aexpr->access.tmpl_args, n);
			break;
		case ACCESS_INDEX:
			len = expr_pack_expand(ctx, scope, aexpr->access.array, n);
			tmplen = expr_pack_expand(ctx, scope, aexpr->access.index, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
			break;
		case ACCESS_FIELD:
			len = expr_pack_expand(ctx, scope, aexpr->access._struct, n);
			break;
		case ACCESS_TUPLE:
			len = expr_pack_expand(ctx, scope, aexpr->access.tuple, n);
			break;
		}
		break;
	case EXPR_ALLOC:
		len = expr_pack_expand(ctx, scope, aexpr->alloc.init, n);
		tmplen = expr_pack_expand(ctx, scope, aexpr->alloc.cap, n);
		if (!pack_expand_update_len(&len, tmplen)) return -1;
		break;
	case EXPR_APPEND:
		len = expr_pack_expand(ctx, scope, aexpr->append.object, n);
		tmplen = expr_pack_expand(ctx, scope, aexpr->append.value, n);
		if (!pack_expand_update_len(&len, tmplen)) return -1;
		if (aexpr->append.length != NULL) {
			tmplen = expr_pack_expand(ctx, scope, aexpr->append.length, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_ASSERT:
	case EXPR_INSERT:
		if (aexpr->assert.cond == NULL) return -1;
		len = expr_pack_expand(ctx, scope, aexpr->assert.cond, n);
		if (aexpr->assert.message != NULL) {
			tmplen = expr_pack_expand(ctx, scope, aexpr->assert.message, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_ASSIGN:
		len = expr_pack_expand(ctx, scope, aexpr->assign.object, n);
		tmplen = expr_pack_expand(ctx, scope, aexpr->assign.value, n);
		if (!pack_expand_update_len(&len, tmplen)) return -1;
		break;
	case EXPR_BINARITHM:
		if (aexpr->binarithm.fold) return -1;
		len = expr_pack_expand(ctx, scope, aexpr->binarithm.lvalue, n);
		tmplen = expr_pack_expand(ctx, scope, aexpr->binarithm.rvalue, n);
		if (!pack_expand_update_len(&len, tmplen)) return -1;
		break;
	case EXPR_CALL:
		len = expr_pack_expand(ctx, scope, aexpr->call.lvalue, n);
		for (const struct ast_call_argument *arg = aexpr->call.args;
				arg; arg = arg->next) {
			if (arg->variadic) {
				continue;
			}
			tmplen = expr_pack_expand(ctx, scope, arg->value, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_CAST:
		len = expr_pack_expand(ctx, scope, aexpr->cast.value, n);
		tmplen = type_pack_expand(ctx, scope, aexpr->cast.type, n);
		if (!pack_expand_update_len(&len, tmplen)) return -1;
		break;
	case EXPR_COMPOUND:
		len = -1;
		for (const struct ast_expression_list *expr = &aexpr->compound.list;
				expr; expr = expr->next) {
			tmplen = expr_pack_expand(ctx, scope, expr->expr, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_DELETE:
		len = expr_pack_expand(ctx, scope, aexpr->delete.expr, n);
		break;
	case EXPR_FOR:
		len = expr_pack_expand(ctx, scope, aexpr->_for.cond, n);
		if (aexpr->_for.bindings != NULL) {
			tmplen = expr_pack_expand(ctx, scope, aexpr->_for.bindings, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		if (aexpr->_for.afterthought != NULL) {
			tmplen = expr_pack_expand(ctx, scope, aexpr->_for.afterthought, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		tmplen = expr_pack_expand(ctx, scope, aexpr->_for.body, n);
		if (!pack_expand_update_len(&len, tmplen)) return -1;
		break;
	case EXPR_FREE:
		len = expr_pack_expand(ctx, scope, aexpr->free.expr, n);
		break;
	case EXPR_IF:
		len = expr_pack_expand(ctx, scope, aexpr->_if.cond, n);
		tmplen = expr_pack_expand(ctx, scope, aexpr->_if.true_branch, n);
		if (!pack_expand_update_len(&len, tmplen)) return -1;
		if (aexpr->_if.false_branch != NULL) {
			tmplen = expr_pack_expand(ctx, scope, aexpr->_if.false_branch, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_LITERAL:
		if (aexpr->literal.storage != STORAGE_ARRAY) return -1;
		len = -1;
		for (const struct ast_array_literal *item = aexpr->literal.array;
				item; item = item->next) {
			if (item->expand) {
				continue;
			}
			tmplen = expr_pack_expand(ctx, scope, item->value, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_MATCH:
		len = expr_pack_expand(ctx, scope, aexpr->match.value, n);
		for (const struct ast_match_case *c = aexpr->match.cases;
				c; c = c->next) {
			if (c->type != NULL) {
				tmplen = type_pack_expand(ctx, scope, c->type, n);
				if (!pack_expand_update_len(&len, tmplen)) return -1;
			}
			for (const struct ast_expression_list *expr = &c->exprs;
					expr; expr = expr->next) {
				tmplen = expr_pack_expand(ctx, scope, expr->expr, n);
				if (!pack_expand_update_len(&len, tmplen)) return -1;
			}
		}
		break;
	case EXPR_MEASURE:
		switch (aexpr->measure.op) {
		case M_ALIGN:
		case M_SIZE:
			len = type_pack_expand(ctx, scope, aexpr->measure.type, n);
			break;
		case M_LEN:
		case M_OFFSET:
			len = expr_pack_expand(ctx, scope, aexpr->measure.value, n);
			break;
		case M_SIZEPACK:
			return -1;
		}
		break;
	case EXPR_PROPAGATE:
		len = expr_pack_expand(ctx, scope, aexpr->propagate.value, n);
		break;
	case EXPR_RETURN:
		if (aexpr->_return.value == NULL) return -1;
		len = expr_pack_expand(ctx, scope, aexpr->_return.value, n);
		break;
	case EXPR_SLICE:
		len = expr_pack_expand(ctx, scope, aexpr->slice.object, n);
		if (aexpr->slice.start != NULL) {
			tmplen = expr_pack_expand(ctx, scope, aexpr->slice.start, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		if (aexpr->slice.end != NULL) {
			tmplen = expr_pack_expand(ctx, scope, aexpr->slice.end, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_STRUCT:
		len = -1;
		if (aexpr->_struct.type.name != NULL) {
			tmplen = ident_pack_expand(ctx, scope, &aexpr->_struct.type,
				aexpr->_struct.tmpl_args, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		for (const struct ast_field_value *f = aexpr->_struct.fields;
				f; f = f->next) {
			if (f->type != NULL) {
				tmplen = type_pack_expand(ctx, scope, f->type, n);
				if (!pack_expand_update_len(&len, tmplen)) return -1;
			}
			tmplen = expr_pack_expand(ctx, scope, f->initializer, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_SWITCH:
		len = expr_pack_expand(ctx, scope, aexpr->_switch.value, n);
		for (const struct ast_switch_case *c = aexpr->_switch.cases;
				c; c = c->next) {
			for (const struct ast_case_option *opt = c->options;
					opt; opt = opt->next) {
				tmplen = expr_pack_expand(ctx, scope, opt->value, n);
				if (!pack_expand_update_len(&len, tmplen)) return -1;
			}
			for (const struct ast_expression_list *expr = &c->exprs;
					expr; expr = expr->next) {
				tmplen = expr_pack_expand(ctx, scope, expr->expr, n);
				if (!pack_expand_update_len(&len, tmplen)) return -1;
			}
		}
		break;
	case EXPR_TUPLE:
		len = -1;
		for (const struct ast_expression_tuple *item = &aexpr->tuple;
				item; item = item->next) {
			tmplen = expr_pack_expand(ctx, scope, item->expr, n);
			if (!pack_expand_update_len(&len, tmplen)) return -1;
		}
		break;
	case EXPR_UNARITHM:
		len = expr_pack_expand(ctx, scope, aexpr->unarithm.operand, n);
		break;
	case EXPR_VAARG:
	case EXPR_VAEND:
		len = expr_pack_expand(ctx, scope, aexpr->vaarg.ap, n);
		break;
	case EXPR_YIELD:
		if (aexpr->control.value == NULL) return -1;
		len = expr_pack_expand(ctx, scope, aexpr->control.value, n);
		break;
	case EXPR_BREAK:
	case EXPR_CONTINUE:
	case EXPR_VASTART:
		return -1;
	case EXPR_BINDING:
	case EXPR_DEFER:
	case EXPR_DEFINE:
		assert(0); // unreachable: disallowed by syntax
	}

	return len;
}

static void
check_expr_fold(struct context *ctx,
	const struct ast_expression *aexpr,
	struct expression *expr,
	const struct type *hint)
{
	struct ast_expression *pack = NULL;
	enum { UNKNOWN, LEFT, RIGHT } kind = UNKNOWN;
	size_t len = -1;

	if (aexpr->binarithm.lvalue != NULL) {
		size_t n = expr_pack_expand(ctx, NULL,
			aexpr->binarithm.lvalue, -1);
		if (n == (size_t)-1) {
			check_expression(ctx, aexpr->binarithm.lvalue,
				expr, NULL);
			kind = LEFT;
		} else {
			pack = aexpr->binarithm.lvalue;
			kind = RIGHT;
			len = n;
		}
	}
	if (aexpr->binarithm.rvalue != NULL) {
		size_t n = expr_pack_expand(ctx, NULL,
			aexpr->binarithm.rvalue, -1);
		if (n == (size_t)-1) {
			if (len == (size_t)-1) {
				error(ctx, aexpr->loc, expr,
					"Binary fold operand should contain unexpanded parameter packs, but it doesn't");
				return;
			}
			assert(pack != NULL);
			check_expression(ctx, aexpr->binarithm.rvalue,
				expr, NULL);
			assert(kind != LEFT);
			kind = RIGHT;
		} else {
			if (len != (size_t)-1) {
				error(ctx, aexpr->loc, expr,
					"Binary fold operand shouldn't contain any unexpanded parameter packs, but it does");
				return;
			}
			assert(pack == NULL);
			pack = aexpr->binarithm.rvalue;
			assert(kind != RIGHT);
			kind = LEFT;
			len = n;
		}
		return false;
	case STORAGE_POINTER:
		return type->pointer.flags & PTR_NULLABLE;
	case STORAGE_STRUCT:
	case STORAGE_UNION:
		for (struct struct_field *sf = type->struct_union.fields;
				sf != NULL; sf = sf->next) {
			if (!type_has_default(ctx, sf->type)) {
				return false;
	}

	if (pack == NULL) {
		error(ctx, aexpr->loc, expr,
			"Fold expression must operate on unexpanded parameter packs");
		return;
	}
	assert(len != (size_t)-1);
	assert(kind != UNKNOWN);

	struct scope *scope = ctx->scope;
	scope_push(&scope, SCOPE_DEFINES);

	size_t i = 0;
	if (aexpr->binarithm.lvalue == NULL || aexpr->binarithm.rvalue == NULL) {
		if (len == 0) {
			expr->type = EXPR_LITERAL;
			expr->result = &builtin_type_bool;

			switch (aexpr->binarithm.op) {
			case BIN_LAND:
				expr->literal.bval = true;
				break;
			case BIN_LOR:
				expr->literal.bval = false;
				break;
			default:
				error(ctx, aexpr->loc, expr,
					"Folding on zero-length parameter packs is only allowed with && and || operators");
			}
			return;
		}
		return true;
	case STORAGE_TUPLE:
		for (const struct type_tuple *t = &type->tuple;
				t != NULL; t = t->next) {
			if (!type_has_default(ctx, t->type)) {
				return false;
			}

		expr_pack_expand(ctx, scope, pack, kind == LEFT ? 0 : len - 1);
		ctx->scope = scope;
		check_expression(ctx, pack, expr, NULL);
		scope_pop(&ctx->scope);
		i = 1;
	}

	for (; i < len; i++) {
		size_t n = kind == LEFT ? i : len - i - 1;
		expr_pack_expand(ctx, scope, pack, n);

		struct expression *operand = xcalloc(1,
			sizeof(struct expression));
		ctx->scope = scope;
		check_expression(ctx, pack, operand, NULL);
		scope_pop(&ctx->scope);

		if (operand->result->storage == STORAGE_ERROR) {
			mkerror(aexpr->loc, expr);
			return;
		}

		struct expression *old = xcalloc(1, sizeof(struct expression));
		*old = *expr;
		struct expression *lvalue = kind == LEFT ? old : operand;
		struct expression *rvalue = kind == LEFT ? operand : old;

		expr->loc = aexpr->loc;
		expr->type = EXPR_BINARITHM;
		expr->binarithm.op = aexpr->binarithm.op;

		expr->result = type_promote(ctx, lvalue->result, rvalue->result);
		if (expr->result == NULL) {
			char *ltypename = gen_typename(lvalue->result);
			char *rtypename = gen_typename(rvalue->result);
			error(ctx, aexpr->loc, expr,
				"Cannot promote lvalue %s and rvalue %s",
				ltypename, rtypename);
			free(ltypename);
			free(rtypename);
			return;
		}

		expr->binarithm.lvalue = lower_implicit_cast(ctx,
			expr->result, lvalue);
		expr->binarithm.rvalue = lower_implicit_cast(ctx,
			expr->result, rvalue);

		check_binarithm_op(ctx, expr, expr->binarithm.op);
		if (expr->result->storage == STORAGE_ERROR) {
			return;
		}
		return true;
	case STORAGE_ALIAS:
		return type_has_default(ctx, type_dealias(ctx, type));
	case STORAGE_FCONST:
	case STORAGE_ICONST:
	case STORAGE_NULL:
	case STORAGE_RCONST:
		abort(); // unreachable
	}
	abort(); // Unreachable
}

static void
@@ -1124,6 +2918,11 @@ check_expr_binarithm(struct context *ctx,
	struct expression *expr,
	const struct type *hint)
{
	if (aexpr->binarithm.fold) {
		check_expr_fold(ctx, aexpr, expr, hint);
		return;
	}

	expr->type = EXPR_BINARITHM;
	expr->binarithm.op = aexpr->binarithm.op;

@@ -1217,6 +3016,7 @@ check_binding_unpack(struct context *ctx,
			struct identifier ident = {
				.name = cur->name,
			};
			adjust_reserved_ident(&ident);

			if (abinding->is_static) {
				struct identifier gen = {0};
@@ -1331,6 +3131,7 @@ check_expr_binding(struct context *ctx,
				type = &builtin_type_error;
			}
			binding->initializer = value;
			adjust_reserved_ident(&ident);
			binding->object = scope_insert(ctx->scope,
				O_CONST, &ident, &ident, NULL, value);
			goto done;
@@ -1346,6 +3147,7 @@ check_expr_binding(struct context *ctx,
			binding->object = scope_insert(ctx->scope,
				O_DECL, &gen, &ident, type, NULL);
		} else {
			adjust_reserved_ident(&ident);
			binding->object = scope_insert(ctx->scope,
				O_BIND, &ident, &ident, type, NULL);
		}
@@ -1444,32 +3246,45 @@ check_expr_call(struct context *ctx,
{
	expr->type = EXPR_CALL;

	struct scope_object *obj = NULL;
	if (aexpr->call.lvalue->type == EXPR_ACCESS
			&& aexpr->call.lvalue->access.type == ACCESS_IDENTIFIER) {
		struct identifier ident = aexpr->call.lvalue->access.ident;
		adjust_reserved_ident(&ident);
		obj = scope_lookup(ctx->scope, &ident);
		wrap_resolver(ctx, obj, resolve_decl);
	}

	struct expression *lvalue = xcalloc(1, sizeof(struct expression));
	check_expression(ctx, aexpr->call.lvalue, lvalue, NULL);
	expr->call.lvalue = lvalue;
	const struct type *fntype = NULL;
	if (!obj || obj->otype != O_TEMPLATE) {
		check_expression(ctx, aexpr->call.lvalue, lvalue, NULL);

	const struct type *fntype = type_dereference(ctx, lvalue->result);
	if (!fntype) {
		error(ctx, aexpr->loc, expr,
			"Cannot dereference nullable pointer type for function call");
		return;
	}
	fntype = type_dealias(ctx, fntype);
	if (fntype->storage == STORAGE_ERROR) {
		mkerror(aexpr->loc, expr);
		return;
	};
	if (fntype->storage != STORAGE_FUNCTION) {
		error(ctx, aexpr->loc, expr,
			"Cannot call non-function type");
		return;
		fntype = type_dereference(ctx, lvalue->result);
		if (!fntype) {
			error(ctx, aexpr->loc, expr,
				"Cannot dereference nullable pointer type for function call");
			return;
		}
		fntype = type_dealias(ctx, fntype);
		if (fntype->storage == STORAGE_ERROR) {
			mkerror(aexpr->loc, expr);
			return;
		};
		if (fntype->storage != STORAGE_FUNCTION) {
			error(ctx, aexpr->loc, expr,
				"Cannot call non-function type");
			return;
		}
	}
	expr->result = fntype->func.result;

	struct call_argument *arg, **next = &expr->call.args;
	struct ast_call_argument *aarg = aexpr->call.args;
	struct type_func_param *param = fntype->func.params;
	while ((param || fntype->func.variadism == VARIADISM_C) && aarg) {
	struct type_func_param *param = fntype ? fntype->func.params : NULL;
	size_t exp_idx = -1, exp_len = -1;
	struct scope *exp_scope = ctx->scope;
	scope_push(&exp_scope, SCOPE_DEFINES);
	while ((param || !fntype || fntype->func.variadism == VARIADISM_C) && aarg) {
		arg = *next = xcalloc(1, sizeof(struct call_argument));
		arg->value = xcalloc(1, sizeof(struct expression));

@@ -1477,6 +3292,7 @@ check_expr_call(struct context *ctx,
				&& fntype->func.variadism == VARIADISM_HARE
				&& !aarg->variadic) {
			if (param->type->storage == STORAGE_ERROR) {
				mkerror(aexpr->loc, expr);
				return;
			};
			lower_vaargs(ctx, aarg, arg->value,
@@ -1487,11 +3303,33 @@ check_expr_call(struct context *ctx,
			break;
		}

		// TODO: type hints aren't used at all for function templates,
		// even when they could (and should) be
		const struct type *ptype = NULL;
		if (param) {
			ptype = param->type;
		}
		if (exp_idx == (size_t)-1 && aarg->variadic && (aarg->next || !fntype
				|| fntype->func.variadism != VARIADISM_HARE)) {
			exp_len = expr_pack_expand(ctx, NULL, aarg->value, -1);
			if (exp_len == (size_t)-1) {
				error(ctx, aarg->value->loc, expr,
					"No parameter packs to expand");
				return;
			}
			exp_idx = 0;
		}
		if (exp_idx != (size_t)-1) {
			expr_pack_expand(ctx, exp_scope, aarg->value, exp_idx);
			exp_idx++;
			if (exp_idx == exp_len) {
				exp_idx = -1;
				exp_len = -1;
			}
		}
		ctx->scope = exp_scope;
		check_expression(ctx, aarg->value, arg->value, ptype);
		ctx->scope = ctx->scope->parent;

		if (param) {
			if (!type_is_assignable(ctx, ptype, arg->value->result)) {
@@ -1514,11 +3352,67 @@ check_expr_call(struct context *ctx,
		}
	}

	if (!fntype) {
		check_access_identifier(ctx, aexpr->call.lvalue,
			lvalue, expr->call.args);
		if (lvalue->result->storage == STORAGE_ERROR) {
			mkerror(aexpr->loc, expr);
			return;
		}
		fntype = lvalue->result;
		if (fntype->storage != STORAGE_FUNCTION) {
			error(ctx, aexpr->loc, expr,
				"Cannot call non-function type");
			return;
		}

		arg = expr->call.args;
		aarg = aexpr->call.args;
		param = fntype->func.params;
		while (param && aarg) {
			assert(arg);

			if (!param->next && fntype->func.variadism == VARIADISM_HARE
					&& !aarg->variadic) {
				if (param->type->storage == STORAGE_ERROR) {
					mkerror(aexpr->loc, expr);
					return;
				}
				lower_vaargs(ctx, aarg, arg->value,
					param->type->array.members);
				arg->value = lower_implicit_cast(ctx,
					param->type, arg->value);
				param = NULL;
				aarg = NULL;
				break;
			}

			if (!type_is_assignable(ctx, param->type,
					arg->value->result)) {
				char *argtypename = gen_typename(arg->value->result);
				char *paramtypename = gen_typename(param->type);
				error(ctx, aarg->value->loc, expr,
					"Argument type %s is not assignable to parameter type %s",
					argtypename, paramtypename);
				free(argtypename);
				free(paramtypename);
				return;
			}
			arg->value = lower_implicit_cast(ctx,
				param->type, arg->value);

			aarg = aarg->next;
			arg = arg->next;
			param = param->next;
		}
	}

	if (param && !param->next && fntype->func.variadism == VARIADISM_HARE) {
		// No variadic arguments, lower to empty slice
		arg = *next = xcalloc(1, sizeof(struct call_argument));
		arg->value = xcalloc(1, sizeof(struct expression));
		if (param->type->storage == STORAGE_ERROR) {
			mkerror(aexpr->loc, expr);
			return;
		};
		lower_vaargs(ctx, NULL, arg->value,
@@ -1538,6 +3432,8 @@ check_expr_call(struct context *ctx,
		return;
	}

	expr->call.lvalue = lvalue;
	expr->result = fntype->func.result;
}

static void
@@ -1625,16 +3521,59 @@ check_expr_cast(struct context *ctx,
	expr->result = aexpr->cast.kind == C_TEST? &builtin_type_bool : secondary;
}

// XXX: this function could probably maybe be simplified; it's kinda funny rn
static void
check_array_member(struct context *ctx,
	const struct ast_expression *aexpr,
	struct array_literal ***next,
	const struct type **type,
	struct array_literal *const *array,
	const struct type *hint,
	size_t *len)
{
	struct expression *value = xcalloc(1, sizeof(struct expression));
	check_expression(ctx, aexpr, value, *type);
	struct array_literal *cur = **next =
		xcalloc(1, sizeof(struct array_literal));
	cur->value = value;

	if (!*type) {
		*type = value->result;
	} else {
		if (!hint) {
			// The promote_flexible in check_expression_literal
			// might've caused the type to change out from under our
			// feet
			*type = (*array)->value->result;
		}
		if (!type_is_assignable(ctx, *type, value->result)) {
			char *typename1 = gen_typename(*type);
			char *typename2 = gen_typename(value->result);
			error(ctx, aexpr->loc, NULL,
				"Array members must be of a uniform type, previously seen %s, but now see %s",
				typename1, typename2);
			free(typename1);
			free(typename2);
			*len = -1;
			return;
		}
		if (!hint) {
			// Ditto
			*type = (*array)->value->result;
		}
		cur->value = lower_implicit_cast(ctx, *type, cur->value);
	}

	*next = &cur->next;
	++*len;
}

static void
check_expr_array(struct context *ctx,
	const struct ast_expression *aexpr,
	struct expression *expr,
	const struct type *hint)
{
	size_t len = 0;
	bool expand = false;
	struct ast_array_literal *item = aexpr->literal.array;
	struct array_literal *cur, **next = &expr->literal.array;
	const struct type *type = NULL;
	if (hint) {
		hint = type_dealias(ctx, hint);
@@ -1666,46 +3605,49 @@ check_expr_array(struct context *ctx,
		}
	}

	while (item) {
		struct expression *value = xcalloc(1, sizeof(struct expression));
		check_expression(ctx, item->value, value, type);
		cur = *next = xcalloc(1, sizeof(struct array_literal));
		cur->value = value;
	size_t len = 0;
	bool expand = false;
	struct array_literal **next = &expr->literal.array;
	for (const struct ast_array_literal *item = aexpr->literal.array;
			item; item = item->next) {
		size_t n = -1;
		if (item->expand) {
			n = expr_pack_expand(ctx, NULL, item->value, -1);
		}
		if (item->expand && n != (size_t)-1) {
			struct scope *scope = ctx->scope;
			scope_push(&scope, SCOPE_DEFINES);

			for (size_t i = 0; i < n; i++) {
				expr_pack_expand(ctx, scope, item->value, i);
				ctx->scope = scope;
				check_array_member(ctx, item->value, &next, &type,
					&expr->literal.array, hint, &len);
				if (len == (size_t)-1) {
					mkerror(item->value->loc, expr);
					return;
				}
				scope_pop(&ctx->scope);
			}

		if (!type) {
			type = value->result;
			continue;
		} else {
			if (!hint) {
				// The promote_flexible in
				// check_expression_literal might've caused the
				// type to change out from under our feet
				type = expr->literal.array->value->result;
			if (item->expand && n == (size_t)-1) {
				if (item->next) {
					error(ctx, item->value->loc, expr,
						"No parameter packs to expand");
					return;
				}
				expand = true;
			}
			if (!type_is_assignable(ctx, type, value->result)) {
				char *typename1 = gen_typename(type);
				char *typename2 = gen_typename(value->result);
				error(ctx, item->value->loc, expr,
					"Array members must be of a uniform type, previously seen %s, but now see %s",
					typename1, typename2);
				free(typename1);
				free(typename2);

			check_array_member(ctx, item->value, &next, &type,
				&expr->literal.array, hint, &len);
			if (len == (size_t)-1) {
				mkerror(item->value->loc, expr);
				return;
			}
			if (!hint) {
				// Ditto
				type = expr->literal.array->value->result;
			}
			cur->value = lower_implicit_cast(ctx, type, cur->value);
		}

		if (item->expand) {
			expand = true;
			assert(!item->next);
		}

		item = item->next;
		next = &cur->next;
		++len;
	}

	if (type == NULL) {
@@ -2261,6 +4203,7 @@ check_expr_match(struct context *ctx,
			struct identifier ident = {
				.name = acase->name,
			};
			adjust_reserved_ident(&ident);
			struct scope *scope = scope_push(
				&ctx->scope, SCOPE_MATCH);
			_case->object = scope_insert(scope, O_BIND,
@@ -2343,6 +4286,16 @@ check_expr_measure(struct context *ctx,
	case M_ALIGN:
	case M_SIZE:
		break;
	case M_SIZEPACK:
		expr->type = EXPR_LITERAL;
		expr->literal.uval = ident_pack_expand(ctx, NULL,
			&aexpr->measure.ident, NULL, -1);
		if (expr->literal.uval == (uint64_t)-1) {
			error(ctx, aexpr->loc, expr,
				"size... argument must be a parameter pack");
			return;
		}
		return;
	case M_LEN:
		expr->type = EXPR_LEN;
		expr->len.value = xcalloc(1, sizeof(struct expression));
@@ -2808,15 +4761,25 @@ check_expr_struct(struct context *ctx,

	const struct type *stype = NULL;
	if (aexpr->_struct.type.name) {
		struct scope_object *obj = scope_lookup(ctx->scope,
				&aexpr->_struct.type);
		struct identifier ident = aexpr->_struct.type;
		adjust_reserved_ident(&ident);
		struct scope_object *obj = scope_lookup(ctx->scope, &ident);
		// resolve the unknown type
		wrap_resolver(ctx, obj, resolve_type);
		wrap_resolver(ctx, obj, resolve_decl);
		if (!obj) {
			error(ctx, aexpr->loc, expr,
				"Unknown type alias");
			return;
		}
		if (obj->otype == O_TEMPLATE) {
			push_error_context(ctx, aexpr->loc);
			obj = specialize(ctx, obj, aexpr->_struct.tmpl_args, NULL);
			pop_error_context(ctx);
			if (!obj) {
				mkerror(aexpr->loc, expr);
				return;
			}
		}

		if (obj->otype != O_TYPE) {
			error(ctx, aexpr->loc, expr,
@@ -3046,6 +5009,35 @@ num_cases(struct context *ctx, const struct type *type)
	}
}

static void
check_case_option(struct context *ctx,
	const struct ast_expression *aexpr,
	struct expression *expr,
	struct case_option ***next,
	const struct type *hint,
	size_t *nmemb)
{
	struct case_option *opt = **next =
		xcalloc(1, sizeof(struct case_option));
	opt->value = xcalloc(1, sizeof(struct expression));
	*next = &opt->next;
	++*nmemb;

	struct expression *value = xcalloc(1, sizeof(struct expression));
	check_expression(ctx, aexpr, value, hint);
	if (!type_is_assignable(ctx, hint, value->result)) {
		error(ctx, aexpr->loc, expr, "Invalid type for switch case");
		return;
	}
	value = lower_implicit_cast(ctx, hint, value);

	if (!eval_expr(ctx, value, opt->value)) {
		error(ctx, aexpr->loc, expr,
			"Unable to evaluate case at compile time");
		return;
	}
}

static void
check_expr_switch(struct context *ctx,
	const struct ast_expression *aexpr,
@@ -3091,32 +5083,44 @@ check_expr_switch(struct context *ctx,
			has_default_case = true;
		}

		struct case_option *opt, **next_opt = &_case->options;
		struct case_option **next_opt = &_case->options;
		for (const struct ast_case_option *aopt = acase->options;
				aopt; aopt = aopt->next) {
			opt = *next_opt = xcalloc(1, sizeof(struct case_option));
			struct expression *value =
				xcalloc(1, sizeof(struct expression));
			struct expression *evaled =
				xcalloc(1, sizeof(struct expression));
			if (!aopt->variadic) {
				check_case_option(ctx, aopt->value,
					expr, &next_opt, type, &n);
				continue;
			}

			check_expression(ctx, aopt->value, value, type);
			if (!type_is_assignable(ctx, type, value->result)) {
			size_t len = expr_pack_expand(ctx,
				NULL, aopt->value, -1);
			if (len == (size_t)-1) {
				error(ctx, aopt->value->loc, expr,
					"Invalid type for switch case");
				return;
					"No parameter packs to expand");
				continue;
			}
			value = lower_implicit_cast(ctx, type, value);

			if (!eval_expr(ctx, value, evaled)) {
				error(ctx, aopt->value->loc, expr,
					"Unable to evaluate case at compile time");
				return;
			struct scope *scope = ctx->scope;
			scope_push(&scope, SCOPE_DEFINES);

			for (size_t i = 0; i < len; i++) {
				expr_pack_expand(ctx, scope, aopt->value, i);
				ctx->scope = scope;
				check_case_option(ctx, aopt->value,
					expr, &next_opt, type, &n);
				scope_pop(&ctx->scope);
			}
		}

			opt->value = evaled;
			next_opt = &opt->next;
			n++;
		// check if error occurred in check_switch_case_options
		if (expr->type == EXPR_LITERAL) {
			assert(expr->result->storage == STORAGE_ERROR);
			return;
		}

		if (acase->options != NULL && _case->options == NULL) {
			error(ctx, acase->exprs.expr->loc, _case->value,
				"Non-default case has no options");
		}

		// Lower to compound
@@ -3145,33 +5149,25 @@ check_expr_switch(struct context *ctx,
		struct expression *_case;
		struct location loc;
	};
	struct located_case *cases_array = xcalloc(n, sizeof(struct located_case));
	struct expression **cases_array = xcalloc(n, sizeof(struct expression *));
	size_t i = 0;
	for (acase = aexpr->_switch.cases, _case = expr->_switch.cases;
			_case; acase = acase->next, _case = _case->next) {
		assert(acase);
		const struct ast_case_option *aopt;
		const struct case_option *opt;
		for (aopt = acase->options, opt = _case->options;
				opt; aopt = aopt->next, opt = opt->next) {
			assert(aopt);
	for (_case = expr->_switch.cases; _case; _case = _case->next) {
		for (const struct case_option *opt = _case->options;
				opt; opt = opt->next) {
			assert(i < n);
			cases_array[i]._case = opt->value;
			cases_array[i].loc = aopt->value->loc;
			cases_array[i] = opt->value;
			i++;
		}
		assert(!aopt);
	}
	assert(!acase);
	assert(i == n);
	qsort(cases_array, n, sizeof(struct located_case), &casecmp);
	qsort(cases_array, n, sizeof(struct expression *), &casecmp);
	bool has_duplicate = false;
	for (size_t i = 1; i < n; i++) {
		if (cases_array[i]._case->result->storage == STORAGE_ERROR) {
		if (cases_array[i]->result->storage == STORAGE_ERROR) {
			break;
		}
		const struct expression_literal *a = &cases_array[i - 1]._case->literal;
		const struct expression_literal *b = &cases_array[i]._case->literal;
		const struct expression_literal *a = &cases_array[i - 1]->literal;
		const struct expression_literal *b = &cases_array[i]->literal;
		bool equal;
		if (type_is_integer(ctx, value->result)) {
			equal = a->uval == b->uval;
@@ -3188,7 +5184,7 @@ check_expr_switch(struct context *ctx,
			equal = a->rune == b->rune;
		}
		if (equal) {
			error(ctx, cases_array[i].loc, cases_array[i]._case,
			error(ctx, cases_array[i]->loc, cases_array[i],
				"Duplicate switch case");
			has_duplicate = true;
		}
@@ -3237,6 +5233,32 @@ check_expr_switch(struct context *ctx,
	}
}

static void
check_tuple_member(struct context *ctx,
	const struct ast_expression *aexpr,
	struct expression_tuple **tuple,
	const struct type_tuple **hint,
	struct type_tuple **result,
	size_t *nmemb)
{
	if (*nmemb > 0) {
		(*result)->next = xcalloc(1, sizeof(struct type_tuple));
		*result = (*result)->next;
		(*tuple)->next = xcalloc(1, sizeof(struct expression_tuple));
		*tuple = (*tuple)->next;
	}
	++*nmemb;

	(*tuple)->value = xcalloc(1, sizeof(struct expression));
	check_expression(ctx, aexpr, (*tuple)->value,
		*hint ? (*hint)->type : NULL);
	(*result)->type = (*tuple)->value->result;

	if (*hint) {
		*hint = (*hint)->next;
	}
}

static void
check_expr_tuple(struct context *ctx,
	const struct ast_expression *aexpr,
@@ -3253,25 +5275,41 @@ check_expr_tuple(struct context *ctx,
	struct type_tuple result = {0};
	struct type_tuple *rtype = &result;

	size_t nmemb = 0;
	struct expression_tuple *tuple = &expr->tuple;
	for (const struct ast_expression_tuple *atuple = &aexpr->tuple;
			atuple; atuple = atuple->next) {
		tuple->value = xcalloc(1, sizeof(struct expression));
		check_expression(ctx, atuple->expr, tuple->value, ttuple ? ttuple->type : NULL);
		rtype->type = tuple->value->result;
		if (atuple->variadic) {
			size_t n = expr_pack_expand(ctx,
				NULL, atuple->expr, -1);
			if (n == (size_t)-1) {
				error(ctx, atuple->expr->loc, expr,
					"No parameter packs to expand");
				return;
			}

		if (atuple->next) {
			rtype->next = xcalloc(1, sizeof(struct type_tuple));
			rtype = rtype->next;
			tuple->next = xcalloc(1, sizeof(struct expression_tuple));
			tuple = tuple->next;
		}
			struct scope *scope = ctx->scope;
			scope_push(&scope, SCOPE_DEFINES);

		if (ttuple) {
			ttuple = ttuple->next;
			for (size_t i = 0; i < n; i++) {
				expr_pack_expand(ctx, scope, atuple->expr, i);
				ctx->scope = scope;
				check_tuple_member(ctx, atuple->expr, &tuple,
					&ttuple, &rtype, &nmemb);
				scope_pop(&ctx->scope);
			}
		} else {
			check_tuple_member(ctx, atuple->expr, &tuple,
				&ttuple, &rtype, &nmemb);
		}
	}

	if (nmemb < 2) {
		error(ctx, aexpr->loc, expr,
			"Tuple must contain at least two members");
		return;
	}

	if (hint && type_dealias(ctx, hint)->storage == STORAGE_TUPLE) {
		expr->result = hint;
	} else if (hint && type_dealias(ctx, hint)->storage == STORAGE_TAGGED) {
@@ -3314,21 +5352,19 @@ check_expr_tuple(struct context *ctx,

	ttuple = &type_dealias(ctx, expr->result)->tuple;
	struct expression_tuple *etuple = &expr->tuple;
	const struct ast_expression_tuple *atuple = &aexpr->tuple;
	while (etuple) {
		if (!ttuple) {
			error(ctx, atuple->expr->loc, expr,
			error(ctx, etuple->value->loc, expr,
				"Too many values for tuple type");
			return;
		}
		if (!type_is_assignable(ctx, ttuple->type, etuple->value->result)) {
			error(ctx, atuple->expr->loc, expr,
			error(ctx, etuple->value->loc, expr,
				"Value is not assignable to tuple value type");
			return;
		}
		etuple->value = lower_implicit_cast(ctx, ttuple->type, etuple->value);
		etuple = etuple->next;
		atuple = atuple->next;
		ttuple = ttuple->next;
	}
	if (ttuple) {
@@ -3615,17 +5651,6 @@ append_decl(struct context *ctx, struct declaration *decl)
	ctx->decls = decls;
}

static void
resolve_unresolved(struct context *ctx)
{
	while (ctx->unresolved) {
		struct ast_types *unresolved = ctx->unresolved;
		ctx->unresolved = unresolved->next;
		type_store_lookup_atype(ctx, unresolved->type);
		free(unresolved);
	}
}

void
check_function(struct context *ctx,
	const struct scope_object *obj,
@@ -3662,24 +5687,55 @@ check_function(struct context *ctx,
	}

	decl->func.scope = scope_push(&ctx->scope, SCOPE_FUNC);
	struct ast_function_parameters *params = afndecl->prototype.params;
	while (params) {
		if (!params->name) {
			error(ctx, params->loc, NULL,
	const struct ast_function_parameters *aparams = afndecl->prototype.params;
	const struct type_func_param *params = ctx->fntype->func.params;
	while (aparams) {
		if (!aparams->type) {
			assert(aparams->variadic);
			assert(!params);
			assert(ctx->fntype->func.variadism == VARIADISM_C);
			break;
		}
		if (!aparams->name) {
			error(ctx, aparams->loc, NULL,
				"Function parameters must be named");
			return;
		}
		struct identifier ident = {
			.name = params->name,
			.name = aparams->name,
		};

		if (aparams->variadic && (aparams->next
				|| ctx->fntype->func.variadism != VARIADISM_HARE)) {
			adjust_reserved_ident(&ident);
			struct scope_object *obj = scope_insert(decl->func.scope,
				O_PACK, &ident, &ident, NULL, NULL);
			struct types **next = &obj->pack_types;
			while (params) {
				*next = xcalloc(1, sizeof(struct types));
				(*next)->type = params->type;
				next = &(*next)->next;
				params = params->next;
			}
			break;
		}

		assert(params);
		// i'm pretty sure the lookup here is still necessary even
		// though we already have the type, because the scoping rules
		// are different for function implementation declarations than
		// for function prototype declarations or function types
		const struct type *type = type_store_lookup_atype(
				ctx, params->type);
		if (obj->type->func.variadism == VARIADISM_HARE
				&& !params->next) {
			type = type_store_lookup_slice(ctx, params->loc, type);
				ctx, aparams->type);
		if (aparams->variadic) {
			assert(!aparams->next);
			assert(ctx->fntype->func.variadism == VARIADISM_HARE);
			type = type_store_lookup_slice(ctx, aparams->loc, type);
		}
		adjust_reserved_ident(&ident);
		scope_insert(decl->func.scope, O_BIND,
			&ident, &ident, type, NULL);
		aparams = aparams->next;
		params = params->next;
	}

@@ -3737,29 +5793,74 @@ end:
	if ((adecl->function.flags & FN_TEST) && !ctx->is_test) {
		return;
	}
	append_decl(ctx, decl);
	append_decl(ctx, decl);
}

static struct incomplete_declaration *
incomplete_declaration_create(struct context *ctx, struct location loc,
		struct scope *scope, const struct identifier *ident,
		const struct identifier *name, bool no_adjust_symbol)
{
	struct scope *subunit = ctx->unit->parent;
	ctx->unit->parent = NULL;
	struct identifier adj_name = *name;
	struct identifier adj_ident = *ident;
	if (!no_adjust_symbol) {
		adjust_reserved_ident(&adj_name);
		// adjust_reserved_ident's buffer is statically allocated, so
		// temporarily duplicate adj_name.name before overwriting the
		// buffer (identifiers will be duplicated in scope_object_init)
		adj_name.name = xstrdup(adj_name.name);
		adjust_reserved_ident(&adj_ident);
	}
	struct incomplete_declaration *idecl =
		(struct incomplete_declaration *)scope_lookup(scope, &adj_name);
	ctx->unit->parent = subunit;

	if (idecl) {
		error_norec(ctx, loc, "Duplicate global identifier '%s'",
			identifier_unparse(ident));
	}
	idecl =  xcalloc(1, sizeof(struct incomplete_declaration));

	scope_object_init((struct scope_object *)idecl, O_SCAN,
			&adj_ident, &adj_name, NULL, NULL);
	if (!no_adjust_symbol) {
		free(adj_name.name);
	}
	scope_insert_from_object(scope, (struct scope_object *)idecl);
	return idecl;
}

static struct incomplete_declaration *
incomplete_declaration_create(struct context *ctx, struct location loc,
		struct scope *scope, const struct identifier *ident,
		const struct identifier *name)
incomplete_template_create(struct context *ctx,
	struct location loc,
	const struct identifier *ident)
{
	struct scope *subunit = ctx->unit->parent;
	ctx->unit->parent = NULL;
	struct identifier adjusted = *ident;
	adjust_reserved_ident(&adjusted);
	struct incomplete_declaration *idecl =
		(struct incomplete_declaration *)scope_lookup(scope, name);
		(struct incomplete_declaration *)scope_lookup(ctx->scope, &adjusted);
	ctx->unit->parent = subunit;

	if (idecl) {
		error_norec(ctx, loc, "Duplicate global identifier '%s'",
			identifier_unparse(ident));
		if (idecl->type == IDECL_ENUM_FLD
				|| (!idecl->next && !idecl->decl.template)) {
			error_norec(ctx, loc,
				"Incompatible duplicate global identifier: '%s'",
				identifier_unparse(ident));
		}
		return idecl;
	}
	idecl =  xcalloc(1, sizeof(struct incomplete_declaration));
	idecl = xcalloc(1, sizeof(struct incomplete_declaration));

	scope_object_init((struct scope_object *)idecl, O_SCAN,
			ident, name, NULL, NULL);
	scope_insert_from_object(scope, (struct scope_object *)idecl);
			&adjusted, &adjusted, NULL, NULL);
	scope_insert_from_object(ctx->scope, (struct scope_object *)idecl);

	idecl->type = IDECL_DECL;
	return idecl;
}

@@ -3792,7 +5893,7 @@ scan_enum_field(struct context *ctx, struct scope *imports,
	};
	struct incomplete_declaration *fld =
		incomplete_declaration_create(ctx, f->loc, enum_scope,
				&name, &localname);
				&name, &localname, false);
	fld->type = IDECL_ENUM_FLD;
	fld->imports = imports;
	fld->obj.type = etype,
@@ -3857,7 +5958,7 @@ scan_types(struct context *ctx, struct scope *imp, const struct ast_decl *decl)
		check_hosted_main(ctx, decl->loc, NULL, with_ns, NULL);
		struct incomplete_declaration *idecl =
			incomplete_declaration_create(ctx, decl->loc, ctx->scope,
					&with_ns, &t->ident);
					&with_ns, &t->ident, false);
		idecl->decl = (struct ast_decl){
			.decl_type = ADECL_TYPE,
			.loc = decl->loc,
@@ -4085,6 +6186,13 @@ end:
	});
}

static bool
ident_is_reserved(const struct identifier *ident)
{
	return strncmp(ident->name, "_Z", 2) == 0
		&& ident->name[strlen(ident->name) - 1] != '$';
}

void
resolve_function(struct context *ctx, struct incomplete_declaration *idecl)
{
@@ -4102,6 +6210,17 @@ resolve_function(struct context *ctx, struct incomplete_declaration *idecl)

	idecl->obj.otype = O_DECL;
	idecl->obj.type = fntype;

	// function templates need to be checked here for scoping reasons
	// functions which aren't templates are checked later, to allow for what
	// would otherwise be circular dependencies in the function body
	// XXX: circular dependencies aren't allowed in function bodies. this is
	// kinda a lazy way to get things working because the scoping stuff is
	// complicated as hell and i coded everything in a very unmaintainable
	// janky way
	if (ident_is_reserved(&idecl->obj.ident)) {
		check_function(ctx, &idecl->obj, &idecl->decl);
	}
}

void
@@ -4196,6 +6315,337 @@ end:
	});
}

static void
check_explicit_tmpl_instantiation(struct context *ctx,
	const struct scope_object *obj,
	const struct ast_decl *adecl)
{
	const struct type *type;
	switch (adecl->decl_type) {
	case ADECL_GLOBAL:
		if (adecl->global.init != NULL) {
			error(ctx, adecl->loc, NULL,
				"Explicit instantiation can't be initialized");
			return;
		}
		assert(adecl->global.type != NULL);
		type = type_store_lookup_atype(ctx, adecl->global.type);
		break;
	case ADECL_FUNC:
		if (adecl->function.body != NULL) {
			error(ctx, adecl->loc, NULL,
				"Explicit instantiation can't be implemented");
			return;
		}
		const struct ast_type fn_atype = {
			.storage = STORAGE_FUNCTION,
			.flags = 0,
			.func = adecl->function.prototype,
		};
		type = type_store_lookup_atype(ctx, &fn_atype);
		break;
	default:
		assert(0); // unreachable
	}
	if (type->id != obj->type->id) {
		error(ctx, adecl->loc, NULL,
			"Explicit instantiation's type is incompatible with template declaration");
		return;
	}
}

static void
instantiate_extern_template(struct context *ctx,