llm
Download
1294 lines 66 KiBCModified 2026-08-31 raw
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294
// This file defines how to compile loops

#include <gmp.h>

#include "../ast.h"
#include "../config.h"
#include "../environment.h"
#include "../stdlib/datatypes.h"
#include "../stdlib/integers.h"
#include "../stdlib/table.h"
#include "../stdlib/text.h"
#include "../typecheck.h"
#include "../util.h"
#include "compilation.h"

// For loops with an optional `i` index variable (`for i, x in ...`), the index
// is an Int64 counting 1, 2, 3, ..., an independent counter declared before
// the loop and stepped once per yielded element:
static Text_t index_counter_decl(Text_t index) {
    return index.length > 0 ? Texts("Int64_t ", index, "$counter = 0;\n") : EMPTY_TEXT;
}

static Text_t index_counter_step(Text_t index) {
    return index.length > 0 ? Texts("Int64_t ", index, " = ++", index, "$counter;\n") : EMPTY_TEXT;
}

// ---------------------------------------------------------------------------
// Copy-on-write guard hoisting.
//
// Every indexed list write normally pays a per-write CoW guard
// (`if (data_refcount > 0) List$compact(...)` inside List_lvalue). Even when
// the branch is never taken, the *returning* List$compact call in a hot loop
// pins registers and blocks vectorization. When we can prove the guard can't
// fire mid-loop, we hoist it: compact once (if shared) before the loop, then
// compile the loop's list accesses against hoisted data/stride/length locals
// (List_get_hoisted / List_lvalue_hoisted / List_swap_hoisted, with bounds
// checks kept and only the CoW guard and the header re-reads are removed).
//
// Soundness: data_refcount is not a general aliasing property, since writes
// through aliased pointers never bump it. It changes only at specific
// compiler-emitted operations: taking a value snapshot (copying/dereferencing
// a list value, slicing), starting a list iteration (which snapshots), and
// resizing. Snapshots taken *before* the loop are discharged by the one
// up-front compact. Mid-loop, only the loop body itself runs, so it suffices
// that the body provably contains none of those operations. We establish that
// with a strict whitelist scan: the body may contain only scalar-typed
// declarations/assignments/updates, reads and writes of list elements whose
// items are scalar-typed, arithmetic/comparison operators, if/continue/break/pass,
// and nested loops over integer ranges. Any function call, method call,
// lambda, return (deferred blocks could run arbitrary code), list-typed
// value, iteration over a container, comprehension, or anything else not
// explicitly whitelisted makes the loop ineligible and it compiles exactly as
// before. Shadowing can't smuggle in a different list under a hoisted name:
// list-typed declarations are not whitelisted, and loop variables are always
// integer-typed here (only integer-range iterables are allowed).
// ---------------------------------------------------------------------------

typedef struct {
    Table_t *written; // variable name -> Var ast of lists written by index/swap
    Table_t *accessed; // variable name -> Var ast of ALL lists indexed (reads + writes)
} cow_scan_t;

static bool cow_expr_ok(env_t *env, ast_t *ast, cow_scan_t *scan);

// Scalar-ish types: values whose copies can't share a list buffer. A tagged
// word that may point at its own heap object still counts, since that object
// is never a list: `Int` (GMP bignum) and `Num` (exact real) both qualify.
static bool cow_type_ok(type_t *t) {
    if (t == NULL) return false;
    switch (t->tag) {
    case IntType:
    case BigIntType:
    case NumType:
    case FloatType:
    case BoolType:
    case ByteType: return true;
    case OptionalType: return cow_type_ok(Match(t, OptionalType)->type);
    case StructType: {
        for (arg_t *field = Match(t, StructType)->fields; field; field = field->next)
            if (!cow_type_ok(field->type)) return false;
        return true;
    }
    default: return false;
    }
}

// Non-fatal variable type lookup: the scanner must never `code_err` (it runs
// speculatively and merely declines the optimization on anything unmodeled,
// including discard variables like `_` that have no binding).
static type_t *cow_var_type(env_t *env, ast_t *var_ast) {
    if (var_ast->tag != Var) return NULL;
    binding_t *binding = get_binding(env, Match(var_ast, Var)->name);
    return binding ? binding->type : NULL;
}

// An indexed list element access (`xs[i]`) on a plain variable with
// scalar-typed items. Used for both reads and (in Assign targets) writes;
// records the list in `accessed` so its header can be hoisted into locals.
static bool cow_index_ok(env_t *env, ast_t *ast, cow_scan_t *scan) {
    DeclareMatch(index, ast, Index);
    if (index->index == NULL) return false; // `ptr[]` dereference takes a snapshot
    type_t *indexed_t = cow_var_type(env, index->indexed);
    if (indexed_t == NULL) return false;
    type_t *container_t = value_type(indexed_t);
    if (container_t->tag != ListType) return false; // table reads can insert defaults
    if (!cow_type_ok(Match(container_t, ListType)->item_type)) return false;
    Table$str_set(scan->accessed, Match(index->indexed, Var)->name, index->indexed);
    return true;
}

// Clearing each argument through the scan is what keeps a list from crossing
// into a call: a list-typed variable or element fails cow_type_ok there.
// Checking the argument's own type here as well would reject an untyped
// numeric literal, whose type is `Num` until the call coerces it.
static bool cow_call_args_ok(env_t *env, arg_ast_t *args, cow_scan_t *scan) {
    for (arg_ast_t *arg = args; arg; arg = arg->next)
        if (!cow_expr_ok(env, arg->value, scan)) return false;
    return true;
}

// The `self` of a method call, which is either a scalar-typed value
// (`v.len2()`) or a type's namespace (`Float64.sqrt(x)`, which parses as a
// method call on the type name).
static bool cow_method_self_ok(env_t *env, ast_t *self, cow_scan_t *scan) {
    type_t *self_t = cow_var_type(env, self);
    if (self_t != NULL && self_t->tag == TypeInfoType) return true;
    return cow_expr_ok(env, self, scan) && cow_type_ok(get_type(env, self));
}

static bool cow_expr_ok(env_t *env, ast_t *ast, cow_scan_t *scan) {
    if (ast == NULL) return false;
    switch (ast->tag) {
    case Int:
    case Num:
    case Bool:
    case None: return true;
    case Var: return cow_type_ok(cow_var_type(env, ast));
    case NonOptional: return cow_expr_ok(env, Match(ast, NonOptional)->value, scan);
    case Not: return cow_expr_ok(env, Match(ast, Not)->value, scan);
    case Negative: return cow_expr_ok(env, Match(ast, Negative)->value, scan);
    case Index: return cow_index_ok(env, ast, scan) && cow_expr_ok(env, Match(ast, Index)->index, scan);
    case FunctionCall: {
        // Constructor calls of scalar types (e.g. `Int64(1)`) are pure
        // conversions: with scalar-only arguments they can't touch any list.
        // A call through a plain variable stays disallowed: the variable could
        // hold a lambda, and a lambda can capture the list even when its
        // signature is all scalars.
        DeclareMatch(call, ast, FunctionCall);
        type_t *fn_t = cow_var_type(env, call->fn);
        if (fn_t == NULL || fn_t->tag != TypeInfoType || !cow_type_ok(Match(fn_t, TypeInfoType)->type)) return false;
        return cow_call_args_ok(env, call->args, scan);
    }
    case MethodCall: {
        // A method on a scalar-typed value (`v.len2()`) or in a type's
        // namespace (`Float64.sqrt(x)`), taking and returning only scalars.
        // This is the same class of call the BINOP_CASES arm below already
        // admits: an operator on a struct compiles to a call to the type's own
        // metamethod, so `a - b` on a Vec3 already emits `Vec3$minus(...)`.
        // Nothing reachable this way can name the loop's list.
        DeclareMatch(call, ast, MethodCall);
        if (!cow_method_self_ok(env, call->self, scan)) return false;
        return cow_type_ok(get_type(env, ast)) && cow_call_args_ok(env, call->args, scan);
    }
    case FieldAccess: {
        // Reading a field of an all-scalar struct reaches no list. A module or
        // type namespace on the left is not a scalar struct, so it stops here.
        DeclareMatch(access, ast, FieldAccess);
        type_t *subject_t = get_type(env, access->fielded);
        if (subject_t == NULL || subject_t->tag != StructType || !cow_type_ok(subject_t)) return false;
        return cow_expr_ok(env, access->fielded, scan);
    }
    case RecordLiteral: {
        // Building an all-scalar struct out of parts the scan has cleared.
        if (!cow_type_ok(get_type(env, ast))) return false;
        return cow_call_args_ok(env, Match(ast, RecordLiteral)->args, scan);
    }
    case BINOP_CASES: {
        if (is_update_assignment(ast)) return false; // statements, not expressions
        binary_operands_t operands = BINARY_OPERANDS(ast);
        return cow_expr_ok(env, operands.lhs, scan) && cow_expr_ok(env, operands.rhs, scan);
    }
    default: return false;
    }
}

static bool cow_stmt_ok(env_t *env, ast_t *ast, cow_scan_t *scan);

static bool cow_block_ok(env_t *env, ast_t *block, cow_scan_t *scan) {
    env_t *scope = fresh_scope(env);
    if (block->tag == Block) {
        for (ast_list_t *stmt = Match(block, Block)->statements; stmt; stmt = stmt->next)
            if (!cow_stmt_ok(scope, stmt->ast, scan)) return false;
        return true;
    }
    return cow_stmt_ok(scope, block, scan);
}

// Only integer-range iterables (`a.to(b)` on integers, or a plain integer
// count) are allowed for nested loops: iterating a container takes a snapshot
// of it, which is exactly the refcount bump we must rule out.
static bool cow_iterable_ok(env_t *env, ast_t *iter, cow_scan_t *scan) {
    if (iter->tag == MethodCall) {
        DeclareMatch(call, iter, MethodCall);
        if (!streq(call->name, "to")) return false;
        if (!cow_expr_ok(env, call->self, scan) || !is_int_type(get_type(env, call->self))) return false;
        for (arg_ast_t *arg = call->args; arg; arg = arg->next)
            if (!cow_expr_ok(env, arg->value, scan)) return false;
        return true;
    }
    return cow_expr_ok(env, iter, scan) && is_int_type(get_type(env, iter));
}

static bool cow_for_ok(env_t *env, ast_t *ast, cow_scan_t *scan) {
    DeclareMatch(for_, ast, For);
    if (for_->iters == NULL || for_->iters->next != NULL) return false; // no lockstep
    if (for_->empty != NULL) return false;
    if (!cow_iterable_ok(env, for_->iters->ast, scan)) return false;
    for (ast_list_t *var = for_->vars; var; var = var->next)
        if (var->ast->tag != Var) return false; // no `&x` reference vars
    if (for_->at != NULL && for_->at->tag != Var) return false;
    env_t *scope = for_scope(env, ast);
    return cow_block_ok(scope, for_->body, scan);
}

static bool cow_stmt_ok(env_t *env, ast_t *ast, cow_scan_t *scan) {
    switch (ast->tag) {
    case Pass:
    case Continue:
    case Break: return true;
    case Block: return cow_block_ok(env, ast, scan);
    case Declare: {
        DeclareMatch(decl, ast, Declare);
        if (!cow_expr_ok(env, decl->value, scan)) return false;
        // The value expression passed the scalar whitelist, so its type (and
        // thus the declared variable's) is scalar; an explicit annotation
        // could only promote it to another scalar type. Discard declarations
        // (`_ := ...`) have no binding, so don't query the variable itself.
        bind_statement(env, ast); // register the variable for later type lookups
        return cow_type_ok(get_type(env, decl->value));
    }
    case Assign: {
        DeclareMatch(assign, ast, Assign);
        for (ast_list_t *value = assign->values; value; value = value->next)
            if (!cow_expr_ok(env, value->ast, scan)) return false;
        for (ast_list_t *target = assign->targets; target; target = target->next) {
            if (target->ast->tag == Var) {
                if (!cow_type_ok(cow_var_type(env, target->ast))) return false;
            } else if (target->ast->tag == Index) {
                if (!cow_index_ok(env, target->ast, scan)) return false;
                if (!cow_expr_ok(env, Match(target->ast, Index)->index, scan)) return false;
                ast_t *indexed = Match(target->ast, Index)->indexed;
                Table$str_set(scan->written, Match(indexed, Var)->name, indexed);
            } else {
                return false;
            }
        }
        return true;
    }
    case UPDATE_CASES: {
        binary_operands_t operands = UPDATE_OPERANDS(ast);
        if (!cow_type_ok(cow_var_type(env, operands.lhs))) return false;
        return cow_expr_ok(env, operands.rhs, scan);
    }
    case MethodCall: {
        // `xs.swap(i, j)` is the one whitelisted method: it compiles inline
        // (List_swap) and behaves exactly like a pair of indexed writes: it
        // never snapshots or resizes, and its own CoW guard is elided under a
        // hoist just like List_lvalue's.
        DeclareMatch(call, ast, MethodCall);
        if (!streq(call->name, "swap")) return false;
        if (call->self->tag != Var) return false;
        type_t *self_t = cow_var_type(env, call->self);
        if (self_t == NULL) return false;
        type_t *container_t = value_type(self_t);
        if (container_t->tag != ListType) return false;
        if (!cow_type_ok(Match(container_t, ListType)->item_type)) return false;
        for (arg_ast_t *arg = call->args; arg; arg = arg->next)
            if (!cow_expr_ok(env, arg->value, scan)) return false;
        Table$str_set(scan->written, Match(call->self, Var)->name, call->self);
        Table$str_set(scan->accessed, Match(call->self, Var)->name, call->self);
        return true;
    }
    case If: {
        DeclareMatch(if_, ast, If);
        if (if_->condition == NULL || !cow_expr_ok(env, if_->condition, scan)) return false;
        if (!cow_block_ok(env, if_->body, scan)) return false;
        if (if_->else_body != NULL && !cow_block_ok(env, if_->else_body, scan)) return false;
        return true;
    }
    case While: {
        DeclareMatch(while_, ast, While);
        if (while_->condition != NULL && !cow_expr_ok(env, while_->condition, scan)) return false;
        return cow_block_ok(env, while_->body, scan);
    }
    case Repeat: return cow_block_ok(env, Match(ast, Repeat)->body, scan);
    case For: return cow_for_ok(env, ast, scan);
    default: return false; // calls, lambdas, returns, comprehensions, C code, ...
    }
}

public
bool is_cow_hoisted(env_t *env, ast_t *var_ast) {
    return var_ast->tag == Var && env->cow_hoisted && Table$str_get(*env->cow_hoisted, Match(var_ast, Var)->name);
}

public
Text_t cow_hoisted_local(ast_t *var_ast, const char *field) {
    return Texts("cow$", Match(var_ast, Var)->name, "$", field);
}

// If `loop_ast` (a For/While/Repeat) qualifies for CoW-guard hoisting, return
// a copy of `env` with the written list variables recorded in ->cow_hoisted
// and append the up-front compact-if-shared code for each to `prelude`.
// Otherwise return `env` unchanged with an empty prelude.
static env_t *cow_hoist_env(env_t *env, ast_t *loop_ast, Text_t *prelude) {
    cow_scan_t scan = {.written = new (Table_t), .accessed = new (Table_t)};
    env_t *scratch = fresh_scope(env);
    bool safe = false;
    switch (loop_ast->tag) {
    case While: {
        DeclareMatch(while_, loop_ast, While);
        safe = (while_->condition == NULL || cow_expr_ok(scratch, while_->condition, &scan))
               && cow_block_ok(scratch, while_->body, &scan);
        break;
    }
    case Repeat: safe = cow_block_ok(scratch, Match(loop_ast, Repeat)->body, &scan); break;
    case For: safe = cow_for_ok(scratch, loop_ast, &scan); break;
    default: safe = false; break;
    }
    if (!safe || Table$length(*scan.accessed) == 0) return env;

    env_t *hoisted = new (env_t);
    *hoisted = *env;
    hoisted->cow_hoisted = new (Table_t, .fallback = env->cow_hoisted);
    // Two phases, because different variables can alias the same heap list
    // (and thus the same header struct): first compact every *written* list
    // if shared, then capture every accessed list's header (data/stride/
    // length) into locals. Captures must come after ALL compacts, since a
    // compact changes the header and an alias captured before it would go stale.
    // Accesses inside the loop compile against these locals, so the C
    // compiler can keep the header in registers and strength-reduce the
    // stride multiply; going through the list pointer would force a reload on
    // every iteration, since element stores could alias the list struct.
    Text_t compacts = EMPTY_TEXT, captures = EMPTY_TEXT;
    for (int64_t i = 0; i < (int64_t)scan.accessed->entries.length; i++) {
        struct {
            const char *name;
            ast_t *var_ast;
        } *entry = scan.accessed->entries.data + scan.accessed->entries.stride * i;
        // Already hoisted by an enclosing loop: its locals are in scope.
        if (env->cow_hoisted && Table$str_get(*env->cow_hoisted, entry->name)) continue;
        ast_t *indexed = entry->var_ast;
        type_t *container_t = value_type(get_type(env, indexed));
        type_t *item_type = Match(container_t, ListType)->item_type;
        Text_t ptr = cow_hoisted_local(indexed, "ptr");
        compacts = Texts(compacts, "List_t *", ptr, " = ", compile_to_pointer_depth(env, indexed, 1, false), ";\n");
        if (Table$str_get(*scan.written, entry->name))
            compacts = Texts(compacts, "if unlikely (", ptr, "->data_refcount > 0) List$compact(", ptr, ", sizeof(",
                             compile_type(item_type), "));\n");
        captures = Texts(captures, "void *", cow_hoisted_local(indexed, "data"), " = ", ptr, "->data;\n", //
                         "int64_t ", cow_hoisted_local(indexed, "stride"), " = ", ptr, "->stride;\n", //
                         "int64_t ", cow_hoisted_local(indexed, "length"), " = ", ptr, "->length;\n");
        Table$str_set(hoisted->cow_hoisted, entry->name, entry->var_ast);
    }
    if (Table$length(*hoisted->cow_hoisted) == 0) return env;
    *prelude = Texts("// Hoisted copy-on-write guards + list headers:\n", compacts, captures);
    return hoisted;
}

// The TypeInfo companions a `--debug` build gives a `for` loop's own
// variables. `vars` is the loop's value variables plus its `at` counter;
// their types are looked up through the loop's scope rather than derived
// here, so each kind of iteration (list, table, integer range, iterator
// function, `for &x` by reference) gets whatever type it actually bound.
static Text_t compile_debug_loop_typeinfos(env_t *env, env_t *body_scope, ast_list_t *vars) {
    if (!env->do_debugging) return EMPTY_TEXT;
    Text_t code = EMPTY_TEXT;
    for (ast_list_t *v = vars; v; v = v->next) {
        ast_t *var = v->ast->tag == StackReference ? Match(v->ast, StackReference)->value : v->ast;
        if (var->tag != Var) continue;
        const char *name = Match(var, Var)->name;
        binding_t *b = get_binding(body_scope, name);
        // Only plain locals: a binding whose code is anything but the bare
        // variable is not a variable a debugger can look up by that name.
        if (b && Text$equal_values(b->code, Texts("_$", name)))
            code = Texts(code, compile_debug_typeinfo(env, name, b->type));
    }
    return code;
}

// Compile `for [i,] &x in xs`, in-place mutable iteration over a list.
//
// `&x` is a live pointer into the list's buffer, so element updates are plain
// in-place stores with no per-element bounds checks or copy-on-write guards.
// The list itself stays usable inside the body (reads and indexed writes see
// live data through the normal checked paths); the only things it can't do
// while the loop runs are the two that would invalidate the raw pointer or
// break copy-on-write, and both are runtime failures rather than silent bugs:
//   1. If a snapshot shares the buffer at loop entry (data_refcount > 0), we
//      compact once up front, the same copy CoW would charge on first write.
//   2. List_ref_iter_guard (stdlib/list.h) re-checks at the top of each
//      iteration AND once after the loop (including `stop` exits), so a
//      resize or copy in any iteration, even the last, fails before the
//      loop's results can be used. A mid-loop snapshot can't be *protected*
//      without a per-write CoW check (it would see the remainder of the
//      current iteration's writes), so it fails rather than silently
//      corrupting.
// `&x` itself is a non-escaping stack reference, so it can't outlive the loop.
static Text_t compile_for_reference_loop(env_t *env, ast_t *ast, Text_t naked_body, Text_t stop, type_t *item_t,
                                         Text_t index, ast_t *value_var) {
    DeclareMatch(for_, ast, For);
    ast_t *iter = for_->iters->ast;

    const char *name = Match(Match(value_var, StackReference)->value, Var)->name;
    Text_t value = Texts("_$", name);
    Text_t item_type_code = compile_type(item_t);
    Text_t guard = Texts("List_ref_iter_guard(ptr, data0, n0, stride0, ", quoted_str(ast->file->filename), ", ",
                         (int64_t)(ast->start - ast->file->text), ", ", (int64_t)(ast->end - ast->file->text), ");\n");

    Text_t loop = Texts("for (int64_t i = 1; i <= n0; ++i) {\n", guard, item_type_code, " *", value, " = (",
                        item_type_code, " *)(data0 + (i-1)*stride0);\n",
                        index.length > 0 ? Texts("Int64_t ", index, " = i;\n") : EMPTY_TEXT, naked_body, "}");

    if (for_->empty) loop = Texts("if (n0 > 0) {\n", loop, "\n} else ", compile_statement(env, for_->empty));

    return Texts("{ // for &", name,
                 " in ...\n"
                 "List_t *ptr = ",
                 compile_to_pointer_depth(env, iter, 1, false),
                 ";\n"
                 "if unlikely (ptr->data_refcount > 0) List$compact(ptr, sizeof(",
                 item_type_code,
                 "));\n"
                 "void *data0 = ptr->data;\n"
                 "int64_t n0 = ptr->length;\n"
                 "int64_t stride0 = ptr->stride;\n",
                 loop, stop,
                 // Post-loop check (runs on normal completion and on `stop`,
                 // which lands on the label above): catches a resize/copy made
                 // during the final iteration, which the per-iteration guard
                 // (top-of-body) would otherwise never see.
                 "\nif (n0 > 0) ", guard, "}\n");
}

// Build the C call for one step of an iterator function or closure, e.g.
// `next(&x, &y)` or `((ret(*)(...))next.fn)(&x, &y, next.userdata)`:
static Text_t compile_iterator_call(type_t *iter_value_t, Text_t next_fn, Text_t args_joined) {
    if (iter_value_t->tag == ClosureType) {
        type_t *fn_t = Match(iter_value_t, ClosureType)->fn;
        arg_t *closure_fn_args = NULL;
        for (arg_t *arg = Match(fn_t, FunctionType)->args; arg; arg = arg->next)
            closure_fn_args = new (arg_t, .name = arg->name, .type = arg->type, .default_val = arg->default_val,
                                   .next = closure_fn_args);
        closure_fn_args = new (arg_t, .name = "userdata", .type = Type(PointerType, .pointed = Type(MemoryType)),
                               .next = closure_fn_args);
        REVERSE_LIST(closure_fn_args);
        Text_t fn_type_code =
            compile_type(Type(FunctionType, .args = closure_fn_args, .ret = Match(fn_t, FunctionType)->ret));
        return Texts("((", fn_type_code, ")", next_fn, ".fn)(", args_joined, args_joined.length > 0 ? ", " : "",
                     next_fn, ".userdata)");
    } else {
        return Texts(next_fn, "(", args_joined, ")");
    }
}

// `xs.pairs()` and `t.entries()` are multi-value iterators, but when they
// appear directly in for-position we compile them as inline index loops --
// no closure is allocated and no indirect call happens per iteration. The
// result is identical to iterating the closure; this is a pure performance
// special-case. Returns true (having appended to setup/advance/cleanup and
// advanced *var_p) if `iter_ast` is such a call, false otherwise.
static bool is_combinatoric_call(env_t *env, ast_t *iter_ast) {
    if (iter_ast->tag != MethodCall) return false;
    DeclareMatch(call, iter_ast, MethodCall);
    if (call->args) return false; // extra args -> let the normal path report the error
    type_t *self_t = value_type(get_type(env, call->self));
    if (streq(call->name, "pairs")) return self_t->tag == ListType;
    if (streq(call->name, "entries"))
        return self_t->tag == TableType && Match(self_t, TableType)->value_type != PRESENT_TYPE;
    return false;
}

static void compile_combinatoric_fragment(env_t *env, env_t *body_scope, ast_t *iter_ast, Text_t temp,
                                          ast_list_t **var_p, Text_t *setup, Text_t *advance, Text_t *cleanup) {
    DeclareMatch(call, iter_ast, MethodCall);
    ast_t *self = call->self;
    type_t *raw_self = get_type(env, self);
    type_t *self_t = value_type(raw_self);

    if (streq(call->name, "pairs")) {
        // Each unordered pair of distinct elements once (i < j). An incref'd
        // snapshot gives the same "mutations aren't seen" behavior as the
        // closure: mutating the list mid-loop copies its buffer first.
        type_t *item_t = Match(self_t, ListType)->item_type;
        Text_t list = Texts(temp, "$list"), i = Texts(temp, "$i"), j = Texts(temp, "$j");
        if (raw_self->tag == PointerType) {
            Text_t ptr = Texts(temp, "$ptr");
            *setup = Texts(*setup, "List_t *", ptr, " = ", compile_to_pointer_depth(env, self, 1, false),
                           ";\nLIST_INCREF(*", ptr, ");\nList_t ", list, " = *", ptr, ";\n");
            *cleanup = Texts(*cleanup, "LIST_DECREF(*", ptr, ");\n");
        } else {
            *setup = Texts(*setup, "List_t ", list, " = ", compile_to_pointer_depth(env, self, 0, false), ";\n");
        }
        *setup = Texts(*setup, "int64_t ", i, " = 1, ", j, " = 1;\n");
        *advance = Texts(*advance, j, " += 1;\nif (", j, " > (int64_t)", list, ".length) { ", i, " += 1; ", j, " = ", i,
                         " + 1; }\nif (", j, " > (int64_t)", list, ".length) break;\n");
        ast_t *va = (*var_p)->ast;
        *var_p = (*var_p)->next;
        if (!streq(Match(va, Var)->name, "_"))
            *advance = Texts(*advance, compile_declaration(item_t, compile(body_scope, va)), " = *(",
                             compile_type(item_t), "*)(", list, ".data + (", i, "-1)*", list, ".stride);\n");
        ast_t *vb = (*var_p)->ast;
        *var_p = (*var_p)->next;
        if (!streq(Match(vb, Var)->name, "_"))
            *advance = Texts(*advance, compile_declaration(item_t, compile(body_scope, vb)), " = *(",
                             compile_type(item_t), "*)(", list, ".data + (", j, "-1)*", list, ".stride);\n");
    } else { // "entries"
        type_t *key_t = Match(self_t, TableType)->key_type;
        type_t *value_t = Match(self_t, TableType)->value_type;
        Text_t list = Texts(temp, "$entries"), idx = Texts(temp, "$i");
        if (raw_self->tag == PointerType) {
            Text_t ptr = Texts(temp, "$ptr");
            *setup = Texts(*setup, "Table_t *", ptr, " = ", compile_to_pointer_depth(env, self, 1, false),
                           ";\nLIST_INCREF(", ptr, "->entries);\nList_t ", list, " = ", ptr, "->entries;\n");
            *cleanup = Texts(*cleanup, "LIST_DECREF(", ptr, "->entries);\n");
        } else {
            *setup =
                Texts(*setup, "List_t ", list, " = (", compile_to_pointer_depth(env, self, 0, false), ").entries;\n");
        }
        *setup = Texts(*setup, "int64_t ", idx, " = 0;\n");
        *advance = Texts(*advance, "if (", idx, " >= ", list, ".length) break;\n", idx, " += 1;\n");
        ast_t *key_var = (*var_p)->ast;
        *var_p = (*var_p)->next;
        if (!streq(Match(key_var, Var)->name, "_"))
            *advance = Texts(*advance, compile_declaration(key_t, compile(body_scope, key_var)), " = *(",
                             compile_type(key_t), "*)(", list, ".data + (", idx, "-1)*", list, ".stride);\n");
        ast_t *value_var = (*var_p)->ast;
        *var_p = (*var_p)->next;
        if (!streq(Match(value_var, Var)->name, "_")) {
            Text_t value_offset = Texts("offsetof(struct { ", compile_declaration(key_t, Text("k")), "; ",
                                        compile_declaration(value_t, Text("v")), "; }, v)");
            *advance = Texts(*advance, compile_declaration(value_t, compile(body_scope, value_var)), " = *(",
                             compile_type(value_t), "*)(", list, ".data + (", idx, "-1)*", list, ".stride + ",
                             value_offset, ");\n");
        }
    }
}

// Compile `for x, y in xs, ys`, lockstep iteration over several iterables.
//
// Each iterable contributes a setup step (run once, leftmost first, so the
// iterables are evaluated in source order) and an advance-or-break step at the
// top of a single shared loop, so the loop ends as soon as ANY iterable runs
// out. Each iterable's yielded values bind to its slice of the loop variables
// (`_` discards one); arity was already checked in for_scope(). The `else`
// block (if any) runs when the body never ran at all.
//
// A single-iterable `for a, b in xs.pairs()` also routes here (so pairs() and
// entries() get their inline codegen); it's just a lockstep loop of one.
static Text_t compile_lockstep_loop(env_t *env, ast_t *ast, env_t *body_scope, Text_t naked_body, Text_t stop) {
    DeclareMatch(for_, ast, For);

    Text_t setup = EMPTY_TEXT, advance = EMPTY_TEXT, cleanup = EMPTY_TEXT;
    ast_list_t *var = for_->vars;
    int64_t k = 0;
    for (ast_list_t *it = for_->iters; it; it = it->next, k += 1) {
        Text_t temp = Texts("lockstep", k);
        if (is_combinatoric_call(env, it->ast)) {
            compile_combinatoric_fragment(env, body_scope, it->ast, temp, &var, &setup, &advance, &cleanup);
            continue;
        }
        type_t *raw_t = get_type(env, it->ast);
        type_t *iter_value_t = value_type(raw_t);
        switch (iter_value_t->tag) {
        case ListType: {
            type_t *item_t = Match(iter_value_t, ListType)->item_type;
            Text_t list = Texts(temp, "$list"), idx = Texts(temp, "$i");
            if (raw_t->tag == PointerType) {
                Text_t ptr = Texts(temp, "$ptr");
                setup = Texts(setup, "List_t *", ptr, " = ", compile_to_pointer_depth(env, it->ast, 1, false),
                              ";\nLIST_INCREF(*", ptr, ");\nList_t ", list, " = *", ptr, ";\n");
                cleanup = Texts(cleanup, "LIST_DECREF(*", ptr, ");\n");
            } else {
                setup = Texts(setup, "List_t ", list, " = ", compile_to_pointer_depth(env, it->ast, 0, false), ";\n");
            }
            setup = Texts(setup, "int64_t ", idx, " = 0;\n");
            advance = Texts(advance, "if (", idx, " >= ", list, ".length) break;\n", idx, " += 1;\n");
            ast_t *v = var->ast;
            var = var->next;
            if (!streq(Match(v, Var)->name, "_"))
                advance = Texts(advance, compile_declaration(item_t, compile(body_scope, v)), " = *(",
                                compile_type(item_t), "*)(", list, ".data + (", idx, "-1)*", list, ".stride);\n");
            break;
        }
        case TableType: {
            // Only sets ({T}) reach here (direct table iteration is a compile
            // error in iteration_slots()); iterate the entry keys:
            type_t *item_t = Match(iter_value_t, TableType)->key_type;
            Text_t list = Texts(temp, "$entries"), idx = Texts(temp, "$i");
            if (raw_t->tag == PointerType) {
                Text_t ptr = Texts(temp, "$ptr");
                setup = Texts(setup, "Table_t *", ptr, " = ", compile_to_pointer_depth(env, it->ast, 1, false),
                              ";\nLIST_INCREF(", ptr, "->entries);\nList_t ", list, " = ", ptr, "->entries;\n");
                cleanup = Texts(cleanup, "LIST_DECREF(", ptr, "->entries);\n");
            } else {
                setup = Texts(setup, "List_t ", list, " = (", compile_to_pointer_depth(env, it->ast, 0, false),
                              ").entries;\n");
            }
            setup = Texts(setup, "int64_t ", idx, " = 0;\n");
            advance = Texts(advance, "if (", idx, " >= ", list, ".length) break;\n", idx, " += 1;\n");
            ast_t *v = var->ast;
            var = var->next;
            if (!streq(Match(v, Var)->name, "_"))
                advance = Texts(advance, compile_declaration(item_t, compile(body_scope, v)), " = *(",
                                compile_type(item_t), "*)(", list, ".data + (", idx, "-1)*", list, ".stride);\n");
            break;
        }
        case BigIntType: {
            Text_t cur = Texts(temp, "$cur"), max = Texts(temp, "$max");
            setup = Texts(setup, "Int_t ", cur, " = I_small(1), ", max, " = ",
                          compile_to_pointer_depth(env, it->ast, 0, false), ";\n");
            advance = Texts(advance, "if (Int$compare_value(", cur, ", ", max, ") > 0) break;\n");
            ast_t *v = var->ast;
            var = var->next;
            if (!streq(Match(v, Var)->name, "_"))
                advance = Texts(advance, "Int_t ", compile(body_scope, v), " = ", cur, ";\n");
            advance = Texts(advance, cur, " = Int$plus(", cur, ", I_small(1));\n");
            break;
        }
        case IntType: {
            Text_t type_code = compile_type(iter_value_t);
            Text_t cur = Texts(temp, "$cur"), max = Texts(temp, "$max");
            setup = Texts(setup, "Int64_t ", cur, " = 0, ", max, " = (Int64_t)(",
                          compile_to_pointer_depth(env, it->ast, 0, false), ");\n");
            advance = Texts(advance, "if (", cur, " >= ", max, ") break;\n", cur, " += 1;\n");
            ast_t *v = var->ast;
            var = var->next;
            if (!streq(Match(v, Var)->name, "_"))
                advance = Texts(advance, type_code, " ", compile(body_scope, v), " = (", type_code, ")", cur, ";\n");
            break;
        }
        case TextType: {
            Text_t state = Texts(temp, "$state"), idx = Texts(temp, "$i");
            setup = Texts(setup, "TextIter_t ", state, " = NEW_TEXT_ITER_STATE(",
                          compile_to_pointer_depth(env, it->ast, 0, false), ");\nint64_t ", idx, " = 0;\n");
            advance = Texts(advance, "if (", idx, " >= (int64_t)", state, ".stack[0].text.length) break;\n");
            ast_t *v = var->ast;
            var = var->next;
            if (!streq(Match(v, Var)->name, "_"))
                advance = Texts(advance, "Text_t ", compile(body_scope, v), " = Text$from_grapheme(",
                                "Text$get_grapheme_fast(&", state, ", ", idx, "));\n");
            advance = Texts(advance, idx, " += 1;\n");
            break;
        }
        case FunctionType:
        case ClosureType: {
            Text_t next_fn;
            if (is_idempotent(it->ast)) {
                next_fn = compile_to_pointer_depth(env, it->ast, 0, false);
            } else {
                next_fn = Texts(temp, "$next");
                setup = Texts(setup, compile_declaration(iter_value_t, next_fn), " = ",
                              compile_to_pointer_depth(env, it->ast, 0, false), ";\n");
            }

            arg_t *yield_args = iterator_yield_args(iter_value_t);
            if (yield_args) {
                // Multi-value iterator protocol: the loop variables are the
                // `&` out-slots (declared outside the loop, written each call):
                Text_t args_joined = EMPTY_TEXT;
                int64_t pos = 1;
                for (arg_t *arg = yield_args; arg; arg = arg->next, pos += 1) {
                    type_t *val_t = Match(arg->type, PointerType)->pointed;
                    ast_t *v = var->ast;
                    var = var->next;
                    Text_t name = streq(Match(v, Var)->name, "_") ? Texts(temp, "$yield", pos) : compile(body_scope, v);
                    setup = Texts(setup, compile_declaration(val_t, name), ";\n");
                    args_joined = pos == 1 ? Texts("&", name) : Texts(args_joined, ", &", name);
                }
                advance =
                    Texts(advance, "if (!", compile_iterator_call(iter_value_t, next_fn, args_joined), ") break;\n");
            } else {
                __typeof(iter_value_t->__data.FunctionType) *fn =
                    iter_value_t->tag == ClosureType ? Match(Match(iter_value_t, ClosureType)->fn, FunctionType)
                                                     : Match(iter_value_t, FunctionType);
                Text_t call = compile_iterator_call(iter_value_t, next_fn, EMPTY_TEXT);
                ast_t *v = var->ast;
                var = var->next;
                bool discard = streq(Match(v, Var)->name, "_");
                if (fn->ret->tag == OptionalType) {
                    Text_t cur = Texts(temp, "$cur");
                    setup = Texts(setup, compile_declaration(fn->ret, cur), ";\n");
                    advance = Texts(advance, cur, " = ", call, ";\nif (", check_none(fn->ret, cur), ") break;\n");
                    if (!discard)
                        advance = Texts(advance,
                                        compile_declaration(Match(fn->ret, OptionalType)->type, compile(body_scope, v)),
                                        " = ", optional_into_nonnone(fn->ret, cur), ";\n");
                } else if (!discard) {
                    advance = Texts(advance, compile_declaration(fn->ret, compile(body_scope, v)), " = ", call, ";\n");
                } else {
                    advance = Texts(advance, call, ";\n");
                }
            }
            break;
        }
        default: code_err(it->ast, "Iteration is not implemented for type: ", type_to_text(raw_t));
        }
    }

    Text_t at_decl = EMPTY_TEXT, at_step = EMPTY_TEXT;
    if (for_->at) {
        Text_t index = compile(body_scope, for_->at);
        at_decl = index_counter_decl(index);
        at_step = index_counter_step(index);
    }

    Text_t code = Texts("{ // lockstep iteration\n", setup, at_decl);
    if (for_->empty) code = Texts(code, "Bool_t lockstep$ran = no;\n");
    code = Texts(code, "for (;;) {\n", advance);
    if (for_->empty) code = Texts(code, "lockstep$ran = yes;\n");
    code = Texts(code, at_step, naked_body, "}\n", stop, "\n", cleanup);
    if (for_->empty) code = Texts(code, "if (!lockstep$ran) ", compile_statement(env, for_->empty), "\n");
    return Texts(code, "}\n");
}

static Text_t compile_for_loop_impl(env_t *env, ast_t *ast) {
    DeclareMatch(for_, ast, For);
    ast_t *iter = for_->iters->ast;

    // If we're iterating over a comprehension, that's actually just doing
    // one loop, we don't need to compile the comprehension as a list
    // comprehension. This is a common case for reducers like `(+: i*2 for i
    // in 5)` or `(and) x.is_good() for x in xs`
    if (!for_->iters->next && iter->tag == Comprehension) {
        DeclareMatch(comp, iter, Comprehension);
        ast_t *body = for_->body;
        if (for_->at)
            code_err(for_->at, "An `at` counter isn't supported when iterating over a comprehension. "
                               "Put the `at` inside the comprehension instead.");
        if (for_->vars) {
            if (for_->vars->ast->tag == StackReference)
                code_err(for_->vars->ast, "You can't iterate by reference over a comprehension");
            if (for_->vars->next) code_err(for_->vars->next->ast, "This is too many variables for iteration");

            body = WrapAST(
                ast, Block,
                .statements =
                    new (ast_list_t, .ast = WrapAST(ast, Declare, .var = for_->vars->ast, .value = comp->expr),
                         .next = body->tag == Block ? Match(body, Block)->statements : new (ast_list_t, .ast = body)));
        }

        if (comp->filter) body = WrapAST(for_->body, If, .condition = comp->filter, .body = body);
        ast_t *loop = WrapAST(ast, For, .vars = comp->vars, .at = comp->at, .iters = comp->iters, .body = body);
        return compile_statement(env, loop);
    }

    env_t *body_scope = for_scope(env, ast);
    // `continue`/`break` can target any loop variable by name, including the `at` counter:
    ast_list_t *ctx_vars = for_->at ? new (ast_list_t, .ast = for_->at, .next = for_->vars) : for_->vars;
    // The loop's own variables get their TypeInfo companions at the top of the
    // body. Every branch below declares the loop variables immediately before
    // it plants `naked_body`, so putting the companions at the front of the
    // body puts them in the same block as the variables they describe, which
    // is what a debugger stopped inside the loop looks in. This has to happen
    // before the body is compiled: compiling it binds the body's own
    // declarations into body_scope too, and those get their companions from
    // the Declare case instead.
    Text_t loop_var_typeinfos = compile_debug_loop_typeinfos(env, body_scope, ctx_vars);
    loop_ctx_t loop_ctx = (loop_ctx_t){
        .loop_name = "for",
        .loop_vars = ctx_vars,
        .deferred = body_scope->deferred,
        .next = body_scope->loop_ctx,
    };
    body_scope->loop_ctx = &loop_ctx;
    // Naked means no enclosing braces:
    Text_t naked_body = Texts(loop_var_typeinfos, compile_inline_block(body_scope, for_->body));
    if (loop_ctx.skip_label.length > 0) naked_body = Texts(naked_body, "\n", loop_ctx.skip_label, ": continue;");
    Text_t stop = loop_ctx.stop_label.length > 0 ? Texts("\n", loop_ctx.stop_label, ":;") : EMPTY_TEXT;

    // Lockstep loops, and single `for a, b in xs.pairs()` / `for k, v in
    // t.entries()` loops, compile to inline index loops (no closure, no
    // indirect calls) through the same fragment machinery:
    if (for_->iters->next || is_combinatoric_call(env, iter))
        return compile_lockstep_loop(env, ast, body_scope, naked_body, stop);

    // Special case for improving performance for numeric iteration:
    if (iter->tag == MethodCall && streq(Match(iter, MethodCall)->name, "to")
        && is_int_type(get_type(env, Match(iter, MethodCall)->self))) {
        // TODO: support other integer types
        arg_ast_t *args = Match(iter, MethodCall)->args;
        if (!args) code_err(iter, "to() needs at least one argument");

        type_t *int_type = get_type(env, Match(iter, MethodCall)->self);
        type_t *step_type = int_type->tag == ByteType ? Type(IntType, .bits = TYPE_IBITS8) : int_type;

        Text_t last = EMPTY_TEXT, step = EMPTY_TEXT, optional_step = EMPTY_TEXT;
        if (!args->name || streq(args->name, "last")) {
            last = compile_to_type(env, args->value, int_type);
            if (args->next) {
                if (args->next->name && !streq(args->next->name, "step"))
                    code_err(args->next->value, "Invalid argument name: ", args->next->name);
                if (get_type(env, args->next->value)->tag == OptionalType)
                    optional_step = compile_to_type(env, args->next->value, Type(OptionalType, step_type));
                else step = compile_to_type(env, args->next->value, step_type);
            }
        } else if (streq(args->name, "step")) {
            if (get_type(env, args->value)->tag == OptionalType)
                optional_step = compile_to_type(env, args->value, Type(OptionalType, step_type));
            else step = compile_to_type(env, args->value, step_type);
            if (args->next) {
                if (args->next->name && !streq(args->next->name, "last"))
                    code_err(args->next->value, "Invalid argument name: ", args->next->name);
                last = compile_to_type(env, args->next->value, int_type);
            }
        }

        if (last.length == 0) code_err(iter, "No `last` argument was given");

        Text_t type_code = compile_type(int_type);
        // `for i, x in a.to(b)`: `i` is an Int64 iteration counter (1, 2, 3, ...)
        // and `x` is the range value, typed like the range's endpoints.
        ast_t *index_var = for_->at;
        ast_t *value_var = single_loop_var(for_->vars);
        Text_t index = index_var ? compile(body_scope, index_var) : EMPTY_TEXT;
        Text_t value = value_var ? compile(body_scope, value_var) : Text("i");
        Text_t counter_decl = index_counter_decl(index);
        Text_t index_decl = index_counter_step(index);
        Text_t open = index.length > 0 ? Text("{\n") : EMPTY_TEXT;
        Text_t close = index.length > 0 ? Text("\n}\n") : EMPTY_TEXT;
        if (int_type->tag == BigIntType) {
            if (optional_step.length > 0)
                step = Texts("({ OptionalInt_t maybe_step = ", optional_step,
                             "; maybe_step->small == 0 ? "
                             "(Int$compare_value(last, first) >= 0 "
                             "? I_small(1) : I_small(-1)) : (Int_t)maybe_step; "
                             "})");
            else if (step.length == 0)
                step = Text("Int$compare_value(last, first) >= 0 ? "
                            "I_small(1) : I_small(-1)");
            return Texts(open, counter_decl, "for (", type_code,
                         " first = ", compile(env, Match(iter, MethodCall)->self), ", ", value,
                         " = first, last = ", last, ", step = ", step,
                         "; "
                         "Int$compare_value(",
                         value, ", last) != Int$compare_value(step, I_small(0)); ", value, " = Int$plus(", value,
                         ", step)) {\n", index_decl, "\t", naked_body, "}", stop, close);
        } else {
            if (optional_step.length > 0)
                step = Texts("({ ", compile_type(Type(OptionalType, step_type)), " maybe_step = ", optional_step,
                             "; "
                             "maybe_step.is_none ? (",
                             type_code, ")(last >= first ? 1 : -1) : maybe_step.value; })");
            else if (step.length == 0) step = Texts("(", type_code, ")(last >= first ? 1 : -1)");
            return Texts(open, counter_decl, "for (", type_code,
                         " first = ", compile(env, Match(iter, MethodCall)->self), ", ", value,
                         " = first, last = ", last, ", step = ", step, "; (", compile_type(step_type), ")step > 0 ? ",
                         value, " <= last : ", value, " >= last; ", value, " += step) {\n", index_decl, "\t",
                         naked_body, "}", stop, close);
        }
    } else if (iter->tag == MethodCall && streq(Match(iter, MethodCall)->name, "onward")
               && get_type(env, Match(iter, MethodCall)->self)->tag == BigIntType) {
        // Special case for Int.onward()
        arg_ast_t *args = Match(iter, MethodCall)->args;
        arg_t *arg_spec =
            new (arg_t, .name = "step", .type = INT_TYPE, .default_val = FakeAST(Int, .i = I_small(1)), .next = NULL);
        Text_t step = compile_arguments(env, iter, arg_spec, args);
        ast_t *index_var = for_->at;
        ast_t *value_var = single_loop_var(for_->vars);
        Text_t index = index_var ? compile(body_scope, index_var) : EMPTY_TEXT;
        Text_t value = value_var ? compile(body_scope, value_var) : Text("i");
        Text_t open = index.length > 0 ? Text("{\n") : EMPTY_TEXT;
        Text_t close = index.length > 0 ? Text("\n}\n") : EMPTY_TEXT;
        return Texts(open, index_counter_decl(index), "for (Int_t ", value, " = ",
                     compile(env, Match(iter, MethodCall)->self), ", ", "step = ", step, "; ; ", value, " = Int$plus(",
                     value, ", step)) {\n", index_counter_step(index), "\t", naked_body, "}", stop, close);
    }

    type_t *iter_t = get_type(env, iter);
    type_t *iter_value_t = value_type(iter_t);

    switch (iter_value_t->tag) {
    case ListType: {
        type_t *item_t = Match(iter_value_t, ListType)->item_type;
        ast_t *index_var = for_->at;
        ast_t *value_var = single_loop_var(for_->vars);
        Text_t index = index_var ? compile(body_scope, index_var) : EMPTY_TEXT;
        Text_t value = EMPTY_TEXT;

        if (value_var && value_var->tag == StackReference)
            return compile_for_reference_loop(env, ast, naked_body, stop, item_t, index, value_var);

        if (value_var) value = compile(body_scope, value_var);

        Text_t loop = EMPTY_TEXT;
        loop = Texts(loop, "for (int64_t i = 1; i <= iterating.length; ++i)");

        if (index.length > 0) naked_body = Texts("Int64_t ", index, " = i;\n", naked_body);

        if (value.length > 0) {
            loop = Texts(loop, "{\n", compile_declaration(item_t, value), " = *(", compile_type(item_t),
                         "*)(iterating.data + (i-1)*iterating.stride);\n", naked_body, "\n}");
        } else {
            loop = Texts(loop, "{\n", naked_body, "\n}");
        }

        if (for_->empty)
            loop = Texts("if (iterating.length > 0) {\n", loop, "\n} else ", compile_statement(env, for_->empty));

        if (iter_t->tag == PointerType) {
            loop = Texts("{\n"
                         "List_t *ptr = ",
                         compile_to_pointer_depth(env, iter, 1, false),
                         ";\n"
                         "\nLIST_INCREF(*ptr);\n"
                         "List_t iterating = *ptr;\n",
                         loop, stop,
                         "\nLIST_DECREF(*ptr);\n"
                         "}\n");

        } else {
            loop = Texts("{\n"
                         "List_t iterating = ",
                         compile_to_pointer_depth(env, iter, 0, false), ";\n", loop, stop, "}\n");
        }
        return loop;
    }
    case TableType: {
        // Only sets ({T}) reach here (direct table iteration is a compile
        // error in for_scope()); iterate the entry keys:
        Text_t loop = Text("for (int64_t i = 0; i < (int64_t)iterating.length; ++i) {\n");
        if (for_->at) loop = Texts(loop, "Int64_t ", compile(body_scope, for_->at), " = i + 1;\n");
        ast_t *value_var = single_loop_var(for_->vars);
        if (value_var) {
            Text_t item = compile(body_scope, value_var);
            type_t *item_t = Match(iter_value_t, TableType)->key_type;
            loop = Texts(loop, compile_declaration(item_t, item), " = *(", compile_type(item_t), "*)(",
                         "iterating.data + i*iterating.stride);\n");
        }

        loop = Texts(loop, naked_body, "\n}");

        if (for_->empty) {
            loop = Texts("if (iterating.length > 0) {\n", loop, "\n} else ", compile_statement(env, for_->empty));
        }

        // NOTE: the `stop` label goes before the DECREF (like the ListType
        // case) so that stopping out of the loop still releases the refcount.
        if (iter_t->tag == PointerType) {
            loop = Texts("{\n", "Table_t *t = ", compile_to_pointer_depth(env, iter, 1, false),
                         ";\n"
                         "LIST_INCREF(t->entries);\n"
                         "List_t iterating = t->entries;\n",
                         loop, stop,
                         "\nLIST_DECREF(t->entries);\n"
                         "}\n");
        } else {
            loop = Texts("{\n", "List_t iterating = (", compile_to_pointer_depth(env, iter, 0, false), ").entries;\n",
                         loop, stop, "}\n");
        }
        return loop;
    }
    case BigIntType: {
        // `for x in n` counts with the count's own type (`Int`); the optional
        // index form `for i, x in n` adds an Int64 index. The index is just a
        // counter starting at 1, and it can't plausibly overflow within any
        // physically executable loop, so it needs no relation to the bound.
        ast_t *index_var = for_->at;
        ast_t *value_var = single_loop_var(for_->vars);
        Text_t index = index_var ? compile(body_scope, index_var) : EMPTY_TEXT;
        Text_t value = value_var ? compile(body_scope, value_var) : EMPTY_TEXT;

        Text_t n;
        if (iter->tag == Int) {
            mpz_t i;
            mpz_init_int(i, Match(iter, Int)->i);
            if (mpz_cmpabs_ui(i, BIGGEST_SMALL_INT) <= 0) n = Text$from_str(mpz_get_str(NULL, 10, i));
            else goto big_n;

            if (for_->empty && mpz_cmp_si(i, 0) <= 0) {
                return compile_statement(env, for_->empty);
            } else {
                return Texts("for (int64_t i = 1; i <= ", n, "; ++i) {\n",
                             index.length > 0 ? Texts("\tInt64_t ", index, " = i;\n") : EMPTY_TEXT,
                             value.length > 0 ? Texts("\tInt_t ", value, " = I_small(i);\n") : EMPTY_TEXT, "\t",
                             naked_body, "}\n", stop, "\n");
            }
        }

    big_n:
        n = compile_to_pointer_depth(env, iter, 0, false);
        Text_t i = value.length > 0 ? value : Text("i");
        Text_t n_var = value.length > 0 ? Texts("max", i) : Text("n");
        Text_t counter_decl = index_counter_decl(index);
        Text_t index_decl = index_counter_step(index);
        if (for_->empty) {
            return Texts("{\n", counter_decl, "Int_t ", n_var, " = ", n,
                         ";\n"
                         "if (Int$compare_value(",
                         n_var,
                         ", I(0)) > 0) {\n"
                         "for (Int_t ",
                         i, " = I(1); Int$compare_value(", i, ", ", n_var, ") <= 0; ", i, " = Int$plus(", i,
                         ", I(1))) {\n", index_decl, "\t", naked_body,
                         "}\n"
                         "} else ",
                         compile_statement(env, for_->empty), stop,
                         "\n"
                         "}\n");
        } else {
            Text_t open = index.length > 0 ? Text("{\n") : EMPTY_TEXT;
            Text_t close = index.length > 0 ? Text("\n}\n") : EMPTY_TEXT;
            return Texts(open, counter_decl, "for (Int_t ", i, " = I(1), ", n_var, " = ", n, "; Int$compare_value(", i,
                         ", ", n_var, ") <= 0; ", i, " = Int$plus(", i, ", I(1))) {\n", index_decl, "\t", naked_body,
                         "}\n", stop, close);
        }
    }
    case IntType: {
        // Native-int counts (`for [i,] x in n` where n is Int64/Int32/...): a
        // native loop whose value variable has the count's own type and whose
        // optional index is an Int64. The internal counter is Int64 so that
        // counting to a smaller type's maximum can't overflow.
        ast_t *index_var = for_->at;
        ast_t *value_var = single_loop_var(for_->vars);
        Text_t index = index_var ? compile(body_scope, index_var) : EMPTY_TEXT;
        Text_t value = value_var ? compile(body_scope, value_var) : EMPTY_TEXT;
        Text_t type_code = compile_type(iter_value_t);
        Text_t n = compile_to_pointer_depth(env, iter, 0, false);
        Text_t decls = Texts(index.length > 0 ? Texts("Int64_t ", index, " = i$;\n") : EMPTY_TEXT,
                             value.length > 0 ? Texts(type_code, " ", value, " = (", type_code, ")i$;\n") : EMPTY_TEXT);
        Text_t n_var = value.length > 0 ? Texts("max", value) : Text("n");
        if (for_->empty) {
            return Texts("{\n"
                         "Int64_t ",
                         n_var, " = (Int64_t)(", n,
                         ");\n"
                         "if (",
                         n_var,
                         " > 0) {\n"
                         "for (Int64_t i$ = 1; i$ <= ",
                         n_var, "; ++i$) {\n", decls, "\t", naked_body,
                         "}\n"
                         "} else ",
                         compile_statement(env, for_->empty), stop,
                         "\n"
                         "}\n");
        } else {
            return Texts("for (Int64_t i$ = 1, ", n_var, " = (Int64_t)(", n, "); i$ <= ", n_var, "; ++i$) {\n", decls,
                         "\t", naked_body, "}\n", stop, "\n");
        }
    }
    case FunctionType:
    case ClosureType: {
        // Iterator function. `for x at i in iterfn` gives an Int64 iteration
        // counter (1, 2, 3, ...) alongside each yielded value.
        ast_t *index_var = for_->at;
        Text_t code = Text("{\n");
        if (index_var) {
            Text_t index = compile(body_scope, index_var);
            code = Texts(code, index_counter_decl(index));
            naked_body = Texts(index_counter_step(index), naked_body);
        }

        Text_t next_fn;
        if (is_idempotent(iter)) {
            next_fn = compile_to_pointer_depth(env, iter, 0, false);
        } else {
            code = Texts(code, compile_declaration(iter_value_t, Text("next")), " = ",
                         compile_to_pointer_depth(env, iter, 0, false), ";\n");
            next_fn = Text("next");
        }

        __typeof(iter_value_t->__data.FunctionType) *fn =
            iter_value_t->tag == ClosureType ? Match(Match(iter_value_t, ClosureType)->fn, FunctionType)
                                             : Match(iter_value_t, FunctionType);

        // Multi-value iterator protocol: every argument is a `&` out-parameter
        // and the return is a Bool saying whether values were produced. The
        // loop variables themselves are the out-slots: each call writes the
        // next values directly into them, no tuple or wrapper value involved.
        arg_t *yield_args = iterator_yield_args(iter_value_t);
        if (yield_args) {
            Text_t args_joined = EMPTY_TEXT;
            int64_t pos = 1;
            ast_list_t *v = for_->vars;
            for (arg_t *arg = yield_args; arg; arg = arg->next, pos += 1) {
                type_t *val_t = Match(arg->type, PointerType)->pointed;
                Text_t name = (v && !streq(Match(v->ast, Var)->name, "_")) ? Texts("_$", Match(v->ast, Var)->name)
                                                                           : Texts("yield$", pos);
                code = Texts(code, compile_declaration(val_t, name), ";\n");
                args_joined = pos == 1 ? Texts("&", name) : Texts(args_joined, ", &", name);
                if (v) v = v->next;
            }

            Text_t get_next = compile_iterator_call(iter_value_t, next_fn, args_joined);

            if (for_->empty) {
                code = Texts(code, "if (", get_next,
                             ") {\n"
                             "\tdo{\n\t\t",
                             naked_body, "\t} while(", get_next,
                             ");\n"
                             "} else {\n\t",
                             compile_statement(env, for_->empty), "}", stop, "\n}\n");
            } else {
                code = Texts(code, "while(", get_next, ") {\n\t", naked_body, "}\n", stop, "\n}\n");
            }
            return code;
        }

        ast_t *value_var = single_loop_var(for_->vars);

        Text_t get_next = compile_iterator_call(iter_value_t, next_fn, EMPTY_TEXT);

        if (fn->ret->tag == OptionalType) {
            // Use an optional variable `cur` for each iteration step, which
            // will be checked for none
            code = Texts(code, compile_declaration(fn->ret, Text("cur")), ";\n");
            get_next = Texts("(cur=", get_next, ", !", check_none(fn->ret, Text("cur")), ")");
            if (value_var) {
                naked_body = Texts(
                    compile_declaration(Match(fn->ret, OptionalType)->type, Texts("_$", Match(value_var, Var)->name)),
                    " = ", optional_into_nonnone(fn->ret, Text("cur")), ";\n", naked_body);
            }
            if (for_->empty) {
                code = Texts(code, "if (", get_next,
                             ") {\n"
                             "\tdo{\n\t\t",
                             naked_body, "\t} while(", get_next,
                             ");\n"
                             "} else {\n\t",
                             compile_statement(env, for_->empty), "}", stop, "\n}\n");
            } else {
                code = Texts(code, "while(", get_next, ") {\n\t", naked_body, "}\n", stop, "\n}\n");
            }
        } else {
            if (value_var) {
                naked_body = Texts(compile_declaration(fn->ret, Texts("_$", Match(value_var, Var)->name)), " = ",
                                   get_next, ";\n", naked_body);
            } else {
                naked_body = Texts(get_next, ";\n", naked_body);
            }
            if (for_->empty)
                code_err(for_->empty, "This iteration loop will always have values, "
                                      "so this block will never run");
            code = Texts(code, "for (;;) {\n\t", naked_body, "}\n", stop, "\n}\n");
        }

        return code;
    }
    case TextType: {
        ast_t *index_var = for_->at;
        ast_t *value_var = single_loop_var(for_->vars);
        Text_t index = index_var ? compile(body_scope, index_var) : EMPTY_TEXT;
        Text_t value = value_var ? compile(body_scope, value_var) : EMPTY_TEXT;

        Text_t code =
            Texts("{\n"
                  "TextIter_t ",
                  value, "$state = NEW_TEXT_ITER_STATE(", compile_to_pointer_depth(env, iter, 0, false), ");\n");

        Text_t loop = Texts("for (int64_t ", value, "$i = 0; ", value, "$i < (int64_t)", value,
                            "$state.stack[0].text.length; ", value, "$i += 1) {\n");

        if (index.length > 0) {
            // 1-indexed, to match text cluster indexing:
            loop = Texts(loop, "Int64_t ", index, " = ", value, "$i + 1;\n");
        }

        if (value.length > 0) {
            loop = Texts(loop, "int32_t g = Text$get_grapheme_fast(&", value, "$state, ", value,
                         "$i);\n"
                         "Text_t ",
                         value, " = Text$from_grapheme(g);\n", naked_body, "}\n");
        } else {
            loop = Texts(loop, naked_body, "}\n");
        }

        if (for_->empty)
            loop = Texts("if (", value, "$state.stack[0].text.length > 0) {\n", loop, "\n} else ",
                         compile_statement(env, for_->empty));

        code = Texts(code, loop, stop, "}\n");
        return code;
    }
    default: code_err(iter, "Iteration is not implemented for type: ", type_to_text(iter_t));
    }
}

public
Text_t compile_for_loop(env_t *env, ast_t *ast) {
    Text_t cow_prelude = EMPTY_TEXT;
    env = cow_hoist_env(env, ast, &cow_prelude);
    Text_t code = compile_for_loop_impl(env, ast);
    if (cow_prelude.length > 0) code = Texts("{\n", cow_prelude, code, "\n}");
    return code;
}

public
Text_t compile_repeat(env_t *env, ast_t *ast) {
    Text_t cow_prelude = EMPTY_TEXT;
    env = cow_hoist_env(env, ast, &cow_prelude);
    ast_t *body = Match(ast, Repeat)->body;
    env_t *scope = fresh_scope(env);
    loop_ctx_t loop_ctx = (loop_ctx_t){
        .loop_name = "repeat",
        .deferred = scope->deferred,
        .next = env->loop_ctx,
    };
    scope->loop_ctx = &loop_ctx;
    Text_t body_code = compile_statement(scope, body);
    if (loop_ctx.skip_label.length > 0) body_code = Texts(body_code, "\n", loop_ctx.skip_label, ": continue;");
    Text_t loop = Texts("for (;;) {\n\t", body_code, "\n}");
    if (loop_ctx.stop_label.length > 0) loop = Texts(loop, "\n", loop_ctx.stop_label, ":;");
    if (cow_prelude.length > 0) loop = Texts("{\n", cow_prelude, loop, "\n}");
    return loop;
}

public
Text_t compile_while(env_t *env, ast_t *ast) {
    Text_t cow_prelude = EMPTY_TEXT;
    env = cow_hoist_env(env, ast, &cow_prelude);
    DeclareMatch(while_, ast, While);
    env_t *scope = fresh_scope(env);
    loop_ctx_t loop_ctx = (loop_ctx_t){
        .loop_name = "while",
        .deferred = scope->deferred,
        .next = env->loop_ctx,
    };
    scope->loop_ctx = &loop_ctx;
    Text_t body = compile_statement(scope, while_->body);
    if (loop_ctx.skip_label.length > 0) body = Texts(body, "\n", loop_ctx.skip_label, ": continue;");
    Text_t loop =
        Texts("while (", while_->condition ? compile(scope, while_->condition) : Text("yes"), ") {\n\t", body, "\n}");
    if (loop_ctx.stop_label.length > 0) loop = Texts(loop, "\n", loop_ctx.stop_label, ":;");
    if (cow_prelude.length > 0) loop = Texts("{\n", cow_prelude, loop, "\n}");
    return loop;
}

public
Text_t compile_continue(env_t *env, ast_t *ast) {
    const char *target = Match(ast, Continue)->target;
    for (loop_ctx_t *ctx = env->loop_ctx; ctx; ctx = ctx->next) {
        bool matched = !target || strcmp(target, ctx->loop_name) == 0;
        for (ast_list_t *var = ctx->loop_vars; var && !matched; var = var ? var->next : NULL) {
            ast_t *var_ast = var->ast->tag == StackReference ? Match(var->ast, StackReference)->value : var->ast;
            matched = (strcmp(target, Match(var_ast, Var)->name) == 0);
        }

        if (matched) {
            if (ctx->skip_label.length == 0) {
                static int64_t skip_label_count = 1;
                ctx->skip_label = Texts("skip_", skip_label_count);
                ++skip_label_count;
            }
            Text_t code = EMPTY_TEXT;
            for (deferral_t *deferred = env->deferred; deferred && deferred != ctx->deferred; deferred = deferred->next)
                code = Texts(code, compile_statement(deferred->defer_env, deferred->block));
            if (code.length > 0) return Texts("{\n", code, "goto ", ctx->skip_label, ";\n}\n");
            else return Texts("goto ", ctx->skip_label, ";");
        }
    }
    if (env->loop_ctx) code_err(ast, "This is not inside any loop");
    else if (target) code_err(ast, "No loop target named '", target, "' was found");
    else return Text("continue;");
}

public
Text_t compile_break(env_t *env, ast_t *ast) {
    const char *target = Match(ast, Break)->target;
    for (loop_ctx_t *ctx = env->loop_ctx; ctx; ctx = ctx->next) {
        bool matched = !target || strcmp(target, ctx->loop_name) == 0;
        for (ast_list_t *var = ctx->loop_vars; var && !matched; var = var ? var->next : var) {
            ast_t *var_ast = var->ast->tag == StackReference ? Match(var->ast, StackReference)->value : var->ast;
            matched = (strcmp(target, Match(var_ast, Var)->name) == 0);
        }

        if (matched) {
            if (ctx->stop_label.length == 0) {
                static int64_t stop_label_count = 1;
                ctx->stop_label = Texts("stop_", stop_label_count);
                ++stop_label_count;
            }
            Text_t code = EMPTY_TEXT;
            for (deferral_t *deferred = env->deferred; deferred && deferred != ctx->deferred; deferred = deferred->next)
                code = Texts(code, compile_statement(deferred->defer_env, deferred->block));
            if (code.length > 0) return Texts("{\n", code, "goto ", ctx->stop_label, ";\n}\n");
            else return Texts("goto ", ctx->stop_label, ";");
        }
    }
    if (env->loop_ctx) code_err(ast, "This is not inside any loop");
    else if (target) code_err(ast, "No loop target named '", target, "' was found");
    else return Text("break;");
}