#define IR_FILE_BUFFER_BUF_LEN (4096)

struct irFileBuffer {
	gbVirtualMemory vm;
	isize           offset;
	gbFile *        output;
	char            buf[IR_FILE_BUFFER_BUF_LEN];
};

void ir_file_buffer_init(irFileBuffer *f, gbFile *output) {
	isize size = 8*gb_virtual_memory_page_size(nullptr);
	f->vm = gb_vm_alloc(nullptr, size);
	f->offset = 0;
	f->output = output;
}

void ir_file_buffer_destroy(irFileBuffer *f) {
	if (f->offset > 0) {
		// NOTE(bill): finish writing buffered data
		gb_file_write(f->output, f->vm.data, f->offset);
	}

	gb_vm_free(f->vm);
}

void ir_file_buffer_write(irFileBuffer *f, void const *data, isize len) {
	if (len > f->vm.size) {
		//NOTE(thebirk): Flush the vm data before we print this directly
		//               otherwise we get out of order printing which is no good
		gb_file_write(f->output, f->vm.data, f->offset);
		f->offset = 0;

		gb_file_write(f->output, data, len);
		return;
	}

	if ((f->vm.size - f->offset) < len) {
		gb_file_write(f->output, f->vm.data, f->offset);
		f->offset = 0;
	}
	u8 *cursor = cast(u8 *)f->vm.data + f->offset;
	gb_memmove(cursor, data, len);
	f->offset += len;
}


void ir_fprintf(irFileBuffer *f, char const *fmt, ...) {
	va_list va;
	va_start(va, fmt);
	isize len = gb_snprintf_va(f->buf, IR_FILE_BUFFER_BUF_LEN-1, fmt, va);
	ir_file_buffer_write(f, f->buf, len-1);
	va_end(va);
}
void ir_write_string(irFileBuffer *f, String s) {
	ir_file_buffer_write(f, s.text, s.len);
}

#if 1
#define ir_write_str_lit(f, s) ir_write_string((f), str_lit(s))
#else
void ir_write_str_lit(irFileBuffer *f, char const *s) {
	isize len = gb_strlen(s);
	ir_file_buffer_write(f, s, len);
}
#endif
void ir_write_byte(irFileBuffer *f, u8 c) {
	ir_file_buffer_write(f, &c, 1);
}
void ir_write_escaped_byte(irFileBuffer *f, u8 c) {
	static char const hex_table[] = "0123456789ABCDEF";
	char buf[3];
	buf[0] = '\\';
	buf[1] = hex_table[c >> 4];
	buf[2] = hex_table[c & 0x0f];
	ir_file_buffer_write(f, buf, 3);
}

void ir_write_i64(irFileBuffer *f, i64 i) {
	String str = i64_to_string(i, f->buf, IR_FILE_BUFFER_BUF_LEN-1);
	ir_write_string(f, str);
}
void ir_write_u64(irFileBuffer *f, u64 i) {
	String str = u64_to_string(i, f->buf, IR_FILE_BUFFER_BUF_LEN-1);
	ir_write_string(f, str);
}
void ir_write_big_int(irFileBuffer *f, BigInt const &x, Type *type, bool swap_endian) {
	if (x.len == 2) {
		u64 words[2] = {};
		BigInt y = x;
		if (swap_endian) {
			gb_memmove(words, y.d.words, 16);
			u8 *bytes = cast(u8 *)words;
			for (isize i = 0; i < 8; i++) {
				bytes[i] = bytes[16-i]; // byte swap
			}
			y.d.words = words;
		}

		String s = big_int_to_string(temporary_allocator(), &y, 10);
		ir_write_string(f, s);
	} else {
		i64 i = 0;
		if (x.neg) {
			i = big_int_to_i64(&x);
		} else {
			i = cast(i64)big_int_to_u64(&x);
		}
		if (swap_endian) {
			i64 size = type_size_of(type);
			switch (size) {
			case 2: i = cast(i64)cast(i16)gb_endian_swap16(cast(u16)cast(i16)i); break;
			case 4: i = cast(i64)cast(i32)gb_endian_swap32(cast(u32)cast(i32)i); break;
			case 8: i = cast(i64)gb_endian_swap64(cast(u64)i); break;
			}
		}
		ir_write_i64(f, i);
	}
}

void ir_file_write(irFileBuffer *f, void *data, isize len) {
	ir_file_buffer_write(f, data, len);
}


bool ir_valid_char(u8 c) {
	if (c >= 0x80) {
		return false;
	}

	if (gb_char_is_alphanumeric(c)) {
		return true;
	}

	switch (c) {
	case '$':
	case '-':
	case '.':
	case '_':
		return true;
	}

	return false;
}

void ir_print_escape_string(irFileBuffer *f, String name, bool print_quotes, bool prefix_with_dot) {
	isize extra = 0;
	for (isize i = 0; i < name.len; i++) {
		u8 c = name[i];
		if (!ir_valid_char(c)) {
			extra += 2;
		}
	}

	if (extra == 0) {
		ir_write_string(f, name);
		return;
	}

	if (print_quotes) {
		ir_write_byte(f, '"');
	}

	if (prefix_with_dot) {
		ir_write_byte(f, '.');
	}

	for (isize i = 0; i < name.len; i++) {
		u8 c = name[i];
		if (ir_valid_char(c)) {
			ir_write_byte(f, c);
		} else {
			ir_write_escaped_byte(f, c);
		}
	}

	if (print_quotes) {
		ir_write_byte(f, '"');
	}
}


void ir_print_escape_path(irFileBuffer *f, String path) {
	isize extra = 0;
	for (isize i = 0; i < path.len; i++) {
		u8 c = path[i];
		if (!ir_valid_char(c)) {
			extra += 2;
		}
	}

	if (extra == 0) {
		ir_write_string(f, path);
		return;
	}

	for (isize i = 0; i < path.len; i++) {
		u8 c = path[i];
		if (ir_valid_char(c) || c == ':') {
			ir_write_byte(f, c);
		} else if (c == '\\') {
			ir_write_byte(f, '/');
		} else {
			ir_write_escaped_byte(f, c);
		}
	}
}


void ir_print_encoded_local(irFileBuffer *f, String name) {
	ir_write_byte(f, '%');
	ir_print_escape_string(f, name, true, false);
}

void ir_print_encoded_global(irFileBuffer *f, String name, bool remove_prefix) {
	ir_write_byte(f, '@');
	ir_print_escape_string(f, name, true, !remove_prefix);
}


bool ir_print_debug_location(irFileBuffer *f, irModule *m, irValue *v) {
	if (!m->generate_debug_info) {
		return false;
	}

	GB_ASSERT_NOT_NULL(v);
	GB_ASSERT(v->kind == irValue_Instr);

	if (v->loc != nullptr) {
		if (v->loc->kind == irDebugInfo_Location) {
			ir_fprintf(f, ", !dbg !%d", v->loc->id);
			return true;
		}
	} else {
		irProcedure *proc = v->Instr.block->proc;
		if (proc->entity != nullptr) {
			// GB_ASSERT(proc->is_entry_point || (string_compare(proc->name, str_lit(IR_STARTUP_RUNTIME_PROC_NAME)) == 0));
		}
	}
	return false;
}

void ir_print_type(irFileBuffer *f, irModule *m, Type *t, bool in_struct = false);
void ir_print_value(irFileBuffer *f, irModule *m, irValue *value, Type *type_hint);


void ir_print_alignment_prefix_hack(irFileBuffer *f, i64 alignment) {
	// NOTE(bill): This is written like this as it may need to
	// changed for specific alignments
	switch (alignment) {
	case 1:
		ir_write_string(f, str_lit("[0 x i8]"));
		break;
	case 2:
		ir_write_string(f, str_lit("[0 x i16]"));
		break;
	case 4:
		ir_write_string(f, str_lit("[0 x i32]"));
		break;
	case 8:
		ir_write_string(f, str_lit("[0 x i64]"));
		break;
	case 16:
		ir_write_string(f, str_lit("[0 x <4 x i32>]"));
		break;
	default:
		GB_PANIC("Invalid alignment");
		break;
	}
}


void ir_print_proc_results(irFileBuffer *f, irModule *m, Type *t) {
	set_procedure_abi_types(t);

	GB_ASSERT(is_type_proc(t));
	t = base_type(t);
	isize result_count = t->Proc.result_count;
	if (result_count == 0 || t->Proc.return_by_pointer) {
		ir_write_string(f, str_lit("void"));
	} else {
		Type *rt = t->Proc.abi_compat_result_type;
		if (!is_type_tuple(rt)) {
			ir_print_type(f, m, rt);
		} else if (rt->Tuple.variables.count == 1) {
			ir_print_type(f, m, rt->Tuple.variables[0]->type);
		} else {
			ir_write_byte(f, '{');
			for_array(i, rt->Tuple.variables) {
				Entity *e = rt->Tuple.variables[i];
				if (i > 0) {
					ir_write_string(f, str_lit(", "));
				}
				ir_print_type(f, m, e->type);
			}
			ir_write_byte(f, '}');
		}
	}
}


void ir_print_proc_type_without_pointer(irFileBuffer *f, irModule *m, Type *t) {
	set_procedure_abi_types(t);

	i64 word_bits = 8*build_context.word_size;
	t = base_type(t);
	GB_ASSERT(is_type_proc(t));

	isize param_count = t->Proc.param_count;
	isize result_count = t->Proc.result_count;

	ir_print_proc_results(f, m, t);
	ir_write_string(f, str_lit(" ("));
	if (t->Proc.return_by_pointer) {
		ir_print_type(f, m, reduce_tuple_to_single_type(t->Proc.results));
		// ir_fprintf(f, "* sret noalias ");
		// ir_write_string(f, str_lit("* noalias "));
		ir_write_string(f, str_lit("*"));
		if (build_context.ODIN_OS == "darwin") {
			ir_fprintf(f, " byval");
		}
		if (param_count > 0 || t->Proc.calling_convention == ProcCC_Odin)  {
			ir_write_string(f, str_lit(", "));
		}
	}
	isize param_index = 0;
	for (isize i = 0; i < param_count; i++) {
		Entity *e = t->Proc.params->Tuple.variables[i];
		if (e->kind != Entity_Variable) continue;
		if (param_index > 0) ir_write_string(f, str_lit(", "));

		if (i+1 == param_count && t->Proc.c_vararg) {
			ir_write_string(f, str_lit("..."));
		} else {
			Type *et = t->Proc.abi_compat_params[i];
			if (is_type_tuple(et)) {
				for_array(j, et->Tuple.variables) {
					if (j > 0) ir_write_str_lit(f, ", ");

					ir_print_type(f, m, et->Tuple.variables[j]->type);
					if (e->flags&EntityFlag_NoAlias) {
						ir_write_str_lit(f, " noalias");
					}
					if (e->flags&EntityFlag_ByVal) {
						ir_write_str_lit(f, " byval");
					}
					ir_write_byte(f, ' ');
					param_index++;
				}
				continue;
			} else {
				ir_print_type(f, m, et);
			}
		}

		param_index++;
	}
	if (t->Proc.calling_convention == ProcCC_Odin) {
		if (param_index > 0) ir_write_string(f, str_lit(", "));
		ir_print_type(f, m, t_context_ptr);
	}
	ir_write_byte(f, ')');
}

void ir_print_type(irFileBuffer *f, irModule *m, Type *t, bool in_struct) {
	i64 word_bits = 8*build_context.word_size;
	GB_ASSERT_NOT_NULL(t);
	t = default_type(t);
	GB_ASSERT(is_type_typed(t));

	switch (t->kind) {
	case Type_Basic:
		switch (t->Basic.kind) {
		case Basic_llvm_bool: ir_write_str_lit(f, "i1");  return;
		case Basic_bool:      ir_write_str_lit(f, "i8");  return;
		case Basic_b8:        ir_write_str_lit(f, "i8");  return;
		case Basic_b16:       ir_write_str_lit(f, "i16"); return;
		case Basic_b32:       ir_write_str_lit(f, "i32"); return;
		case Basic_b64:       ir_write_str_lit(f, "i64"); return;

		case Basic_i8:    ir_write_str_lit(f, "i8");   return;
		case Basic_u8:    ir_write_str_lit(f, "i8");   return;
		case Basic_i16:   ir_write_str_lit(f, "i16");  return;
		case Basic_u16:   ir_write_str_lit(f, "i16");  return;
		case Basic_i32:   ir_write_str_lit(f, "i32");  return;
		case Basic_u32:   ir_write_str_lit(f, "i32");  return;
		case Basic_i64:   ir_write_str_lit(f, "i64");  return;
		case Basic_u64:   ir_write_str_lit(f, "i64");  return;
		case Basic_i128:  ir_write_str_lit(f, "i128"); return;
		case Basic_u128:  ir_write_str_lit(f, "i128"); return;

		case Basic_i16le:  ir_write_str_lit(f, "i16");  return;
		case Basic_u16le:  ir_write_str_lit(f, "i16");  return;
		case Basic_i32le:  ir_write_str_lit(f, "i32");  return;
		case Basic_u32le:  ir_write_str_lit(f, "i32");  return;
		case Basic_i64le:  ir_write_str_lit(f, "i64");  return;
		case Basic_u64le:  ir_write_str_lit(f, "i64");  return;
		case Basic_i128le: ir_write_str_lit(f, "i128"); return;
		case Basic_u128le: ir_write_str_lit(f, "i128"); return;

		case Basic_i16be:  ir_write_str_lit(f, "i16");  return;
		case Basic_u16be:  ir_write_str_lit(f, "i16");  return;
		case Basic_i32be:  ir_write_str_lit(f, "i32");  return;
		case Basic_u32be:  ir_write_str_lit(f, "i32");  return;
		case Basic_i64be:  ir_write_str_lit(f, "i64");  return;
		case Basic_u64be:  ir_write_str_lit(f, "i64");  return;
		case Basic_i128be: ir_write_str_lit(f, "i128"); return;
		case Basic_u128be: ir_write_str_lit(f, "i128"); return;

		case Basic_rune: ir_write_str_lit(f, "i32"); return;

		case Basic_int:
		case Basic_uint:
		case Basic_uintptr:
			switch (word_bits) {
			case 32: ir_write_str_lit(f, "i32"); break;
			case 64: ir_write_str_lit(f, "i64"); break;
			default: GB_PANIC("Unhandled word size: %td bits", word_bits); break;
			}
			return;

		case Basic_f16:    ir_write_str_lit(f, "half");     return;
		case Basic_f32:    ir_write_str_lit(f, "float");    return;
		case Basic_f64:    ir_write_str_lit(f, "double");   return;


		case Basic_f16le:    ir_write_str_lit(f, "half");   return;
		case Basic_f32le:    ir_write_str_lit(f, "float");  return;
		case Basic_f64le:    ir_write_str_lit(f, "double"); return;

		case Basic_f16be:    ir_write_str_lit(f, "half");    return;
		case Basic_f32be:    ir_write_str_lit(f, "float");   return;
		case Basic_f64be:    ir_write_str_lit(f, "double");  return;

		case Basic_complex32:  ir_write_str_lit(f, "%%..complex32");       return;
		case Basic_complex64:  ir_write_str_lit(f, "%..complex64");        return;
		case Basic_complex128: ir_write_str_lit(f, "%..complex128");       return;

		case Basic_quaternion64:  ir_write_str_lit(f, "%..quaternion64");  return;
		case Basic_quaternion128: ir_write_str_lit(f, "%..quaternion128"); return;
		case Basic_quaternion256: ir_write_str_lit(f, "%..quaternion256"); return;

		case Basic_any:     ir_write_str_lit(f, "%..any");                 return;
		case Basic_rawptr:  ir_write_str_lit(f, "%..rawptr");              return;
		case Basic_string:  ir_write_str_lit(f, "%..string");              return;
		case Basic_cstring: ir_write_str_lit(f, "i8*");                    return;

		case Basic_typeid:  ir_write_str_lit(f, "%..typeid");              return;
		}
		break;

	case Type_Pointer:
		// TODO(bill, 2019-12-22): Why did I make add this edge case? Is this some weird LLVM issue?
		// if (!is_type_named(t->Pointer.elem) && is_type_empty_struct(t->Pointer.elem)) {
		// 	ir_print_type(f, m, t_rawptr);
		// } else {
			ir_print_type(f, m, t->Pointer.elem);
			ir_write_byte(f, '*');
		// }
		return;
	case Type_Array:
		ir_write_byte(f, '[');
		ir_write_i64(f, t->Array.count);
		ir_write_str_lit(f, " x ");
		ir_print_type(f, m, t->Array.elem);
		ir_write_byte(f, ']');
		return;
	case Type_EnumeratedArray:
		ir_write_byte(f, '[');
		ir_write_i64(f, t->EnumeratedArray.count);
		ir_write_str_lit(f, " x ");
		ir_print_type(f, m, t->EnumeratedArray.elem);
		ir_write_byte(f, ']');
		return;

	case Type_Slice:
		ir_write_byte(f, '{');
		ir_print_type(f, m, t->Slice.elem); ir_write_str_lit(f, "*, ");
		ir_print_type(f, m, t_int);
		ir_write_byte(f, '}');
		return;
	case Type_DynamicArray:
		ir_write_byte(f, '{');
		ir_print_type(f, m, t->DynamicArray.elem); ir_write_str_lit(f, "*, ");
		ir_print_type(f, m, t_int); ir_write_str_lit(f, ", ");
		ir_print_type(f, m, t_int); ir_write_str_lit(f, ", ");
		ir_print_type(f, m, t_allocator);
		ir_write_byte(f, '}');
		return;

	case Type_Enum:
		ir_print_type(f, m, base_enum_type(t));
		return;

	case Type_Union:
		if (t->Union.variants.count == 0) {
			ir_print_encoded_local(f, str_lit("..opaque"));
		} else {
			// NOTE(bill): The zero size array is used to fix the alignment used in a structure as
			// LLVM takes the first element's alignment as the entire alignment (like C)
			i64 align = type_align_of(t);

			if (is_type_union_maybe_pointer_original_alignment(t)) {
				ir_write_byte(f, '{');
				ir_print_type(f, m, t->Union.variants[0]);
				ir_write_byte(f, '}');
				return;
			}

			i64 block_size =  t->Union.variant_block_size;

			ir_write_byte(f, '{');
			ir_print_alignment_prefix_hack(f, align);
			if (is_type_union_maybe_pointer(t)) {
				ir_fprintf(f, ", ");
				ir_print_type(f, m, t->Union.variants[0]);
			} else {
				ir_fprintf(f, ", [%lld x i8], ", block_size);
				ir_print_type(f, m, union_tag_type(t));
			}
			ir_write_byte(f, '}');
		}
		return;

	case Type_Struct: {
		if (t->Struct.is_raw_union) {
			// NOTE(bill): The zero size array is used to fix the alignment used in a structure as
			// LLVM takes the first element's alignment as the entire alignment (like C)
			i64 size_of_union  = type_size_of(t);
			i64 align_of_union = type_align_of(t);
			ir_write_byte(f, '{');
			ir_print_alignment_prefix_hack(f, align_of_union);
			ir_fprintf(f, ", [%lld x i8]}", size_of_union);
			return;
		} else {
			if (t->Struct.is_packed) {
				ir_write_byte(f, '<');
			}
			ir_write_byte(f, '{');
			if (t->Struct.custom_align > 0) {
				ir_print_alignment_prefix_hack(f, t->Struct.custom_align);
				if (t->Struct.fields.count > 0) {
					ir_write_string(f, str_lit(", "));
				}
			}
			for_array(i, t->Struct.fields) {
				if (i > 0) {
					ir_write_string(f, str_lit(", "));
				}
				ir_print_type(f, m, t->Struct.fields[i]->type, true);
			}
			ir_write_byte(f, '}');
			if (t->Struct.is_packed) {
				ir_write_byte(f, '>');
			}
			return;
		}
		break;
	}

	case Type_Named: {
		Type *bt = base_type(t);
		switch (bt->kind) {
		case Type_Struct:
		case Type_Union: {
			Entity *e = t->Named.type_name;
			GB_ASSERT(e != nullptr);
			GB_ASSERT(e->kind == Entity_TypeName);

			String name = e->TypeName.ir_mangled_name;
			if (name.len > 0) {
				ir_print_encoded_local(f, name);
			} else {
				// TODO(bill): Is this correct behaviour?!
				// GB_ASSERT_MSG(name.len > 0, "%.*s %p", LIT(t->Named.name), e);
				// gb_printf_err("%.*s %p\n", LIT(t->Named.name), t->Named.type_name);
				ir_print_type(f, m, bt);
			}
			break;
		}
		default:
			ir_print_type(f, m, bt);
			break;
		}
		return;
	}

	case Type_Tuple:
		if (t->Tuple.variables.count == 1) {
			ir_print_type(f, m, t->Tuple.variables[0]->type);
		} else {
			ir_write_byte(f, '{');
			isize index = 0;
			for_array(i, t->Tuple.variables) {
				if (i > 0) ir_write_string(f, str_lit(", "));
				Entity *e = t->Tuple.variables[i];
				if (e->kind == Entity_Variable) {
					ir_print_type(f, m, e->type);
					index++;
				}
			}
			ir_write_byte(f, '}');
		}
		return;

	case Type_Proc:
		ir_print_proc_type_without_pointer(f, m, t);
		ir_write_byte(f, '*');
		return;

	case Type_Map:
		init_map_internal_types(t);
		GB_ASSERT(t->Map.internal_type != nullptr);
		ir_print_type(f, m, t->Map.internal_type);
		break;

	case Type_BitSet: {
		i64 size = type_size_of(t);
		if (size == 0) {
			ir_write_str_lit(f, "{}");
			return;
		}
		ir_print_type(f, m, bit_set_to_int(t));
		return;
	}

	case Type_SimdVector:
		ir_fprintf(f, "<%lld x ", t->SimdVector.count);;
		ir_print_type(f, m, t->SimdVector.elem);
		ir_write_byte(f, '>');
		return;

	case Type_RelativePointer:
		ir_print_type(f, m, t->RelativePointer.base_integer);
		return;

	case Type_RelativeSlice:
		ir_write_byte(f, '{');
		ir_print_type(f, m, t->RelativePointer.base_integer);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, t->RelativePointer.base_integer);
		ir_write_byte(f, '}');
		return;
	}
}

void ir_print_debug_encoding(irFileBuffer *f, irDebugInfoKind kind, irDebugEncoding encoding) {
	switch (kind) {
	case irDebugInfo_BasicType:
		switch (encoding) {
		case irDebugBasicEncoding_address:       ir_write_str_lit(f, "DW_ATE_address");       return;
		case irDebugBasicEncoding_boolean:       ir_write_str_lit(f, "DW_ATE_boolean");       return;
		case irDebugBasicEncoding_float:         ir_write_str_lit(f, "DW_ATE_float");         return;
		case irDebugBasicEncoding_signed:        ir_write_str_lit(f, "DW_ATE_signed");        return;
		case irDebugBasicEncoding_signed_char:   ir_write_str_lit(f, "DW_ATE_signed_char");   return;
		case irDebugBasicEncoding_unsigned:      ir_write_str_lit(f, "DW_ATE_unsigned");      return;
		case irDebugBasicEncoding_unsigned_char: ir_write_str_lit(f, "DW_ATE_unsigned_char"); return;
		}
	case irDebugInfo_DerivedType:
		switch (encoding) {
		case irDebugBasicEncoding_member:       ir_write_str_lit(f, "DW_TAG_member");       return;
		case irDebugBasicEncoding_pointer_type: ir_write_str_lit(f, "DW_TAG_pointer_type"); return;
		case irDebugBasicEncoding_typedef:      ir_write_str_lit(f, "DW_TAG_typedef");      return;
		}
	case irDebugInfo_CompositeType:
		switch (encoding) {
		case irDebugBasicEncoding_array_type:       ir_write_str_lit(f, "DW_TAG_array_type");       return;
		case irDebugBasicEncoding_enumeration_type: ir_write_str_lit(f, "DW_TAG_enumeration_type"); return;
		case irDebugBasicEncoding_structure_type:   ir_write_str_lit(f, "DW_TAG_structure_type");   return;
		case irDebugBasicEncoding_union_type:       ir_write_str_lit(f, "DW_TAG_union_type");       return;
		}
	}

	GB_PANIC("Unreachable");
}

void ir_print_exact_value(irFileBuffer *f, irModule *m, ExactValue value, Type *type);

void ir_print_compound_element(irFileBuffer *f, irModule *m, ExactValue v, Type *elem_type) {
	ir_print_type(f, m, elem_type);
	ir_write_byte(f, ' ');

	if (v.kind == ExactValue_Invalid || !elem_type_can_be_constant(elem_type)) {
		ir_fprintf(f, "zeroinitializer");
	} else {
		ir_print_exact_value(f, m, v, elem_type);
	}
}

void ir_print_exact_value(irFileBuffer *f, irModule *m, ExactValue value, Type *type) {
	Type *original_type = type;
	type = core_type(type);
	value = convert_exact_value_for_type(value, type);

	// NOTE(bill): Is this correct? Does this handle all cases regarding arrays?
	if (is_type_array(type) && value.kind == ExactValue_String && !is_type_u8(core_array_type(type))) {
		i64 count  = type->Array.count;
		Type *elem = type->Array.elem;

		if (is_type_rune_array(type)) {
			Rune rune;
			isize offset = 0;
			isize width = 1;
			String s = value.value_string;
			ir_write_byte(f, '[');
			for (i64 i = 0; i < count && offset < s.len; i++) {
				width = gb_utf8_decode(s.text+offset, s.len-offset, &rune);
				if (i > 0) ir_write_str_lit(f, ", ");
				ir_print_type(f, m, elem);
				ir_write_byte(f, ' ');
				ir_print_exact_value(f, m, exact_value_i64(rune), elem);
				offset += width;
			}
			GB_ASSERT(offset == s.len);

			ir_write_byte(f, ']');
			return;
		}


		ir_write_byte(f, '[');

		for (i64 i = 0; i < count; i++) {
			if (i > 0) ir_write_str_lit(f, ", ");
			ir_print_type(f, m, elem);
			ir_write_byte(f, ' ');
			ir_print_exact_value(f, m, value, elem);
		}

		ir_write_byte(f, ']');
		return;
	}  else if (is_type_array(type) &&
	    value.kind != ExactValue_Invalid &&
	    value.kind != ExactValue_String &&
	    value.kind != ExactValue_Compound) {
		i64 count  = type->Array.count;
		Type *elem = type->Array.elem;
		ir_write_byte(f, '[');

		for (i64 i = 0; i < count; i++) {
			if (i > 0) ir_write_str_lit(f, ", ");
			ir_print_type(f, m, elem);
			ir_write_byte(f, ' ');
			ir_print_exact_value(f, m, value, elem);
		}

		ir_write_byte(f, ']');
		return;
	}

	switch (value.kind) {
	case ExactValue_Typeid:
		GB_ASSERT(is_type_typeid(type));
		ir_write_u64(f, ir_typeid_as_integer(m, value.value_typeid));
		break;

	case ExactValue_Bool:
		if (value.value_bool) {
			ir_write_string(f, are_types_identical(type, t_llvm_bool) ? str_lit("true") : str_lit("1"));
		} else {
			ir_write_string(f, are_types_identical(type, t_llvm_bool) ? str_lit("false") : str_lit("0"));
		}
		break;
	case ExactValue_String: {
		String str = value.value_string;
		Type *t = core_type(type);
		if (str.len == 0 && !is_type_cstring(t)) {
			ir_write_str_lit(f, "zeroinitializer");
			break;
		}
		if (is_type_u8_slice(type)) {
			irValue *str_array = ir_add_global_string_array(m, str);
			ir_write_str_lit(f, "{i8* getelementptr inbounds (");
			ir_print_type(f, m, str_array->Global.entity->type);
			ir_write_str_lit(f, ", ");
			ir_print_type(f, m, str_array->Global.entity->type);
			ir_write_str_lit(f, "* ");
			ir_print_encoded_global(f, str_array->Global.entity->token.string, false);
			ir_write_str_lit(f, ", ");
			ir_print_type(f, m, t_i32);
			ir_write_str_lit(f, " 0, i32 0), ");
			ir_print_type(f, m, t_int);
			ir_fprintf(f, " %lld}", cast(i64)str.len);
		} else if (!is_type_string(type)) {
			GB_ASSERT(is_type_array(type));
			ir_write_str_lit(f, "c\"");
			ir_print_escape_string(f, str, false, false);
			if (type->Array.count == str.len) {
				ir_write_str_lit(f, "\"");
			} else {
				ir_write_str_lit(f, "\\00\"");
			}
		} else if (is_type_cstring(t)) {
			// HACK NOTE(bill): This is a hack but it works because strings are created at the very end
			// of the .ll file
			irValue *str_array = ir_add_global_string_array(m, str);
			ir_write_str_lit(f, "getelementptr inbounds (");
			ir_print_type(f, m, str_array->Global.entity->type);
			ir_write_str_lit(f, ", ");
			ir_print_type(f, m, str_array->Global.entity->type);
			ir_write_str_lit(f, "* ");
			ir_print_encoded_global(f, str_array->Global.entity->token.string, false);
			ir_write_str_lit(f, ", ");
			ir_print_type(f, m, t_i32);
			ir_write_str_lit(f, " 0, i32 0)");
		} else {
			// HACK NOTE(bill): This is a hack but it works because strings are created at the very end
			// of the .ll file
			irValue *str_array = ir_add_global_string_array(m, str);
			ir_write_str_lit(f, "{i8* getelementptr inbounds (");
			ir_print_type(f, m, str_array->Global.entity->type);
			ir_write_str_lit(f, ", ");
			ir_print_type(f, m, str_array->Global.entity->type);
			ir_write_str_lit(f, "* ");
			ir_print_encoded_global(f, str_array->Global.entity->token.string, false);
			ir_write_str_lit(f, ", ");
			ir_print_type(f, m, t_i32);
			ir_write_str_lit(f, " 0, i32 0), ");
			ir_print_type(f, m, t_int);
			ir_fprintf(f, " %lld}", cast(i64)str.len);
		}
		break;
	}
	case ExactValue_Integer: {
		if (is_type_pointer(type)) {
			if (big_int_is_zero(&value.value_integer)) {
				ir_write_str_lit(f, "null");
			} else {
				ir_write_str_lit(f, "inttoptr (");
				ir_print_type(f, m, t_int);
				ir_write_byte(f, ' ');
				ir_write_big_int(f, value.value_integer, type, is_type_different_to_arch_endianness(type));
				ir_write_str_lit(f, " to ");
				ir_print_type(f, m, t_rawptr);
				ir_write_str_lit(f, ")");
			}
		} else {
			ir_write_big_int(f, value.value_integer, type, is_type_different_to_arch_endianness(type));
		}
		break;
	}
	case ExactValue_Float: {
		GB_ASSERT_MSG(is_type_float(type), "%s", type_to_string(type));
		type = core_type(type);
		u64 u_64 = bit_cast<u64>(value.value_float);
		u32 u_32 = bit_cast<u32>(cast(f32)value.value_float);


	#if 0
		switch (type->Basic.kind) {
		case Basic_f32:
			// IMPORTANT NOTE(bill): LLVM requires all floating point constants to be
			// a 64 bit number if bits_of(float type) <= 64.
			// https://groups.google.com/forum/#!topic/llvm-dev/IlqV3TbSk6M
			// 64 bit mantissa: 52 bits ==> 52-52 ==  0
			// 32 bit mantissa: 23 bits ==> 52-23 == 29
			// 16 bit mantissa: 10 bits ==> 52=10 == 42
			// 29 == 52-23
			u >>= 29;
			u <<= 29;
			break;
		// case Basic_f16:
			// u >>= 42;
			// u <<= 42;
			// break;
		}

		switch (type->Basic.kind) {
		case 0: break;
		default:
			ir_fprintf(f, "0x%016llx", u);
			break;
		}
	#else
		switch (type->Basic.kind) {
		case Basic_f16:
			ir_fprintf(f, "fptrunc (float bitcast (i32 %u to float) to half)", u_32);
			break;
		case Basic_f16le:
			if (build_context.endian_kind != TargetEndian_Little) {
				u_32 = gb_endian_swap32(u_32);
			}
			ir_fprintf(f, "fptrunc (float bitcast (i32 %u to float) to half)", u_32);
			break;
		case Basic_f16be:
			if (build_context.endian_kind != TargetEndian_Big) {
				u_32 = gb_endian_swap32(u_32);
			}
			ir_fprintf(f, "fptrunc (float bitcast (i32 %u to float) to half)", u_32);
			break;


		case Basic_f32:
			ir_fprintf(f, "bitcast (i32 %u to float)", u_32);
			break;
		case Basic_f32le:
			if (build_context.endian_kind != TargetEndian_Little) {
				u_32 = gb_endian_swap32(u_32);
			}
			ir_fprintf(f, "bitcast (i32 %u to float)", u_32);
			break;
		case Basic_f32be:
			if (build_context.endian_kind != TargetEndian_Big) {
				u_32 = gb_endian_swap32(u_32);
			}
			ir_fprintf(f, "bitcast (i32 %u to float)", u_32);
			break;

		case Basic_f64:
			ir_fprintf(f, "0x%016llx", u_64);
			break;
		case Basic_f64le:
			if (build_context.endian_kind != TargetEndian_Little) {
				u_64 = gb_endian_swap64(u_64);
			}
			ir_fprintf(f, "0x%016llx", u_64);
			break;
		case Basic_f64be:
			if (build_context.endian_kind != TargetEndian_Big) {
				u_64 = gb_endian_swap64(u_64);
			}
			ir_fprintf(f, "0x%016llx", u_64);
			break;

		default:
			ir_fprintf(f, "0x%016llx", u_64);
			break;
		}
	#endif
		break;
	}
	case ExactValue_Complex: {
		// xy/ri format
		type = core_type(type);
		GB_ASSERT_MSG(is_type_complex(type), "%s", type_to_string(type));
		Type *ft = base_complex_elem_type(type);
		ir_write_byte(f, ' ');
		ir_write_byte(f, '{');
		ir_print_type(f, m, ft); ir_write_byte(f, ' ');
		ir_print_exact_value(f, m, exact_value_float(value.value_complex->real), ft);
		ir_write_str_lit(f, ", "); ir_print_type(f, m, ft); ir_write_byte(f, ' ');
		ir_print_exact_value(f, m, exact_value_float(value.value_complex->imag), ft);
		ir_write_byte(f, '}');
		break;
	}

	case ExactValue_Quaternion: {
		// xyzw/ijkr format
		type = core_type(type);
		GB_ASSERT_MSG(is_type_quaternion(type), "%s", type_to_string(type));
		Type *ft = base_complex_elem_type(type);
		ir_write_byte(f, ' ');
		ir_write_byte(f, '{');
		ir_print_type(f, m, ft); ir_write_byte(f, ' ');
		ir_print_exact_value(f, m, exact_value_float(value.value_quaternion->imag), ft);
		ir_write_str_lit(f, ", "); ir_print_type(f, m, ft); ir_write_byte(f, ' ');
		ir_print_exact_value(f, m, exact_value_float(value.value_quaternion->jmag), ft);
		ir_write_str_lit(f, ", "); ir_print_type(f, m, ft); ir_write_byte(f, ' ');
		ir_print_exact_value(f, m, exact_value_float(value.value_quaternion->kmag), ft);
		ir_write_str_lit(f, ", "); ir_print_type(f, m, ft); ir_write_byte(f, ' ');
		ir_print_exact_value(f, m, exact_value_float(value.value_quaternion->real), ft);
		ir_write_byte(f, '}');
		break;
	}

	case ExactValue_Pointer:
		if (value.value_pointer == 0) {
			if (is_type_typeid(type)) {
				ir_write_str_lit(f, "0");
			} else {
				ir_write_str_lit(f, "null");
			}
		} else {
			ir_write_str_lit(f, "inttoptr (");
			ir_print_type(f, m, t_int);
			ir_fprintf(f, " %llu to ", cast(u64)cast(uintptr)value.value_pointer);
			ir_print_type(f, m, t_rawptr);
			ir_write_byte(f, ')');
		}
		break;
	case ExactValue_Compound: {
		type = base_type(type);
		if (is_type_slice(type)) {
			irValue *s = ir_add_module_constant(m, type, value);
			ir_print_value(f, m, s, type);
		} else if (is_type_array(type)) {
			ast_node(cl, CompoundLit, value.value_compound);

			Type *elem_type = type->Array.elem;
			isize elem_count = cl->elems.count;
			if (elem_count == 0) {
				ir_write_str_lit(f, "zeroinitializer");
				break;
			}
			if (cl->elems[0]->kind == Ast_FieldValue) {
				// TODO(bill): This is O(N*M) and will be quite slow; it should probably be sorted before hand
				ir_write_byte(f, '[');
				for (i64 i = 0; i < type->Array.count; i++) {
					if (i > 0) ir_write_str_lit(f, ", ");

					bool found = false;

					for (isize j = 0; j < elem_count; j++) {
						Ast *elem = cl->elems[j];
						ast_node(fv, FieldValue, elem);
						if (is_ast_range(fv->field)) {
							ast_node(ie, BinaryExpr, fv->field);
							TypeAndValue lo_tav = ie->left->tav;
							TypeAndValue hi_tav = ie->right->tav;
							GB_ASSERT(lo_tav.mode == Addressing_Constant);
							GB_ASSERT(hi_tav.mode == Addressing_Constant);

							TokenKind op = ie->op.kind;
							i64 lo = exact_value_to_i64(lo_tav.value);
							i64 hi = exact_value_to_i64(hi_tav.value);
							if (op == Token_Ellipsis) {
								hi += 1;
							}
							if (lo == i) {
								TypeAndValue tav = fv->value->tav;
								if (tav.mode != Addressing_Constant) {
									break;
								}
								for (i64 k = lo; k < hi; k++) {
									if (k > lo) ir_write_str_lit(f, ", ");

									ir_print_compound_element(f, m, tav.value, elem_type);
								}

								found = true;
								i += (hi-lo-1);
								break;
							}
						} else {
							TypeAndValue index_tav = fv->field->tav;
							GB_ASSERT(index_tav.mode == Addressing_Constant);
							i64 index = exact_value_to_i64(index_tav.value);
							if (index == i) {
								TypeAndValue tav = fv->value->tav;
								if (tav.mode != Addressing_Constant) {
									break;
								}
								ir_print_compound_element(f, m, tav.value, elem_type);
								found = true;
								break;
							}
						}
					}

					if (!found) {
						ir_print_type(f, m, elem_type);
						ir_write_byte(f, ' ');
						ir_write_str_lit(f, "zeroinitializer");
					}
				}
				ir_write_byte(f, ']');
			} else {
				GB_ASSERT_MSG(elem_count == type->Array.count, "%td != %td", elem_count, type->Array.count);

				ir_write_byte(f, '[');

				for (isize i = 0; i < elem_count; i++) {
					if (i > 0) ir_write_str_lit(f, ", ");
					TypeAndValue tav = cl->elems[i]->tav;
					GB_ASSERT(tav.mode != Addressing_Invalid);
					ir_print_compound_element(f, m, tav.value, elem_type);
				}
				for (isize i = elem_count; i < type->Array.count; i++) {
					if (i >= elem_count) ir_write_str_lit(f, ", ");
					ir_print_compound_element(f, m, empty_exact_value, elem_type);
				}

				ir_write_byte(f, ']');
			}
		} else if (is_type_enumerated_array(type)) {
			ast_node(cl, CompoundLit, value.value_compound);

			Type *elem_type = type->EnumeratedArray.elem;
			isize elem_count = cl->elems.count;
			if (elem_count == 0) {
				ir_write_str_lit(f, "zeroinitializer");
				break;
			}
			if (cl->elems[0]->kind == Ast_FieldValue) {
				// TODO(bill): This is O(N*M) and will be quite slow; it should probably be sorted before hand
				ir_write_byte(f, '[');

				i64 total_lo = exact_value_to_i64(type->EnumeratedArray.min_value);
				i64 total_hi = exact_value_to_i64(type->EnumeratedArray.max_value);

				for (i64 i = total_lo; i <= total_hi; i++) {
					if (i > total_lo) ir_write_str_lit(f, ", ");

					bool found = false;

					for (isize j = 0; j < elem_count; j++) {
						Ast *elem = cl->elems[j];
						ast_node(fv, FieldValue, elem);
						if (is_ast_range(fv->field)) {
							ast_node(ie, BinaryExpr, fv->field);
							TypeAndValue lo_tav = ie->left->tav;
							TypeAndValue hi_tav = ie->right->tav;
							GB_ASSERT(lo_tav.mode == Addressing_Constant);
							GB_ASSERT(hi_tav.mode == Addressing_Constant);

							TokenKind op = ie->op.kind;
							i64 lo = exact_value_to_i64(lo_tav.value);
							i64 hi = exact_value_to_i64(hi_tav.value);
							if (op == Token_Ellipsis) {
								hi += 1;
							}
							if (lo == i) {
								TypeAndValue tav = fv->value->tav;
								if (tav.mode != Addressing_Constant) {
									break;
								}
								for (i64 k = lo; k < hi; k++) {
									if (k > lo) ir_write_str_lit(f, ", ");

									ir_print_compound_element(f, m, tav.value, elem_type);
								}

								found = true;
								i += (hi-lo-1);
								break;
							}
						} else {
							TypeAndValue index_tav = fv->field->tav;
							GB_ASSERT(index_tav.mode == Addressing_Constant);
							i64 index = exact_value_to_i64(index_tav.value);
							if (index == i) {
								TypeAndValue tav = fv->value->tav;
								if (tav.mode != Addressing_Constant) {
									break;
								}
								ir_print_compound_element(f, m, tav.value, elem_type);
								found = true;
								break;
							}
						}
					}

					if (!found) {
						ir_print_type(f, m, elem_type);
						ir_write_byte(f, ' ');
						ir_write_str_lit(f, "zeroinitializer");
					}
				}
				ir_write_byte(f, ']');
			} else {
				GB_ASSERT_MSG(elem_count == type->EnumeratedArray.count, "%td != %td", elem_count, type->EnumeratedArray.count);

				ir_write_byte(f, '[');

				for (isize i = 0; i < elem_count; i++) {
					if (i > 0) ir_write_str_lit(f, ", ");
					TypeAndValue tav = cl->elems[i]->tav;
					GB_ASSERT(tav.mode != Addressing_Invalid);
					ir_print_compound_element(f, m, tav.value, elem_type);
				}
				for (isize i = elem_count; i < type->EnumeratedArray.count; i++) {
					if (i >= elem_count) ir_write_str_lit(f, ", ");
					ir_print_compound_element(f, m, empty_exact_value, elem_type);
				}

				ir_write_byte(f, ']');
			}
		} else if (is_type_simd_vector(type)) {
			ast_node(cl, CompoundLit, value.value_compound);

			Type *elem_type = type->SimdVector.elem;
			isize elem_count = cl->elems.count;
			if (elem_count == 0) {
				ir_write_str_lit(f, "zeroinitializer");
				break;
			}
			GB_ASSERT_MSG(elem_count == type->SimdVector.count, "%td != %td", elem_count, type->SimdVector.count);

			ir_write_byte(f, '<');

			for (isize i = 0; i < elem_count; i++) {
				if (i > 0) ir_write_str_lit(f, ", ");
				TypeAndValue tav = cl->elems[i]->tav;
				GB_ASSERT(tav.mode != Addressing_Invalid);
				ir_print_compound_element(f, m, tav.value, elem_type);
			}
			for (isize i = elem_count; i < type->SimdVector.count; i++) {
				if (i >= elem_count) ir_write_str_lit(f, ", ");
				ir_print_compound_element(f, m, empty_exact_value, elem_type);
			}

			ir_write_byte(f, '>');
		} else if (is_type_struct(type)) {
			gbTempArenaMemory tmp = gb_temp_arena_memory_begin(&m->tmp_arena);
			defer (gb_temp_arena_memory_end(tmp));

			ast_node(cl, CompoundLit, value.value_compound);

			if (cl->elems.count == 0) {
				ir_write_str_lit(f, "zeroinitializer");
				break;
			}

			String tstr = make_string_c(type_to_string(original_type));

			isize value_count = type->Struct.fields.count;
			ExactValue *values = gb_alloc_array(m->tmp_allocator, ExactValue, value_count);
			bool *visited = gb_alloc_array(m->tmp_allocator, bool, value_count);

			if (cl->elems.count > 0) {
				if (cl->elems[0]->kind == Ast_FieldValue) {
					isize elem_count = cl->elems.count;
					for (isize i = 0; i < elem_count; i++) {
						ast_node(fv, FieldValue, cl->elems[i]);
						String name = fv->field->Ident.token.string;

						TypeAndValue tav = fv->value->tav;
						GB_ASSERT(tav.mode != Addressing_Invalid);

						Selection sel = lookup_field(type, name, false);
						Entity *f = type->Struct.fields[sel.index[0]];

						values[f->Variable.field_src_index] = tav.value;
						visited[f->Variable.field_src_index] = true;
					}
				} else {
					for_array(i, cl->elems) {
						Entity *f = type->Struct.fields[i];
						TypeAndValue tav = cl->elems[i]->tav;
						ExactValue val = {};
						if (tav.mode != Addressing_Invalid) {
							val = tav.value;
						}
						values[f->Variable.field_src_index]  = val;
						visited[f->Variable.field_src_index] = true;
					}
				}
			}

			if (type->Struct.is_packed) ir_write_byte(f, '<');
			ir_write_byte(f, '{');
			if (type->Struct.custom_align > 0) {
				ir_print_alignment_prefix_hack(f, cast(i64)type->Struct.custom_align);
				ir_write_str_lit(f, " zeroinitializer");
				if (value_count > 0) {
					ir_write_string(f, str_lit(", "));
				}
			}


			for (isize i = 0; i < value_count; i++) {
				if (i > 0) ir_write_string(f, str_lit(", "));
				Entity *e = type->Struct.fields[i];
				GB_ASSERT(e->kind == Entity_Variable);
				ir_print_compound_element(f, m, values[e->Variable.field_src_index], e->type);
			}


			ir_write_byte(f, '}');
			if (type->Struct.is_packed) ir_write_byte(f, '>');
		} else if (is_type_bit_set(type)) {
			ast_node(cl, CompoundLit, value.value_compound);
			if (cl->elems.count == 0) {
				ir_write_str_lit(f, "zeroinitializer");
				break;
			}

			i64 sz = type_size_of(type);
			if (sz == 0) {
				ir_write_str_lit(f, "zeroinitializer");
				break;
			}

			u64 bits = 0;
			for_array(i, cl->elems) {
				Ast *e = cl->elems[i];
				GB_ASSERT(e->kind != Ast_FieldValue);

				TypeAndValue tav = e->tav;
				if (tav.mode != Addressing_Constant) {
					continue;
				}
				GB_ASSERT(tav.value.kind == ExactValue_Integer);
				i64 v = big_int_to_i64(&tav.value.value_integer);
				i64 lower = type->BitSet.lower;
				bits |= 1ull<<cast(u64)(v-lower);
			}
			if (is_type_different_to_arch_endianness(type)) {
				i64 size = type_size_of(type);
				switch (size) {
				case 2: bits = cast(u64)gb_endian_swap16(cast(u16)bits); break;
				case 4: bits = cast(u64)gb_endian_swap32(cast(u32)bits); break;
				case 8: bits = cast(u64)gb_endian_swap64(cast(u64)bits); break;
				}
			}
			ir_write_u64(f, bits);
		} else {
			ir_write_str_lit(f, "zeroinitializer");
		}

		break;
	}
	case ExactValue_Procedure: {
		irValue **found = nullptr;
		Ast *expr = unparen_expr(value.value_procedure);
		GB_ASSERT(expr != nullptr);

		if (expr->kind == Ast_ProcLit) {
			found = map_get(&m->anonymous_proc_lits, hash_pointer(expr));
		} else {
			Entity *e = strip_entity_wrapping(expr);
			GB_ASSERT(e != nullptr);
			found = map_get(&m->values, hash_entity(e));
		}
		GB_ASSERT_MSG(found != nullptr, "%s", expr_to_string(expr));
		irValue *val = *found;
		ir_print_value(f, m, val, type);
		break;
	}
	default:
		ir_write_str_lit(f, "zeroinitializer");
		break;
	}
}

void ir_print_block_name(irFileBuffer *f, irBlock *b) {
	if (b != nullptr) {
		ir_print_escape_string(f, b->label, false, false);
		ir_fprintf(f, "-%td", b->index);
	} else {
		ir_write_str_lit(f, "<INVALID-BLOCK>");
	}
}

bool ir_print_is_proc_global(irModule *m, irProcedure *proc) {
	if (proc->entity != nullptr &&
	    proc->entity->kind == Entity_Procedure) {
		if (m->entry_point_entity == proc->entity) {
			// gb_printf("%.*s\n", LIT(proc->entity->token.string));
			// TODO(bill): This may not be needed during windows
			return true;
		}
		if (proc->entity->Procedure.link_name.len > 0) {
		    return true;
		}
	}
	return proc->is_foreign || proc->is_export;
}

void ir_print_value(irFileBuffer *f, irModule *m, irValue *value, Type *type_hint) {
	if (value == nullptr) {
		ir_write_str_lit(f, "!!!nullptr_VALUE");
		return;
	}
	switch (value->kind) {
	default: GB_PANIC("Unknown irValue kind"); break;

	case irValue_Constant:
		ir_print_exact_value(f, m, value->Constant.value, type_hint);
		break;

	case irValue_ConstantSlice: {
		irValueConstantSlice *cs = &value->ConstantSlice;
		if (cs->backing_array == nullptr || cs->count == 0) {
			ir_write_str_lit(f, "zeroinitializer");
		} else {
			Type *at = base_type(type_deref(ir_type(cs->backing_array)));
			Type *et = at->Array.elem;
			ir_write_byte(f, '{');
			ir_print_type(f, m, et);
			ir_write_str_lit(f, "* getelementptr inbounds (");
			ir_print_type(f, m, at);
			ir_write_str_lit(f, ", ");
			ir_print_type(f, m, at);
			ir_write_str_lit(f, "* ");
			ir_print_value(f, m, cs->backing_array, at);
			ir_write_str_lit(f, ", i32 0, i32 0), ");
			ir_print_type(f, m, t_int);
			ir_fprintf(f, " %lld}", cs->count);
		}
		break;
	}

	case irValue_Nil:
		ir_write_str_lit(f, "zeroinitializer");
		break;

	case irValue_Undef:
		ir_write_str_lit(f, "undef");
		break;

	case irValue_TypeName:
		ir_print_encoded_local(f, value->TypeName.name);
		break;
	case irValue_Global: {
		Entity *e = value->Global.entity;
		Scope *scope = e->scope;
		bool in_global_scope = false;
		if (scope != nullptr) {
			in_global_scope = (scope->flags & ScopeFlag_Global) != 0;
		}
		ir_print_encoded_global(f, ir_get_global_name(m, value), in_global_scope);
		break;
	}
	case irValue_Param:
		if (value->Param.index >= 0) {
			ir_fprintf(f, "%%_.%d", value->Param.index);
		} else {
			ir_print_encoded_local(f, value->Param.entity->token.string);
		}
		break;
	case irValue_SourceCodeLocation: {
		irValue *file      = value->SourceCodeLocation.file;
		irValue *line      = value->SourceCodeLocation.line;
		irValue *column    = value->SourceCodeLocation.column;
		irValue *procedure = value->SourceCodeLocation.procedure;

		ir_write_byte(f, '{');
		ir_print_type(f, m, t_string); ir_write_byte(f, ' '); ir_print_value(f, m, file, t_string);
		ir_write_string(f, str_lit(", "));
		ir_print_type(f, m, t_i32);    ir_write_byte(f, ' '); ir_print_value(f, m, line, t_i32);
		ir_write_string(f, str_lit(", "));
		ir_print_type(f, m, t_i32);    ir_write_byte(f, ' '); ir_print_value(f, m, column, t_i32);
		ir_write_string(f, str_lit(", "));
		ir_print_type(f, m, t_string); ir_write_byte(f, ' '); ir_print_value(f, m, procedure, t_string);
		ir_write_byte(f, '}');
		break;
	}
	case irValue_Proc:
		ir_print_encoded_global(f, value->Proc.name, ir_print_is_proc_global(m, &value->Proc));
		break;
	case irValue_Instr:
		ir_fprintf(f, "%%%d", value->index);
		break;
	}
}

void ir_print_calling_convention(irFileBuffer *f, irModule *m, ProcCallingConvention cc) {
	switch (cc) {
	case ProcCC_Odin:        ir_write_str_lit(f, "");       break;
	case ProcCC_Contextless: ir_write_str_lit(f, "");       break;
	// case ProcCC_CDecl:       ir_write_str_lit(f, "ccc ");   break;
	case ProcCC_CDecl:       ir_write_str_lit(f, "");   break;
	case ProcCC_StdCall:     ir_write_str_lit(f, "cc 64 "); break;
	case ProcCC_FastCall:    ir_write_str_lit(f, "cc 65 "); break;
	case ProcCC_None:        ir_write_str_lit(f, "");       break;
	default: GB_PANIC("unknown calling convention: %d", cc);
	}
}

void ir_print_context_parameter_prefix(irFileBuffer *f, irModule *m) {
	ir_print_type(f, m, t_context_ptr);
	// ir_write_str_lit(f, " noalias nonnull nocapture inreg ");
	ir_write_str_lit(f, " noalias nonnull nocapture ");
}

void ir_print_instr(irFileBuffer *f, irModule *m, irValue *value) {
	GB_ASSERT(value->kind == irValue_Instr);
	irInstr *instr = &value->Instr;

	ir_write_byte(f, '\t');
	defer (ir_write_byte(f, '\n'));

	switch (instr->kind) {
	default: {
		GB_PANIC("<unknown instr> %d\n", instr->kind);
		ir_fprintf(f, "; <unknown instr> %d\n", instr->kind);
		break;
	}

	case irInstr_StartupRuntime: {
		ir_write_str_lit(f, "call void ");
		ir_print_encoded_global(f, str_lit(IR_STARTUP_RUNTIME_PROC_NAME), false);
		ir_write_str_lit(f, "()");
		break;
	}

	case irInstr_Comment:
		ir_write_str_lit(f, "; ");
		ir_write_string(f, instr->Comment.text);
		break;

	case irInstr_Local: {
		Type *type = instr->Local.entity->type;
		i64 align = instr->Local.alignment;
		if (align <= 0) {
			align = type_align_of(type);
		}
		ir_fprintf(f, "%%%d = alloca ", value->index);
		ir_print_type(f, m, type);
		ir_fprintf(f, ", align %lld", align);
		break;
	}

	case irInstr_ZeroInit: {
		Type *type = type_deref(ir_type(instr->ZeroInit.address));
		ir_write_str_lit(f, "; ZeroInit\n\t");
		ir_write_str_lit(f, "store ");
		ir_print_type(f, m, type);
		ir_write_byte(f, ' ');
		ir_print_exact_value(f, m, empty_exact_value, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type);
		ir_fprintf(f, "* %%%d, align 1", instr->ZeroInit.address->index);
		// ir_fprintf(f, "* %%%d", instr->ZeroInit.address->index);
		break;
	}

	case irInstr_Store: {
		Type *type = type_deref(ir_type(instr->Store.address));
		ir_write_str_lit(f, "store ");
		if (instr->Store.is_volatile) {
			ir_write_str_lit(f, "volatile ");
		}
		ir_print_type(f, m, type);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->Store.value, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type);
		ir_write_str_lit(f, "* ");
		ir_print_value(f, m, instr->Store.address, type);
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_Load: {
		Type *type = instr->Load.type;
		ir_fprintf(f, "%%%d = load ", value->index);
		if (instr->Load.is_volatile) {
			ir_write_str_lit(f, "volatile ");
		}
		ir_print_type(f, m, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type);
		ir_write_str_lit(f, "* ");
		ir_print_value(f, m, instr->Load.address, type);
		if (instr->Load.custom_align > 0) {
			ir_fprintf(f, ", align %lld", instr->Load.custom_align);
		} else {
			ir_fprintf(f, ", align %lld", type_align_of(type));
		}
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_InlineCode:
		{
			switch (instr->InlineCode.id) {
			case BuiltinProc_alloca:
				ir_fprintf(f, "%%%d = ", value->index);
				ir_write_str_lit(f, "alloca i8, ");
				ir_print_type(f, m, ir_type(instr->InlineCode.operands[0]));
				ir_write_str_lit(f, " ");
				ir_print_value(f, m, instr->InlineCode.operands[0], ir_type(instr->InlineCode.operands[0]));
				ir_write_str_lit(f, ", align ");
				ir_print_value(f, m, instr->InlineCode.operands[1], t_i32);
				break;

			case BuiltinProc_cpu_relax:
				if (build_context.metrics.arch == TargetArch_amd64) {
					ir_write_str_lit(f, "call void asm sideeffect \"pause\", \"\"()");
				} else {
					// ir_write_str_lit(f, "call void asm sideeffect \"yield\", \"\"()");
				}
				break;
			default: GB_PANIC("Unknown inline code %d", instr->InlineCode.id); break;
			}
		}
		break;


	case irInstr_AtomicFence:
		ir_write_str_lit(f, "fence ");
		switch (instr->AtomicFence.id) {
		case BuiltinProc_atomic_fence:        ir_write_str_lit(f, "seq_cst"); break;
		case BuiltinProc_atomic_fence_acq:    ir_write_str_lit(f, "acquire"); break;
		case BuiltinProc_atomic_fence_rel:    ir_write_str_lit(f, "release"); break;
		case BuiltinProc_atomic_fence_acqrel: ir_write_str_lit(f, "acq_rel"); break;
		default: GB_PANIC("Unknown atomic fence"); break;
		}
		break;

	case irInstr_AtomicStore: {
		Type *type = type_deref(ir_type(instr->AtomicStore.address));
		ir_write_str_lit(f, "store atomic ");
		ir_print_type(f, m, type);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->AtomicStore.value, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type);
		ir_write_str_lit(f, "* ");
		ir_print_value(f, m, instr->AtomicStore.address, type);

		switch (instr->AtomicStore.id) {
		case BuiltinProc_atomic_store:           ir_write_str_lit(f, " seq_cst ");  break;
		case BuiltinProc_atomic_store_rel:       ir_write_str_lit(f, " release");   break;
		case BuiltinProc_atomic_store_relaxed:   ir_write_str_lit(f, " monotonic"); break;
		case BuiltinProc_atomic_store_unordered: ir_write_str_lit(f, " unordered"); break;
		default: GB_PANIC("Unknown atomic store"); break;
		}

		ir_fprintf(f, ", align %lld", type_align_of(type));

		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_AtomicLoad: {
		Type *type = instr->AtomicLoad.type;
		ir_fprintf(f, "%%%d = load atomic ", value->index);
		ir_print_type(f, m, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type);
		ir_write_str_lit(f, "* ");
		ir_print_value(f, m, instr->AtomicLoad.address, type);

		switch (instr->AtomicLoad.id) {
		case BuiltinProc_atomic_load:           ir_fprintf(f, " seq_cst");   break;
		case BuiltinProc_atomic_load_acq:       ir_fprintf(f, " acquire");   break;
		case BuiltinProc_atomic_load_relaxed:   ir_fprintf(f, " monotonic"); break;
		case BuiltinProc_atomic_load_unordered: ir_fprintf(f, " unordered"); break;
		default: GB_PANIC("Unknown atomic load"); break;
		}

		ir_fprintf(f, ", align %lld", type_align_of(type));
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_AtomicRmw: {
		String operation = {};
		String ordering = {};
		switch (instr->AtomicRmw.id) {
		case BuiltinProc_atomic_add:
		case BuiltinProc_atomic_add_acq:
		case BuiltinProc_atomic_add_rel:
		case BuiltinProc_atomic_add_acqrel:
		case BuiltinProc_atomic_add_relaxed:
			operation = str_lit("add");
			break;
		case BuiltinProc_atomic_sub:
		case BuiltinProc_atomic_sub_acq:
		case BuiltinProc_atomic_sub_rel:
		case BuiltinProc_atomic_sub_acqrel:
		case BuiltinProc_atomic_sub_relaxed:
			operation = str_lit("sub");
			break;
		case BuiltinProc_atomic_and:
		case BuiltinProc_atomic_and_acq:
		case BuiltinProc_atomic_and_rel:
		case BuiltinProc_atomic_and_acqrel:
		case BuiltinProc_atomic_and_relaxed:
			operation = str_lit("and");
			break;
		case BuiltinProc_atomic_nand:
		case BuiltinProc_atomic_nand_acq:
		case BuiltinProc_atomic_nand_rel:
		case BuiltinProc_atomic_nand_acqrel:
		case BuiltinProc_atomic_nand_relaxed:
			operation = str_lit("nand");
			break;
		case BuiltinProc_atomic_or:
		case BuiltinProc_atomic_or_acq:
		case BuiltinProc_atomic_or_rel:
		case BuiltinProc_atomic_or_acqrel:
		case BuiltinProc_atomic_or_relaxed:
			operation = str_lit("or");
			break;
		case BuiltinProc_atomic_xor:
		case BuiltinProc_atomic_xor_acq:
		case BuiltinProc_atomic_xor_rel:
		case BuiltinProc_atomic_xor_acqrel:
		case BuiltinProc_atomic_xor_relaxed:
			operation = str_lit("xor");
			break;
		case BuiltinProc_atomic_xchg:
		case BuiltinProc_atomic_xchg_acq:
		case BuiltinProc_atomic_xchg_rel:
		case BuiltinProc_atomic_xchg_acqrel:
		case BuiltinProc_atomic_xchg_relaxed:
			operation = str_lit("xchg");
			break;
		}

		switch (instr->AtomicRmw.id) {
		case BuiltinProc_atomic_add:
		case BuiltinProc_atomic_sub:
		case BuiltinProc_atomic_and:
		case BuiltinProc_atomic_nand:
		case BuiltinProc_atomic_or:
		case BuiltinProc_atomic_xor:
		case BuiltinProc_atomic_xchg:
			ordering = str_lit("seq_cst");
			break;
		case BuiltinProc_atomic_add_acq:
		case BuiltinProc_atomic_sub_acq:
		case BuiltinProc_atomic_and_acq:
		case BuiltinProc_atomic_nand_acq:
		case BuiltinProc_atomic_or_acq:
		case BuiltinProc_atomic_xor_acq:
		case BuiltinProc_atomic_xchg_acq:
			ordering = str_lit("acquire");
			break;
		case BuiltinProc_atomic_add_rel:
		case BuiltinProc_atomic_sub_rel:
		case BuiltinProc_atomic_and_rel:
		case BuiltinProc_atomic_nand_rel:
		case BuiltinProc_atomic_or_rel:
		case BuiltinProc_atomic_xor_rel:
		case BuiltinProc_atomic_xchg_rel:
			ordering = str_lit("release");
			break;
		case BuiltinProc_atomic_add_acqrel:
		case BuiltinProc_atomic_sub_acqrel:
		case BuiltinProc_atomic_and_acqrel:
		case BuiltinProc_atomic_nand_acqrel:
		case BuiltinProc_atomic_or_acqrel:
		case BuiltinProc_atomic_xor_acqrel:
		case BuiltinProc_atomic_xchg_acqrel:
			ordering = str_lit("acq_rel");
			break;
		case BuiltinProc_atomic_add_relaxed:
		case BuiltinProc_atomic_sub_relaxed:
		case BuiltinProc_atomic_and_relaxed:
		case BuiltinProc_atomic_nand_relaxed:
		case BuiltinProc_atomic_or_relaxed:
		case BuiltinProc_atomic_xor_relaxed:
		case BuiltinProc_atomic_xchg_relaxed:
			ordering = str_lit("monotonic");
			break;
		}

		Type *type = type_deref(ir_type(instr->AtomicRmw.address));
		ir_write_str_lit(f, "atomicrmw ");
		ir_write_string(f, operation);
		ir_write_byte(f, ' ');
		ir_print_type(f, m, type);
		ir_write_str_lit(f, "* ");
		ir_print_value(f, m, instr->AtomicRmw.address, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->AtomicRmw.value, type);

		ir_write_byte(f, ' ');
		ir_write_string(f, ordering);

		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_AtomicCxchg: {
		Type *type = type_deref(ir_type(instr->AtomicCxchg.address));
		bool weak = false;
		String success = {};
		String failure = {};

		switch (instr->AtomicCxchg.id) {
		case BuiltinProc_atomic_cxchgweak:
		case BuiltinProc_atomic_cxchgweak_acq:
		case BuiltinProc_atomic_cxchgweak_rel:
		case BuiltinProc_atomic_cxchgweak_acqrel:
		case BuiltinProc_atomic_cxchgweak_relaxed:
		case BuiltinProc_atomic_cxchgweak_failrelaxed:
		case BuiltinProc_atomic_cxchgweak_failacq:
		case BuiltinProc_atomic_cxchgweak_acq_failrelaxed:
		case BuiltinProc_atomic_cxchgweak_acqrel_failrelaxed:
			weak = true;
			break;
		}

		switch (instr->AtomicCxchg.id) {
		case BuiltinProc_atomic_cxchg:
		case BuiltinProc_atomic_cxchgweak:
			success = str_lit("seq_cst");
			failure = str_lit("seq_cst");
			break;
		case BuiltinProc_atomic_cxchg_acq:
		case BuiltinProc_atomic_cxchgweak_acq:
			success = str_lit("acquire");
			failure = str_lit("seq_cst");
			break;
		case BuiltinProc_atomic_cxchg_rel:
		case BuiltinProc_atomic_cxchgweak_rel:
			success = str_lit("release");
			failure = str_lit("seq_cst");
			break;
		case BuiltinProc_atomic_cxchg_acqrel:
		case BuiltinProc_atomic_cxchgweak_acqrel:
			success = str_lit("acq_rel");
			failure = str_lit("seq_cst");
			break;
		case BuiltinProc_atomic_cxchg_relaxed:
		case BuiltinProc_atomic_cxchgweak_relaxed:
			success = str_lit("monotonic");
			failure = str_lit("monotonic");
			break;
		case BuiltinProc_atomic_cxchg_failrelaxed:
		case BuiltinProc_atomic_cxchgweak_failrelaxed:
			success = str_lit("seq_cst");
			failure = str_lit("monotonic");
			break;
		case BuiltinProc_atomic_cxchg_failacq:
		case BuiltinProc_atomic_cxchgweak_failacq:
			success = str_lit("seq_cst");
			failure = str_lit("acquire");
			break;
		case BuiltinProc_atomic_cxchg_acq_failrelaxed:
		case BuiltinProc_atomic_cxchgweak_acq_failrelaxed:
			success = str_lit("acquire");
			failure = str_lit("monotonic");
			break;
		case BuiltinProc_atomic_cxchg_acqrel_failrelaxed:
		case BuiltinProc_atomic_cxchgweak_acqrel_failrelaxed:
			success = str_lit("acq_rel");
			failure = str_lit("monotonic");
			break;
		}

		ir_fprintf(f, "%%%d = cmpxchg ", value->index);
		if (weak) {
			ir_write_str_lit(f, "weak ");
		}
		ir_print_type(f, m, type);
		ir_write_str_lit(f, "* ");
		ir_print_value(f, m, instr->AtomicCxchg.address, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type); ir_write_str_lit(f, " ");
		ir_print_value(f, m, instr->AtomicCxchg.old_value, type);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, type); ir_write_str_lit(f, " ");
		ir_print_value(f, m, instr->AtomicCxchg.new_value, type);
		ir_write_str_lit(f, " ");
		ir_write_string(f, success);
		ir_write_str_lit(f, " ");
		ir_write_string(f, failure);
		break;
	}

	case irInstr_ArrayElementPtr: {
		Type *et = ir_type(instr->ArrayElementPtr.address);
		ir_fprintf(f, "%%%d = getelementptr inbounds ", value->index);

		ir_print_type(f, m, type_deref(et));
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, et);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->ArrayElementPtr.address, et);
		ir_write_str_lit(f, ", i32 0, ");

		irValue *index =instr->ArrayElementPtr.elem_index;
		Type *t = ir_type(index);
		ir_print_type(f, m, t);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, index, t);
		break;
	}

	case irInstr_StructElementPtr: {
		Type *et = ir_type(instr->StructElementPtr.address);
		ir_fprintf(f, "%%%d = getelementptr inbounds ", value->index);
		i32 index = instr->StructElementPtr.elem_index;
		Type *st = base_type(type_deref(et));
		if (is_type_struct(st)) {
			if (st->Struct.custom_align > 0) {
				index += 1;
			}
		} else if (is_type_union(st)) {
			index += 1;
		}

		ir_print_type(f, m, type_deref(et));
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, et);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->StructElementPtr.address, et);
		ir_write_str_lit(f, ", i32 0, ");
		ir_print_type(f, m, t_i32);
		ir_fprintf(f, " %d", index);
		break;
	}

	case irInstr_PtrOffset: {
		Type *pt = ir_type(instr->PtrOffset.address);
		ir_fprintf(f, "%%%d = getelementptr inbounds ", value->index);
		ir_print_type(f, m, type_deref(pt));
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, pt);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->PtrOffset.address, pt);

		irValue *offset = instr->PtrOffset.offset;
		Type *t = ir_type(offset);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, t);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, offset, t);
		break;
	}

	case irInstr_Phi: {
		ir_fprintf(f, "%%%d = phi ", value->index);
		ir_print_type(f, m, instr->Phi.type);
		// ir_fprintf(f, " ", value->index);
		ir_write_byte(f, ' ');

		for (isize i = 0; i < instr->Phi.edges.count; i++) {
			if (i > 0) {
				ir_write_string(f, str_lit(", "));
			}

			irValue *edge = instr->Phi.edges[i];
			irBlock *block = nullptr;
			if (instr->block != nullptr &&
			    i < instr->block->preds.count) {
				block = instr->block->preds[i];
			}

			ir_write_str_lit(f, "[ ");
			ir_print_value(f, m, edge, instr->Phi.type);
			ir_write_str_lit(f, ", %");
			ir_print_block_name(f, block);
			ir_write_str_lit(f, " ]");
		}
		break;
	}

	case irInstr_StructExtractValue: {
		Type *et = ir_type(instr->StructExtractValue.address);
		ir_fprintf(f, "%%%d = extractvalue ", value->index);
		i32 index = instr->StructExtractValue.index;
		Type *st = base_type(et);
		if (is_type_struct(st)) {
			if (st->Struct.custom_align > 0) {
				index += 1;
			}
		} else if (is_type_union(st)) {
			index += 1;
		}


		ir_print_type(f, m, et);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->StructExtractValue.address, et);
		ir_fprintf(f, ", %d", index);
		break;
	}

	case irInstr_UnionTagPtr: {
		Type *et = ir_type(instr->UnionTagPtr.address);

		Type *ut = type_deref(et);
		if (is_type_union_maybe_pointer(ut)) {
			GB_PANIC("union #maybe UnionTagPtr");
		}

		ir_fprintf(f, "%%%d = getelementptr inbounds ", value->index);
		Type *t = base_type(type_deref(et));
		GB_ASSERT(is_type_union(t));

		ir_print_type(f, m, type_deref(et));
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, et);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->UnionTagPtr.address, et);
		ir_write_str_lit(f, ", ");
		ir_print_type(f, m, t_int);
		ir_write_str_lit(f, " 0, ");
		ir_print_type(f, m, t_i32);
		ir_fprintf(f, " 2 ; UnionTagPtr");
		break;
	}

	case irInstr_UnionTagValue: {
		Type *et = ir_type(instr->UnionTagValue.address);
		Type *t = base_type(et);

		if (is_type_union_maybe_pointer(t)) {
			GB_PANIC("union #maybe UnionTagValue");
		}

		ir_fprintf(f, "%%%d = extractvalue ", value->index);
		GB_ASSERT(is_type_union(t));


		ir_print_type(f, m, et);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->UnionTagValue.address, et);
		ir_fprintf(f, ", 2 ; UnionTagValue");
		break;
	}

	case irInstr_Jump: {
		ir_write_str_lit(f, "br label %");
		ir_print_block_name(f, instr->Jump.block);
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_If: {
		ir_write_str_lit(f, "br i1");
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->If.cond, t_bool);
		ir_write_str_lit(f, ", ");
		ir_write_str_lit(f, "label %");   ir_print_block_name(f, instr->If.true_block);
		ir_write_str_lit(f, ", label %"); ir_print_block_name(f, instr->If.false_block);
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_Return: {
		irInstrReturn *ret = &instr->Return;
		ir_write_str_lit(f, "ret ");
		if (ret->value == nullptr) {
			ir_write_str_lit(f, "void");
		} else {
			Type *t = ir_type(ret->value);
			ir_print_type(f, m, t);
			ir_write_byte(f, ' ');
			ir_print_value(f, m, ret->value, t);
		}
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_Conv: {
		irInstrConv *c = &instr->Conv;
		if (c->kind == irConv_byteswap) {
			int sz = cast(int)(8*type_size_of(c->from));
			ir_fprintf(f, "%%%d = call i%d @llvm.bswap.i%d(", value->index, sz, sz);
			ir_print_type(f, m, c->from);
			ir_write_byte(f, ' ');
			ir_print_value(f, m, c->value, c->from);
			ir_write_byte(f, ')');
			ir_print_debug_location(f, m, value);
		} else {
			ir_fprintf(f, "%%%d = ", value->index);
			ir_write_string(f, ir_conv_strings[c->kind]);
			ir_write_byte(f, ' ');
			ir_print_type(f, m, c->from);
			ir_write_byte(f, ' ');
			ir_print_value(f, m, c->value, c->from);
			ir_write_str_lit(f, " to ");
			ir_print_type(f, m, c->to);
			ir_print_debug_location(f, m, value);
		}
		break;
	}

	case irInstr_Unreachable: {
		ir_write_str_lit(f, "unreachable");
		break;
	}

	case irInstr_UnaryOp: {
		irInstrUnaryOp *uo = &value->Instr.UnaryOp;
		Type *type =  base_type(ir_type(uo->expr));
		Type *elem_type = type;

		ir_fprintf(f, "%%%d = ", value->index);
		switch (uo->op) {
		case Token_Sub:
			if (is_type_float(elem_type)) {
				ir_write_str_lit(f, "fsub");
			} else {
				ir_write_str_lit(f, "sub");
			}
			break;
		case Token_Xor:
		case Token_Not:
			GB_ASSERT(is_type_integer(type) || is_type_boolean(type) || is_type_bit_set(type));
			ir_write_str_lit(f, "xor");
			break;
		default:
			GB_PANIC("Unknown unary operator");
			break;
		}

		ir_write_byte(f, ' ');
		ir_print_type(f, m, type);
		ir_write_byte(f, ' ');
		switch (uo->op) {
		case Token_Sub:
			if (is_type_float(elem_type)) {
				ir_print_exact_value(f, m, exact_value_float(0), elem_type);
			} else {
				ir_write_byte(f, '0');
			}
			break;
		case Token_Xor:
		case Token_Not:
			GB_ASSERT(is_type_integer(type) || is_type_boolean(type) || is_type_bit_set(type));
			ir_write_str_lit(f, "-1");
			break;
		}
		ir_write_str_lit(f, ", ");
		ir_print_value(f, m, uo->expr, type);
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_BinaryOp: {
		irInstrBinaryOp *bo = &value->Instr.BinaryOp;
		Type *type = base_type(ir_type(bo->left));
		Type *elem_type = base_array_type(type);

		ir_fprintf(f, "%%%d = ", value->index);

		if (gb_is_between(bo->op, Token__ComparisonBegin+1, Token__ComparisonEnd-1)) {
			if (is_type_string(elem_type)) {
				GB_PANIC("Unhandled string type");
			} else if (is_type_float(elem_type)) {
				ir_write_str_lit(f, "fcmp ");
				switch (bo->op) {
				case Token_CmpEq: ir_write_str_lit(f, "oeq"); break;
				case Token_NotEq: ir_write_str_lit(f, "one"); break;
				case Token_Lt:    ir_write_str_lit(f, "olt"); break;
				case Token_Gt:    ir_write_str_lit(f, "ogt"); break;
				case Token_LtEq:  ir_write_str_lit(f, "ole"); break;
				case Token_GtEq:  ir_write_str_lit(f, "oge"); break;
				}
			} else if (is_type_complex(elem_type)) {
				GB_PANIC("Unhandled complex type");
				return;
			} else {
				ir_write_str_lit(f, "icmp ");
				if (bo->op != Token_CmpEq &&
				    bo->op != Token_NotEq) {
					if (is_type_unsigned(elem_type)) {
						ir_write_str_lit(f, "u");
					} else {
						ir_write_str_lit(f, "s");
					}
				}
				switch (bo->op) {
				case Token_CmpEq: ir_write_str_lit(f, "eq"); break;
				case Token_NotEq: ir_write_str_lit(f, "ne"); break;
				case Token_Lt:    ir_write_str_lit(f, "lt"); break;
				case Token_Gt:    ir_write_str_lit(f, "gt"); break;
				case Token_LtEq:  ir_write_str_lit(f, "le"); break;
				case Token_GtEq:  ir_write_str_lit(f, "ge"); break;
				default: GB_PANIC("invalid comparison");break;
				}
			}
		} else {
			if (is_type_float(elem_type)) {
				ir_write_str_lit(f, "f");
			}

			switch (bo->op) {
			case Token_Add:    ir_write_str_lit(f, "add");  break;
			case Token_Sub:    ir_write_str_lit(f, "sub");  break;
			case Token_And:    ir_write_str_lit(f, "and");  break;
			case Token_Or:     ir_write_str_lit(f, "or");   break;
			case Token_Xor:    ir_write_str_lit(f, "xor");  break;
			case Token_Shl:    ir_write_str_lit(f, "shl");  break;
			case Token_Mul:    ir_write_str_lit(f, "mul");  break;
			case Token_Not:    ir_write_str_lit(f, "xor");  break;
			case Token_Shr:
				if (is_type_unsigned(elem_type)) {
					ir_write_str_lit(f, "lshr");
				} else {
					ir_write_str_lit(f, "ashr");
				}
				break;

			case Token_AndNot: GB_PANIC("Token_AndNot Should never be called");

			default: {
				if (!is_type_float(elem_type)) {
					if (is_type_unsigned(elem_type)) {
						ir_write_str_lit(f, "u");
					} else {
						ir_write_str_lit(f, "s");
					}
				}

				switch (bo->op) {
				case Token_Quo: ir_write_str_lit(f, "div"); break;
				case Token_Mod: ir_write_str_lit(f, "rem"); break;
				}
				break;
			}
			}
		}

		ir_write_byte(f, ' ');
		ir_print_type(f, m, type);
		ir_write_byte(f, ' ');
		ir_print_value(f, m, bo->left, type);
		ir_write_str_lit(f, ", ");
		ir_print_value(f, m, bo->right, type);

		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_Call: {
		irInstrCall *call = &instr->Call;
		Type *proc_type = base_type(ir_type(call->value));
		GB_ASSERT(is_type_proc(proc_type));
		set_procedure_abi_types(proc_type);

		bool is_c_vararg = proc_type->Proc.c_vararg;
		Type *result_type = call->type;
		if (result_type) {
			ir_fprintf(f, "%%%d = ", value->index);
		}
		ir_write_str_lit(f, "call ");
		ir_print_calling_convention(f, m, proc_type->Proc.calling_convention);
		if (is_c_vararg) {
			ir_print_proc_type_without_pointer(f, m, proc_type);
		} else if (result_type && !proc_type->Proc.return_by_pointer) {
			ir_print_proc_results(f, m, proc_type);
		} else {
			ir_write_str_lit(f, "void");
		}
		ir_write_byte(f, ' ');
		ir_print_value(f, m, call->value, call->type);

		bool return_by_pointer = proc_type->Proc.return_by_pointer;

		isize param_index = 0;

		ir_write_byte(f, '(');
		if (return_by_pointer) {
			GB_ASSERT(call->return_ptr != nullptr);
			ir_print_type(f, m, proc_type->Proc.results);
			ir_write_str_lit(f, "* ");
			ir_print_value(f, m, call->return_ptr, ir_type(call->return_ptr));
			param_index += 1;
		}


		if (call->args.count > 0) {
			TypeTuple *params = &proc_type->Proc.params->Tuple;
			if (proc_type->Proc.c_vararg) {
				isize i = 0;
				isize arg_index = 0;
				for (; i < params->variables.count-1; i++) {
					Entity *e = params->variables[i];
					GB_ASSERT(e != nullptr);
					if (e->kind != Entity_Variable) {
						arg_index++;
						continue;
					}

					if (param_index > 0) ir_write_str_lit(f, ", ");

					Type *t = proc_type->Proc.abi_compat_params[i];
					if (is_type_tuple(t)) {
						for_array(j, t->Tuple.variables) {
							if (j > 0) ir_write_str_lit(f, ", ");

							irValue *arg = call->args[arg_index++];

							ir_print_type(f, m, t->Tuple.variables[j]->type);
							if (e->flags&EntityFlag_NoAlias) {
								ir_write_str_lit(f, " noalias");
							}
							if (e->flags&EntityFlag_ByVal) {
								ir_write_str_lit(f, " byval");
							}
							ir_write_byte(f, ' ');
							ir_print_value(f, m, arg, t);
							param_index++;
						}
					} else {
						ir_print_type(f, m, t);
						if (e->flags&EntityFlag_NoAlias) {
							ir_write_str_lit(f, " noalias");
						}
						if (e->flags&EntityFlag_ImplicitReference) {
							ir_write_str_lit(f, " nonnull dereferenceable");
						}
						if (e->flags&EntityFlag_ByVal) {
							ir_write_str_lit(f, " byval");
						}
						ir_write_byte(f, ' ');
						irValue *arg = call->args[arg_index++];
						ir_print_value(f, m, arg, t);
						param_index++;
					}
				}
				while (arg_index < call->args.count) {
					if (param_index > 0) ir_write_str_lit(f, ", ");

					irValue *arg = call->args[arg_index];
					Type *t = ir_type(arg);
					ir_print_type(f, m, t);
					ir_write_byte(f, ' ');
					ir_print_value(f, m, arg, t);
					arg_index++;
				}
			} else {
				// GB_ASSERT(call->args.count == params->variables.count);
				isize arg_index = 0;
				for_array(i, params->variables) {
					Entity *e = params->variables[i];
					GB_ASSERT(e != nullptr);
					if (e->kind != Entity_Variable) {
						arg_index++;
						continue;
					}

					if (param_index > 0) ir_write_str_lit(f, ", ");

					Type *t = proc_type->Proc.abi_compat_params[i];
					if (is_type_tuple(t)) {
						for_array(j, t->Tuple.variables) {
							if (j > 0) ir_write_str_lit(f, ", ");

							irValue *arg = call->args[arg_index++];

							ir_print_type(f, m, t->Tuple.variables[j]->type);
							if (e->flags&EntityFlag_NoAlias) {
								ir_write_str_lit(f, " noalias");
							}
							if (e->flags&EntityFlag_ByVal) {
								ir_write_str_lit(f, " byval");
							}
							ir_write_byte(f, ' ');
							ir_print_value(f, m, arg, t);
							param_index++;
						}
					} else {
						irValue *arg = call->args[arg_index++];
						ir_print_type(f, m, t);
						if (e->flags&EntityFlag_NoAlias) {
							ir_write_str_lit(f, " noalias");
						}
						if (e->flags&EntityFlag_ByVal) {
							ir_write_str_lit(f, " byval");
						}
						ir_write_byte(f, ' ');
						ir_print_value(f, m, arg, t);
						param_index++;
					}
				}
			}
		}
		if (proc_type->Proc.calling_convention == ProcCC_Odin) {
			if (param_index > 0) ir_write_str_lit(f, ", ");

			ir_print_context_parameter_prefix(f, m);
			ir_print_value(f, m, call->context_ptr, t_context_ptr);
		}
		ir_write_str_lit(f, ")");

		if (proc_type->Proc.diverging) {
			ir_write_str_lit(f, " noreturn");
		}
		switch (call->inlining) {
		case ProcInlining_inline:    ir_write_str_lit(f, " alwaysinline"); break;
		case ProcInlining_no_inline: ir_write_str_lit(f, " noinline");     break;
		}
		ir_print_debug_location(f, m, value);

		break;
	}

	case irInstr_Select: {
		ir_fprintf(f, "%%%d = select i1 ", value->index);
		ir_print_value(f, m, instr->Select.cond, t_bool);
		ir_write_string(f, str_lit(", "));
		ir_print_type(f, m, ir_type(instr->Select.true_value));
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->Select.true_value, ir_type(instr->Select.true_value));
		ir_write_string(f, str_lit(", "));
		ir_print_type(f, m, ir_type(instr->Select.false_value));
		ir_write_byte(f, ' ');
		ir_print_value(f, m, instr->Select.false_value, ir_type(instr->Select.false_value));
		ir_print_debug_location(f, m, value);
		break;
	}

	case irInstr_DebugDeclare: {
		if (!m->generate_debug_info) {
			break;
		}

		irInstrDebugDeclare *dd = &instr->DebugDeclare;
		Type *vt = ir_type(dd->value);
		Entity *e = dd->entity;
		String name = e->token.string;
		TokenPos pos = e->token.pos;

		irDebugInfo **lookup_di = map_get(&m->debug_info, hash_entity(e));
		if (lookup_di != nullptr) {
			GB_ASSERT_NOT_NULL(*lookup_di);
			irDebugInfo* local_var_di = *lookup_di;

			ir_write_str_lit(f, "call void @llvm.dbg.declare(");
			ir_write_str_lit(f, "metadata ");
			ir_print_type(f, m, vt);
			ir_write_byte(f, ' ');
			ir_print_value(f, m, dd->value, vt);
			ir_fprintf(f, ", metadata !%d", local_var_di->id);
			ir_write_str_lit(f, ", metadata !DIExpression())");
			ir_print_debug_location(f, m, value);
		}
		break;
	}
	}
}


void ir_print_proc(irFileBuffer *f, irModule *m, irProcedure *proc) {
	set_procedure_abi_types(proc->type);

	if (proc->body == nullptr) {
		ir_write_str_lit(f, "declare ");
		// if (proc->tags & ProcTag_dll_import) {
			// ir_write_string(f, "dllimport ");
		// }
	} else {
		ir_write_byte(f, '\n');
		ir_write_str_lit(f, "define ");
		if (proc->is_export) {
			ir_write_str_lit(f, "dllexport ");
		}
		// if (!proc->is_export && !proc->is_foreign && !proc->is_entry_point) {
			// ir_write_string(f, "internal ");
		// }
	}

	TypeProc *proc_type = &proc->type->Proc;

	ir_print_calling_convention(f, m, proc_type->calling_convention);

	isize param_count = proc_type->param_count;
	isize result_count = proc_type->result_count;
	ir_print_proc_results(f, m, proc->type);
	ir_write_byte(f, ' ');

// #ifndef GB_SYSTEM_WINDOWS
#if 0
	if(uses_args)
		ir_write_string(f, "@.nix_argpatch_main");
	else
#endif
	ir_print_encoded_global(f, proc->name, ir_print_is_proc_global(m, proc));


	isize param_index = 0;

	ir_write_byte(f, '(');
	if (proc_type->return_by_pointer) {
		ir_print_type(f, m, reduce_tuple_to_single_type(proc_type->results));
		ir_write_str_lit(f, "* sret noalias ");
		if (build_context.ODIN_OS == "darwin") {
			ir_fprintf(f, "byval ");
		}
		ir_write_str_lit(f, "%agg.result");
		param_index += 1;
	}

	if (param_count > 0) {
		TypeTuple *params = &proc_type->params->Tuple;
		isize parameter_index = 0;
		for (isize i = 0; i < param_count; i++, parameter_index++) {
			Entity *e = params->variables[i];
			Type *original_type = e->type;
			Type *abi_type = proc_type->abi_compat_params[i];
			if (e->kind != Entity_Variable) continue;
			if (param_index > 0) ir_write_string(f, str_lit(", "));

			if (i+1 == params->variables.count && proc_type->c_vararg) {
				ir_write_str_lit(f, " ...");
			} else {
				if (is_type_tuple(abi_type)) {
					for_array(j, abi_type->Tuple.variables) {
						if (j > 0) ir_write_string(f, str_lit(", "));

						Type *tft = abi_type->Tuple.variables[j]->type;
						ir_print_type(f, m, tft);
						if (e->flags&EntityFlag_NoAlias) {
							ir_write_str_lit(f, " noalias");
						}
						if (e->flags&EntityFlag_ByVal) {
							ir_write_str_lit(f, " byval");
						}


						if (proc->body != nullptr) {
							ir_fprintf(f, " %%_.%td", parameter_index+j);
						}
					}
					parameter_index += abi_type->Tuple.variables.count-1;
					param_index += abi_type->Tuple.variables.count-1;
				} else {
					ir_print_type(f, m, abi_type);
					if (e->flags&EntityFlag_NoAlias) {
						ir_write_str_lit(f, " noalias");
					}
					if (e->flags&EntityFlag_ByVal) {
						ir_write_str_lit(f, " byval");
					}
					if (proc->body != nullptr) {
						ir_fprintf(f, " %%_.%td", parameter_index);
					}
				}
			}

			param_index++;
		}
	}
	if (proc_type->calling_convention == ProcCC_Odin) {
		if (param_index > 0) ir_write_str_lit(f, ", ");

		ir_print_context_parameter_prefix(f, m);
		ir_write_str_lit(f, "%__.context_ptr");
	}

	ir_write_str_lit(f, ") ");

	switch (proc->inlining) {
	default:
		ir_fprintf(f, "#0 ");
		break;
	case ProcInlining_inline:
		ir_write_str_lit(f, "alwaysinline ");
		ir_fprintf(f, "#1 ");
		break;
	case ProcInlining_no_inline:
		ir_write_str_lit(f, "noinline ");
		ir_fprintf(f, "#2 ");
		break;
	}

	if (proc_type->diverging) {
		ir_write_str_lit(f, "noreturn ");
	}

	if (m->generate_debug_info && proc->entity != nullptr && proc->body != nullptr) {
		irDebugInfo **di_ = map_get(&proc->module->debug_info, hash_pointer(proc->entity));
		if (di_ != nullptr) {
			irDebugInfo *di = *di_;
			GB_ASSERT(di->kind == irDebugInfo_Proc);
			ir_fprintf(f, "!dbg !%d ", di->id); // TODO(lachsinc): !dbg
		}
	}



	if (proc->body != nullptr) {
		// ir_fprintf(f, "nounwind uwtable {\n");

		ir_write_str_lit(f, "{\n");
		for_array(i, proc->blocks) {
			irBlock *block = proc->blocks[i];

			if (i > 0) ir_write_byte(f, '\n');
			ir_print_block_name(f, block);
			ir_write_str_lit(f, ":\n");

			for_array(j, block->instrs) {
				irValue *value = block->instrs[j];
				ir_print_instr(f, m, value);
			}
		}
		ir_write_str_lit(f, "}\n");
	} else {
		ir_write_byte(f, '\n');
	}

	for_array(i, proc->children) {
		ir_print_proc(f, m, proc->children[i]);
	}
}


void ir_print_type_name(irFileBuffer *f, irModule *m, irValue *v) {
	GB_ASSERT(v->kind == irValue_TypeName);
	Type *t = base_type(v->TypeName.type);

	ir_print_encoded_local(f, v->TypeName.name);
	ir_write_string(f, str_lit(" = type "));


	switch (t->kind) {
	case Type_Union:
		if (t->Union.variants.count == 0) {
			ir_write_string(f, str_lit("{}"));
		} else {
			ir_print_type(f, m, t);
		}
		break;
	case Type_Struct:
		if (t->Struct.fields.count == 0) {
			if (t->Struct.is_packed) {
				ir_write_byte(f, '<');
			}
			ir_write_byte(f, '{');
			if (t->Struct.custom_align > 0) {
				ir_print_alignment_prefix_hack(f, t->Struct.custom_align);
			}
			ir_write_byte(f, '}');
			if (t->Struct.is_packed) {
				ir_write_byte(f, '>');
			}
		} else {
			ir_print_type(f, m, t);
		}
		break;
	default:
		ir_print_type(f, m, t);
		break;
	}

	ir_write_byte(f, '\n');
}

bool ir_print_global_type_allowed(Type *t) {
	if (t == nullptr) {
		return true;
	}
	t = core_type(t);
	switch (t->kind) {
	case Type_DynamicArray:
	case Type_Map:
	case Type_Union:
		return false;
	}

	return true;
}

void print_llvm_ir(irGen *ir) {
	irModule *m = &ir->module;

	irFileBuffer buf = {}, *f = &buf;
	ir_file_buffer_init(f, &ir->output_file);
	defer (ir_file_buffer_destroy(f));

	i32 word_bits = cast(i32)(8*build_context.word_size);
	if (build_context.ODIN_OS == "darwin") {
		GB_ASSERT(word_bits == 64);
		ir_write_str_lit(f, "target datalayout = \"e-m:o-i64:64-f80:128-n8:16:32:64-S128\"\n");
		ir_write_str_lit(f, "target triple = \"x86_64-apple-macosx10.8\"\n");
		ir_write_str_lit(f, "\n");
	} else if (build_context.ODIN_OS == "windows") {
		ir_fprintf(f, "target triple = \"x86%s-pc-windows-msvc\"\n\n", word_bits == 64 ? "_64" : "");
		if (word_bits == 64 && build_context.metrics.arch == TargetArch_amd64) {
			ir_fprintf(f, "target datalayout = \"e-m:w-i64:64-f80:128-n8:16:32:64-S128\"\n\n");
		}
	}

	ir_print_encoded_local(f, str_lit("..opaque"));
	ir_write_str_lit(f, " = type {};\n");
	ir_print_encoded_local(f, str_lit("..string"));
	ir_write_str_lit(f, " = type {i8*, ");
	ir_print_type(f, m, t_int);
	ir_write_str_lit(f, "} ; Basic_string\n");
	ir_print_encoded_local(f, str_lit("..rawptr"));
	ir_write_str_lit(f, " = type i8* ; Basic_rawptr\n");

	ir_print_encoded_local(f, str_lit("..complex32"));
	ir_write_str_lit(f, " = type {half, half} ; Basic_complex32\n");
	ir_print_encoded_local(f, str_lit("..complex64"));
	ir_write_str_lit(f, " = type {float, float} ; Basic_complex64\n");
	ir_print_encoded_local(f, str_lit("..complex128"));
	ir_write_str_lit(f, " = type {double, double} ; Basic_complex128\n");

	ir_print_encoded_local(f, str_lit("..quaternion64"));
	ir_write_str_lit(f, " = type {half, half, half, half} ; Basic_quaternion64\n");
	ir_print_encoded_local(f, str_lit("..quaternion128"));
	ir_write_str_lit(f, " = type {float, float, float, float} ; Basic_quaternion128\n");
	ir_print_encoded_local(f, str_lit("..quaternion256"));
	ir_write_str_lit(f, " = type {double, double, double, double} ; Basic_quaternion256\n");

	ir_print_encoded_local(f, str_lit("..typeid"));
	ir_write_str_lit(f, " = type ");
	ir_print_type(f, m, t_uintptr);
	ir_write_str_lit(f, " ; Basic_typeid\n");

	ir_print_encoded_local(f, str_lit("..any"));
	ir_write_str_lit(f, " = type {");
	ir_print_type(f, m, t_rawptr);
	ir_write_str_lit(f, ", ");
	ir_print_type(f, m, t_typeid);
	ir_write_str_lit(f, "} ; Basic_any\n");

	ir_write_str_lit(f, "declare void @llvm.dbg.declare(metadata, metadata, metadata) #3 \n");

	if (string_map_get(&m->members, str_lit("llvm.bswap.i16")) == nullptr) {
		ir_write_str_lit(f, "declare i16 @llvm.bswap.i16(i16) \n");
	}
	if (string_map_get(&m->members, str_lit("llvm.bswap.i32")) == nullptr) {
		ir_write_str_lit(f, "declare i32 @llvm.bswap.i32(i32) \n");
	}
	if (string_map_get(&m->members, str_lit("llvm.bswap.i64")) == nullptr) {
		ir_write_str_lit(f, "declare i64 @llvm.bswap.i64(i64) \n");
	}
	if (string_map_get(&m->members, str_lit("llvm.bswap.i128")) == nullptr) {
		ir_write_str_lit(f, "declare i128 @llvm.bswap.i128(i128) \n");
	}
	ir_write_byte(f, '\n');


	for_array(member_index, m->members.entries) {
		auto *entry = &m->members.entries[member_index];
		irValue *v = entry->value;
		if (v->kind != irValue_TypeName) {
			continue;
		}
		ir_print_type_name(f, m, v);
	}

	ir_write_byte(f, '\n');

	bool dll_main_found = false;

	// NOTE(bill): Print foreign prototypes first
	for_array(member_index, m->members.entries) {
		auto *entry = &m->members.entries[member_index];
		irValue *v = entry->value;
		if (v->kind != irValue_Proc) {
			continue;
		}

		if (v->Proc.body == nullptr) {
			ir_print_proc(f, m, &v->Proc);
		}
	}

	if (ir->print_chkstk) {
		// TODO(bill): Clean up this code
		ir_write_str_lit(f, "\n\n");
		ir_write_str_lit(f, "define void @__chkstk() #0 {\n");
		ir_write_str_lit(f, "\tcall void asm sideeffect \"push   %rcx \\09\\0Apush   %rax \\09\\0Acmp    $$0x1000,%rax \\09\\0Alea    24(%rsp),%rcx \\09\\0Ajb     1f \\09\\0A2: \\09\\0Asub    $$0x1000,%rcx \\09\\0Aorl    $$0,(%rcx) \\09\\0Asub    $$0x1000,%rax \\09\\0Acmp    $$0x1000,%rax \\09\\0Aja     2b \\09\\0A1: \\09\\0Asub    %rax,%rcx \\09\\0Aorl    $$0,(%rcx) \\09\\0Apop    %rax \\09\\0Apop    %rcx \\09\\0Aret \\09\\0A\", \"~{dirflag},~{fpsr},~{flags}\"()\n");
		ir_write_str_lit(f, "\tret void\n");
		ir_write_str_lit(f, "}\n\n");
	}

	// NOTE(bill): Print procedures with bodies next
	for_array(member_index, m->members.entries) {
		auto *entry = &m->members.entries[member_index];
		irValue *v = entry->value;
		if (v->kind != irValue_Proc) {
			continue;
		}

		if (v->Proc.body != nullptr) {
			ir_print_proc(f, m, &v->Proc);
		}
	}

	for_array(member_index, m->members.entries) {
		auto *entry = &m->members.entries[member_index];
		irValue *v = entry->value;
		if (v->kind != irValue_Global) {
			continue;
		}
		irValueGlobal *g = &v->Global;
		Scope *scope = g->entity->scope;
		bool in_global_scope = false;
		if (scope != nullptr) {
			// TODO(bill): Fix this rule. What should it be?
			in_global_scope = (scope->flags & ScopeFlag_Global) != 0;
		}

		ir_print_encoded_global(f, ir_get_global_name(m, v), in_global_scope);
		ir_write_string(f, str_lit(" = "));
		if (g->is_foreign) {
			ir_write_string(f, str_lit("external "));
		}
		if (g->is_export) {
			ir_write_string(f, str_lit("dllexport "));
		}

		if (g->is_private) {
			ir_write_string(f, str_lit("private "));
		} else if (g->is_internal) {
			ir_write_string(f, str_lit("internal "));
		}
		if (g->thread_local_model.len > 0) {
			String model = g->thread_local_model;
			if (model == "default") {
				ir_write_string(f, str_lit("thread_local "));
			} else {
				ir_fprintf(f, "thread_local(%.*s) ", LIT(model));

			}
		}
		if (g->is_constant) {
			if (g->is_unnamed_addr) {
				ir_write_string(f, str_lit("unnamed_addr "));
			}
			ir_write_string(f, str_lit("constant "));
		} else {
			ir_write_string(f, str_lit("global "));
		}


		ir_print_type(f, m, g->entity->type);
		ir_write_byte(f, ' ');
		if (!g->is_foreign) {
			if (g->value != nullptr && ir_print_global_type_allowed(g->entity->type)) {
				ir_print_value(f, m, g->value, g->entity->type);
			} else {
				ir_write_string(f, str_lit("zeroinitializer"));
			}
			if (m->generate_debug_info) {
				irDebugInfo **di_lookup = map_get(&m->debug_info, hash_entity(g->entity));
				if (di_lookup != nullptr) {
					irDebugInfo *di = *di_lookup;
					GB_ASSERT(di);
					GB_ASSERT(di->kind == irDebugInfo_GlobalVariableExpression);
					ir_fprintf(f, ", !dbg !%d", di->id);
				}
			}
		}
		ir_write_byte(f, '\n');
	}

	// TODO(lachsinc): Attribute map inside ir module?
	ir_fprintf(f, "attributes #0 = {nounwind uwtable}\n");
	ir_fprintf(f, "attributes #1 = {nounwind alwaysinline uwtable}\n");
	ir_fprintf(f, "attributes #2 = {nounwind noinline optnone uwtable}\n");
	ir_fprintf(f, "attributes #3 = {nounwind readnone}\n");

	if (m->generate_debug_info) {
		ir_write_byte(f, '\n');

		i32 diec = cast(i32)m->debug_info.entries.count;

		i32 di_version    = diec+1;
		i32 di_debug_info = diec+2;
		i32 di_code_view  = diec+3;
		i32 di_wchar_size = diec+4;



		ir_fprintf(f, "!llvm.dbg.cu = !{!%d}\n", m->debug_compile_unit->id);
		ir_fprintf(f, "!llvm.ident = !{!%d}\n", di_version);
		ir_fprintf(f, "!llvm.module.flags = !{!%d, !%d, !%d}\n", di_debug_info, di_code_view, di_wchar_size);

		ir_fprintf(f, "!0 = !{}\n");

		for_array(di_index, m->debug_info.entries) {
			irDebugInfo *di = m->debug_info.entries[di_index].value;
			GB_ASSERT_MSG(di != nullptr, "Invalid irDebugInfo");
			ir_fprintf(f, "!%d = ", di->id);
			switch (di->kind) {
			case irDebugInfo_CompileUnit: {
				irDebugInfo **found = map_get(&m->debug_info, hash_pointer(di->CompileUnit.file));
				GB_ASSERT_MSG(found != nullptr, "Missing debug info for: %.*s\n", LIT(di->CompileUnit.file->fullpath));
				irDebugInfo *file = *found;
				ir_fprintf(f,
				            "distinct !DICompileUnit("
				              "language: DW_LANG_C_plus_plus" // Is this good enough?
				            ", file: !%d"
				            ", producer: \"Odin %.*s\""
				            ", runtimeVersion: 0"
				            ", isOptimized: false"
				            ", emissionKind: FullDebug"
				            ", retainedTypes: !0" // TODO(lachsinc)
				            ", enums: !%d"
				            ", globals: !%d"
				            ")",
				            file->id,
				            LIT(build_context.ODIN_VERSION),
				            m->debug_compile_unit->CompileUnit.enums->id,
				            m->debug_compile_unit->CompileUnit.globals->id);
				break;
			}
			case irDebugInfo_File:
				ir_fprintf(f, "!DIFile(filename: \""); ir_print_escape_path(f, di->File.filename);
				ir_fprintf(f, "\", directory: \""); ir_print_escape_path(f, di->File.directory);
				ir_fprintf(f, "\"");
				ir_fprintf(f, ")");
				break;
			case irDebugInfo_Proc:
				// TODO(lachsinc): We need to store scope info inside di, not just file info, for procs.
				// Should all subprograms have distinct ??
				ir_fprintf(f, "distinct !DISubprogram("
				              "name: \"%.*s\""
				            ", linkageName: \"%.*s\""
				            ", scope: !%d"
				            ", file: !%d"
				            ", line: %d"
				            ", scopeLine: %d"
				            ", isDefinition: true"
				            ", isLocal: false" // TODO(lachsinc): Is this fine?
				            ", flags: DIFlagPrototyped"
				            ", isOptimized: false"
				            ", unit: !%d"
				            ", type: !%d",
				            LIT(di->Proc.entity->token.string),
				            LIT(di->Proc.name),
				            di->Proc.file->id, // TODO(lachsinc): HACK For now lets pretend all procs scope's == file.
				            di->Proc.file->id,
				            di->Proc.pos.line,
				            di->Proc.pos.line, // NOTE(lachsinc): Assume scopeLine always same as line.
				            m->debug_compile_unit->id,
							di->Proc.type->id);
				ir_write_byte(f, ')'); // !DISubprogram(
				break;
			case irDebugInfo_ProcType:
				ir_fprintf(f, "!DISubroutineType(types: !%d)",
				            di->ProcType.types->id);
				break;
			case irDebugInfo_Location:
				GB_ASSERT_NOT_NULL(di->Location.scope);
				ir_fprintf(f, "!DILocation("
				              "line: %d"
				            ", column: %d"
				            ", scope: !%d)",
				            di->Location.pos.line,
				            di->Location.pos.column,
				            di->Location.scope->id);
				break;
			case irDebugInfo_LexicalBlock:
				GB_ASSERT_NOT_NULL(di->LexicalBlock.file);
				GB_ASSERT_NOT_NULL(di->LexicalBlock.scope);
				ir_fprintf(f, "distinct !DILexicalBlock("
				              "line: %d"
				            ", column: %d"
				            ", file: !%d"
				            ", scope: !%d)",
				            di->LexicalBlock.pos.line,
				            di->LexicalBlock.pos.column,
				            di->LexicalBlock.file->id,
				            di->LexicalBlock.scope->id);
				break;
			case irDebugInfo_GlobalVariableExpression: {
				ir_fprintf(f, "!DIGlobalVariableExpression("
				              "var: !%d"
				            ", expr: !DIExpression(",
				           di->GlobalVariableExpression.var->id);
				if (di->GlobalVariableExpression.var->GlobalVariable.variable->Global.is_constant) {
					ir_write_str_lit(f, "DW_OP_constu, ");
					ir_print_value(f, m, di->GlobalVariable.variable, ir_type(di->GlobalVariable.variable));
					ir_write_str_lit(f, ", DW_OP_stack_value");
				} else {
					// NOTE(lachsinc): non-const globals expect empty "!DIExpression()"
				}
				ir_write_byte(f, ')'); // !DIExpression(
				ir_write_byte(f, ')'); // !DIGlobalVariableExpression(
				break;
			}
			case irDebugInfo_GlobalVariable: {
				ir_fprintf(f, "distinct !DIGlobalVariable("
				              "name: \"%.*s\""
				            ", scope: !%d"
				            ", file: !%d"
				            ", line: %d"
				            ", type: !%d"
				            ", isLocal: true"        // TODO(lachsinc): Check locality ??
				            ", isDefinition: true)", // TODO(lachsinc): ??
				            LIT(di->GlobalVariable.name),
				            di->GlobalVariable.scope->id,
				            di->GlobalVariable.file->id,
				            di->GlobalVariable.pos.line,
				            di->GlobalVariable.type->id);
				break;
			}
			case irDebugInfo_LocalVariable: {
				ir_fprintf(f, "!DILocalVariable("
				              "scope: !%d"
				            ", file: !%d"
				            ", line: %d"
				            ", type: !%d",
				            di->LocalVariable.scope->id,
				            di->LocalVariable.file->id,
				            di->LocalVariable.pos.line,
				            di->LocalVariable.type->id);
				if (di->DerivedType.name.len > 0) {
					ir_fprintf(f, ", name: \"%.*s\"", LIT(di->LocalVariable.name));
				}
				if (di->LocalVariable.arg > 0) {
					ir_fprintf(f, ", arg: %d", di->LocalVariable.arg);
				}
				ir_write_byte(f, ')');
				break;
			}
			case irDebugInfo_BasicType:
				ir_fprintf(f, "!DIBasicType("
				              "name: \"%.*s\""
				            ", size: %d"
				            ", encoding: ",
				            LIT(di->BasicType.name),
				            di->BasicType.size);
				ir_print_debug_encoding(f, irDebugInfo_BasicType, di->BasicType.encoding);
				ir_write_byte(f, ')');
				break;
			case irDebugInfo_DerivedType: {
				if (di->DerivedType.tag == irDebugBasicEncoding_member) {
					// NOTE(lachsinc): We crash llvm super hard if we don't specify a name :)
					Type *t = di->DerivedType.type;
					GB_ASSERT_MSG(di->DerivedType.name.len > 0, "%s", type_to_string(di->DerivedType.type));
				}
				ir_write_str_lit(f, "!DIDerivedType(tag: ");
				ir_print_debug_encoding(f, irDebugInfo_DerivedType, di->DerivedType.tag);
				if (di->DerivedType.name.len > 0) {
					ir_fprintf(f, ", name: \"%.*s\"", LIT(di->DerivedType.name));
				}
				if (di->DerivedType.base_type != nullptr) {
					ir_fprintf(f, ", baseType: !%d", di->DerivedType.base_type->id);
				} else {
					ir_write_str_lit(f, ", baseType: null"); // Valid/required for rawptr
				}
				if (di->DerivedType.size > 0)   ir_fprintf(f, ", size: %d", di->DerivedType.size);
				if (di->DerivedType.align > 0)  ir_fprintf(f, ", align: %d", di->DerivedType.align);
				if (di->DerivedType.offset > 0) ir_fprintf(f, ", offset: %d", di->DerivedType.offset);
				if (di->DerivedType.flags > 0) {
					// TODO(lachsinc): Handle in a more generic manner.
					if (di->DerivedType.flags & irDebugInfoFlag_Bitfield) ir_write_str_lit(f, ", flags: DIFlagBitField, extraData: i64 0");
				}
				ir_write_byte(f, ')');
				break;
			}
			case irDebugInfo_CompositeType: {
				if (di->CompositeType.tag == irDebugBasicEncoding_array_type) {
					GB_ASSERT_NOT_NULL(di->CompositeType.base_type);
					GB_ASSERT(di->CompositeType.array_count >= 0);
					GB_ASSERT(di->CompositeType.name.len == 0);
					GB_ASSERT(di->CompositeType.size >= 0);
				}

				if (di->CompositeType.tag == irDebugBasicEncoding_union_type) {
					GB_ASSERT_NOT_NULL(di->CompositeType.file); // Union _requires_ file to be valid.
				}

				ir_write_str_lit(f, "!DICompositeType(tag: ");
				ir_print_debug_encoding(f, irDebugInfo_CompositeType, di->CompositeType.tag);
				if (di->CompositeType.name.len > 0) {
					ir_fprintf(f, ", name: \"%.*s\"", LIT(di->CompositeType.name));
				}
				if (di->CompositeType.scope != nullptr) {
					ir_fprintf(f, ", scope: !%d", di->CompositeType.scope->id);
				}
				if (di->CompositeType.file != nullptr) {
					ir_fprintf(f, ", file: !%d"
					              ", line: %d",
					              di->CompositeType.file->id,
					              di->CompositeType.pos.line);
				}
				if (di->CompositeType.size > 0)  ir_fprintf(f, ", size: %d", di->CompositeType.size);
				if (di->CompositeType.align > 0) ir_fprintf(f, ", align: %d", di->CompositeType.align);
				if (di->CompositeType.base_type != nullptr) {
					GB_ASSERT(di->CompositeType.tag != irDebugBasicEncoding_structure_type);
					GB_ASSERT(di->CompositeType.tag != irDebugBasicEncoding_union_type);
					ir_fprintf(f, ", baseType: !%d", di->CompositeType.base_type->id);
				}
				if (di->CompositeType.tag == irDebugBasicEncoding_array_type) {
					ir_fprintf(f, ", elements: !{!DISubrange(count: %d)}", di->CompositeType.array_count);
				} else {
					if (di->CompositeType.elements != nullptr) {
						ir_fprintf(f, ", elements: !%d", di->CompositeType.elements->id);
					}
				}
				ir_write_byte(f, ')');
				break;
			}
			case irDebugInfo_Enumerator: {
				ir_fprintf(f, "!DIEnumerator("
				              "name: \"%.*s\""
				            ", value: %lld)",
				            LIT(di->Enumerator.name),
				            di->Enumerator.value);
				break;
			}
			case irDebugInfo_DebugInfoArray:
				ir_fprintf(f, "!{");
				for_array(element_index, di->DebugInfoArray.elements) {
					irDebugInfo *elem = di->DebugInfoArray.elements[element_index];
					if (element_index > 0) ir_write_str_lit(f, ", ");
					if (elem != nullptr) {
						ir_fprintf(f, "!%d", elem->id);
					} else {
						ir_fprintf(f, "null"); // NOTE(lachsinc): Proc's can contain "nullptr" entries to represent void return values.
					}
				}
				ir_write_byte(f, '}');
				break;

			default:
				GB_PANIC("Unhandled irDebugInfo kind %d", di->kind);
				break;
			}

			ir_write_byte(f, '\n');
		}


		ir_fprintf(f, "!%d = !{!\"Odin version %.*s \"}\n", di_version, LIT(build_context.ODIN_VERSION));
		ir_fprintf(f, "!%d = !{i32 2, !\"Debug Info Version\", i32 3}\n", di_debug_info);
		ir_fprintf(f, "!%d = !{i32 2, !\"CodeView\", i32 1}\n",           di_code_view);
		ir_fprintf(f, "!%d = !{i32 1, !\"wchar_size\", i32 2}\n",         di_wchar_size);
	}
}
