llm
Download
495 lines 29.8 KiBCModified 2026-08-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
// This file defines how to compile lists

#include <gc.h>
#include <glob.h>
#include <gmp.h>
#include <math.h>
#include <uninorm.h>

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

// Is `ast` a literal whose value for `item_type` is all-bits-zero? (So a
// comprehension of it can be produced by a zeroed allocation with no fill
// loop.) Only types whose zero value is genuinely all-bits-zero qualify:
// bytes, fixed-width ints (NOT bignum Int, whose 0 is the tagged value 1),
// Nums (+0.0 is all-zero bits; -0.0 is not), and Bools. Unwraps a numeric
// constructor like `Byte(0)` / `Int64(0)` to its literal argument.
static bool is_zero_valued_literal(env_t *env, ast_t *ast, type_t *item_type) {
    if (ast->tag == FunctionCall) {
        DeclareMatch(call, ast, FunctionCall);
        type_t *fn_t = get_type(env, call->fn);
        // Forward through a numeric cast only, since `Byte(0)`/`Int64(0)`/`Num(0.0)`
        // really is all-bits-zero when its argument is. A struct/enum
        // constructor like `Foo(0)` is NOT (its other fields may default to
        // nonzero, and its layout isn't its argument's), so it never qualifies.
        if (fn_t->tag == TypeInfoType && call->args && !call->args->next && !call->args->name) {
            type_t *ctor_t = Match(fn_t, TypeInfoType)->type;
            if (ctor_t->tag == ByteType || ctor_t->tag == IntType || ctor_t->tag == FloatType
                || ctor_t->tag == BoolType)
                return is_zero_valued_literal(env, call->args->value, ctor_t);
        }
        return false;
    }
    if (ast->tag == Int) {
        if (item_type->tag != ByteType && item_type->tag != IntType && item_type->tag != FloatType) return false;
        // Zero always fits the tagged small form (a bignum is never zero):
        Int_t v = Match(ast, Int)->i;
        return (v.small & 1L) && ((v.small >> 2L) == 0);
    }
    if (ast->tag == Num) {
        if (item_type->tag != FloatType) return false;
        double n = num_literal_double(ast);
        return n == 0.0 && !signbit(n);
    }
    if (ast->tag == Bool) return item_type->tag == BoolType && Match(ast, Bool)->b == false;
    return false;
}

static ast_t *add_to_list_comprehension(ast_t *item, ast_t *subject) {
    // Append at the end. `insert` at the default index I(0) hits an inlined
    // fast path in List$insert_value (a bounds-free store, no function call),
    // which GCC selects at compile time here since the index is constant.
    return WrapAST(item, MethodCall, .name = "insert", .self = subject, .args = new (arg_ast_t, .value = item));
}

public
Text_t compile_typed_list(env_t *env, ast_t *ast, type_t *list_type) {
    DeclareMatch(list, ast, List);
    if (!list->items) return Text("EMPTY_LIST");

    type_t *item_type = Match(list_type, ListType)->item_type;
    if (item_type == NULL) code_err(ast, "I couldn't figure out what item type goes into this list");

    int64_t n = 0;
    for (ast_list_t *item = list->items; item; item = item->next) {
        ++n;
        if (item->ast->tag == Comprehension) goto list_comprehension;
    }

    {
        env_t *scope = item_type->tag == EnumType ? with_enum_scope(env, item_type) : env;
        if (is_incomplete_type(item_type)) code_err(ast, "This list's type can't be inferred!");
        bool all_const = true;
        for (ast_list_t *item = list->items; item; item = item->next) {
            if (!is_constant(scope, item->ast, item_type)) {
                all_const = false;
                break;
            }
        }
        if (all_const) {
            // Every item is a compile-time constant: hoist the data into a
            // module-level `static const` array (program lifetime, .rodata) and
            // point a `ConstList` at it. The result is a constant List_t
            // *expression* (no allocation, no copy, valid even as a
            // file-scope initializer) that never dangles wherever it escapes to. Mutation
            // is guarded by copy-on-write (see ConstList in stdlib/lists.h).
            static int64_t const_list_num = 0;
            Text_t data_name = Text$from_str(String("list_literal$", ++const_list_num));
            Text_t items = EMPTY_TEXT;
            for (ast_list_t *item = list->items; item; item = item->next)
                items = Texts(items, items.length > 0 ? Text(", ") : EMPTY_TEXT,
                              compile_to_type(scope, item->ast, item_type));
            env->code->constants = Texts(env->code->constants, "static const ", compile_type(item_type), " ", data_name,
                                         "[", n, "] = {", items, "};\n");
            return Texts("ConstList(", compile_type(item_type), ", ", n, ", ", data_name, ")");
        }
        Text_t code = Texts("TypedListN(", compile_type(item_type), ", ", n);
        for (ast_list_t *item = list->items; item; item = item->next) {
            code = Texts(code, ", ", compile_to_type(scope, item->ast, item_type));
        }
        return Texts(code, ")");
    }

list_comprehension: {
    env_t *scope = item_type->tag == EnumType ? with_enum_scope(env, item_type) : fresh_scope(env);
    static int64_t comp_num = 1;
    int64_t this_comp = comp_num++;
    const char *comprehension_name = String("list$", this_comp);
    ast_t *comprehension_var =
        LiteralCode(Texts("&", comprehension_name), .type = Type(PointerType, .pointed = list_type, .is_stack = true));
    Closure_t comp_action = {.fn = add_to_list_comprehension, .userdata = comprehension_var};
    scope->comprehension_action = &comp_action;

    // Fast path: a single comprehension `[expr for x in SOURCE (if cond)]`
    // whose element count is bounded up front. The result can never have more
    // elements than SOURCE yields (a filter only removes), so pre-allocate
    // that capacity and fill it with the inlined List$insert_value append --
    // no growth reallocation, no repeated GC scans of a growing buffer.
    // SOURCE is hoisted into a temp (evaluated once, since it may not be
    // idempotent, e.g. `xs.reversed()` or `a*b`), and the loop iterates that
    // temp. Bounded sources: a list (capacity = its length) or an integer
    // count `for x in n` (capacity = n; `with_capacity` clamps n <= 0 to
    // empty, and a truncated bignum just means a non-pre-sized fallback, never
    // a wrong result).
    if (list->items && !list->items->next && list->items->ast->tag == Comprehension) {
        DeclareMatch(comp, list->items->ast, Comprehension);
        if (comp->iters && !comp->iters->next && comp->expr->tag != Comprehension) {
            type_t *src_t = value_type(get_type(env, comp->iters->ast));
            Text_t capacity = EMPTY_TEXT;
            const char *src_name = String("comp_src$", this_comp);
            if (src_t->tag == ListType) capacity = Texts(src_name, ".length");
            else if (src_t->tag == BigIntType) capacity = Texts("Int64$from_int(", src_name, ", yes)");
            else if (src_t->tag == IntType) capacity = Texts("(int64_t)(", src_name, ")");
            if (capacity.length > 0) {
                Text_t src_code = compile_to_pointer_depth(env, comp->iters->ast, 0, false);
                Text_t zero = Texts("(", compile_type(item_type), "){0}");
                // Even faster: with no filter and a constant all-bits-zero body
                // (`[Byte(0) for _ in n]`), every slot is zero, so skip the loop
                // entirely and just allocate a zeroed block of `capacity` items.
                if (!comp->filter && is_zero_valued_literal(env, comp->expr, item_type))
                    return Texts("({ ", compile_type(src_t), " ", src_name, " = ", src_code, ";\n", "List$zeroed(",
                                 capacity, ", ", zero, "); })");
                ast_t *src_ref = LiteralCode(Texts(src_name), .type = src_t);
                ast_t *body = add_to_list_comprehension(comp->expr, comprehension_var);
                if (comp->filter) body = WrapAST(comp->expr, If, .condition = comp->filter, .body = body);
                ast_t *loop = WrapAST(list->items->ast, For, .vars = comp->vars, .at = comp->at,
                                      .iters = new (ast_list_t, .ast = src_ref), .body = body);
                return Texts("({ ", compile_type(src_t), " ", src_name, " = ", src_code, "; List_t ",
                             comprehension_name, " = List$with_capacity(", capacity, ", ", zero, ");\n",
                             compile_statement(scope, loop), " ", comprehension_name, "; })");
            }
        }
    }

    Text_t code = Texts("({ List_t ", comprehension_name, " = EMPTY_LIST;");
    // set_binding(scope, comprehension_name, list_type, comprehension_name);
    for (ast_list_t *item = list->items; item; item = item->next) {
        if (item->ast->tag == Comprehension) code = Texts(code, "\n", compile_statement(scope, item->ast));
        else code = Texts(code, compile_statement(scope, add_to_list_comprehension(item->ast, comprehension_var)));
    }
    code = Texts(code, " ", comprehension_name, "; })");
    return code;
}
}

// The generic List runtime calls predicate/comparison closures with the item(s)
// passed BY POINTER (`fn(void *elem, void *userdata)`), because it's compiled
// once and can't know an arbitrary item type's by-value C ABI. To let users
// write natural by-value closures (`func(i:Int) i == 3`), we emit a tiny
// per-item-type adapter whose own signature is that by-pointer one, and which
// dereferences and calls the user's by-value closure. `*(T*)e` is a value copy,
// so the closure can't mutate the list. Deduped per (shape, item type); mirrors
// the closure-shim mechanism in promote() (compile/promotions.c).
static Text_t byval_adapter_name(env_t *env, type_t *item_t, bool is_compare) {
    Text_t item_type_code = compile_type(item_t);
    const char *key = String(is_compare ? "cmp:" : "pred:", Text$as_c_string(item_type_code));
    const char *existing = Table$str_get(env->code->byval_adapters, key);
    if (existing) return Text$from_str(existing);

    Text_t name = Texts("byval$", is_compare ? Text("cmp") : Text("pred"), "$",
                        (int64_t)Table$length(env->code->byval_adapters) + 1);
    Text_t def;
    if (is_compare)
        def = Texts("static int32_t ", name, "(void *a, void *b, void *ud) { Closure_t *u = ud; return ",
                    "((int32_t(*)(", item_type_code, ", ", item_type_code, ", void*))u->fn)(*(", item_type_code,
                    "*)a, *(", item_type_code, "*)b, u->userdata); }\n");
    else
        def = Texts("static bool ", name, "(void *e, void *ud) { Closure_t *u = ud; return ", "((bool(*)(",
                    item_type_code, ", void*))u->fn)(*(", item_type_code, "*)e, u->userdata); }\n");
    env->code->staticdefs = Texts(env->code->staticdefs, def);
    Table$str_set(&env->code->byval_adapters, key, Text$as_c_string(name));
    return name;
}

// Compile a single user-supplied closure argument (matched from `closure_args`
// against a one-arg spec named `arg_name` with the by-value item type) and wrap
// it in a by-value adapter. Sets *decl to `Closure_t <tmp> = <user closure>;`
// and returns `(Closure_t){(void*)<adapter>, &<tmp>}`. The caller must splice
// *decl and the returned expression into a single statement-expression that
// encloses the whole runtime call, so <tmp> outlives the call that reads it (no
// heap allocation). Safe because no List method retains the closure past return.
// Can this closure argument take the shim-free fast path? Only a single,
// directly-passed lambda whose parameters are all explicitly typed as the item
// type (right arity) and whose return type already matches the expected one. In
// that case the lambda's own C body can be compiled with by-pointer arguments
// (see compile_lambda_pointer_args), so it plugs straight into the runtime's
// by-pointer call with no adapter and no boxed closure. Anything else (an
// inferred-type lambda, a named function, a closure variable, a mismatched
// signature) falls back to the adapter path, which also produces the right type
// error when the signature is actually wrong.
static bool byval_lambda_fastpath(env_t *env, arg_ast_t *closure_args, type_t *item_t, bool is_compare,
                                  const char *arg_name) {
    if (!closure_args || closure_args->next) return false;
    if (closure_args->name && !streq(closure_args->name, arg_name)) return false;
    ast_t *lambda_ast = closure_args->value;
    if (!lambda_ast || lambda_ast->tag != Lambda) return false;
    DeclareMatch(lambda, lambda_ast, Lambda);
    int arity = 0;
    for (arg_ast_t *a = lambda->args; a; a = a->next) {
        if (!a->type) return false; // inferred param type: let the adapter path fill it in
        if (!type_eq(parse_type_ast(env, a->type), item_t)) return false;
        arity += 1;
    }
    if (arity != (is_compare ? 2 : 1)) return false;
    type_t *ret = get_function_return_type(env, lambda_ast);
    return type_eq(ret, is_compare ? Type(IntType, .bits = TYPE_IBITS32) : Type(BoolType));
}

static Text_t compile_byval_closure(env_t *env, ast_t *ast, type_t *item_t, bool is_compare, const char *arg_name,
                                    arg_ast_t *closure_args, Text_t *decl) {
    if (byval_lambda_fastpath(env, closure_args, item_t, is_compare, arg_name)) {
        *decl = EMPTY_TEXT;
        return compile_lambda_pointer_args(env, closure_args->value);
    }
    type_t *fn_t = is_compare ? NewFunctionType(Type(IntType, .bits = TYPE_IBITS32), {.name = "x", .type = item_t},
                                                {.name = "y", .type = item_t})
                              : NewFunctionType(Type(BoolType), {.name = "item", .type = item_t});
    arg_t *arg_spec = new (arg_t, .name = arg_name, .type = Type(ClosureType, .fn = fn_t));
    Text_t user_closure = compile_arguments(env, ast, arg_spec, closure_args);
    Text_t tmp = Texts("_byval$", (int64_t)(ast->start - ast->file->text));
    *decl = Texts("Closure_t ", tmp, " = ", user_closure, "; ");
    // Extra parens: heap_push_value/heap_pop_value are function-like macros, and
    // the braced Closure literal contains a comma that would otherwise be split.
    return Texts("((Closure_t){(void*)", byval_adapter_name(env, item_t, is_compare), ", &", tmp, "})");
}

public
Text_t compile_list_method_call(env_t *env, ast_t *ast) {
    DeclareMatch(call, ast, MethodCall);
    type_t *self_t = get_type(env, call->self);

    int64_t pointer_depth = 0;
    type_t *self_value_t = self_t;
    for (; self_value_t->tag == PointerType; self_value_t = Match(self_value_t, PointerType)->pointed)
        pointer_depth += 1;

    Text_t self = compile(env, call->self);
#define EXPECT_POINTER()                                                                                               \
    do {                                                                                                               \
        if (pointer_depth < 1) code_err(call->self, "I expected a list pointer here, not a list value");               \
        else if (pointer_depth > 1) code_err(call->self, "I expected a list pointer here, not a nested list pointer"); \
    } while (0)
    type_t *item_t = Match(self_value_t, ListType)->item_type;
    Text_t padded_item_size = Texts("sizeof(", compile_type(item_t), ")");

    if (streq(call->name, "insert")) {
        EXPECT_POINTER();
        arg_t *arg_spec =
            new (arg_t, .name = "item", .type = item_t,
                 .next = new (arg_t, .name = "at", .type = INT_TYPE, .default_val = FakeAST(Int, .i = I_small(0))));
        return Texts("List$insert_value(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     padded_item_size, ")");
    } else if (streq(call->name, "insert_all")) {
        EXPECT_POINTER();
        arg_t *arg_spec =
            new (arg_t, .name = "items", .type = self_value_t,
                 .next = new (arg_t, .name = "at", .type = INT_TYPE, .default_val = FakeAST(Int, .i = I_small(0))));
        return Texts("List$insert_all(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     padded_item_size, ")");
    } else if (streq(call->name, "remove_at")) {
        EXPECT_POINTER();
        arg_t *arg_spec =
            new (arg_t, .name = "index", .type = INT_TYPE, .default_val = FakeAST(Int, .i = I_small(-1)),
                 .next = new (arg_t, .name = "count", .type = INT_TYPE, .default_val = FakeAST(Int, .i = I_small(1))));
        return Texts("List$remove_at(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     padded_item_size, ")");
    } else if (streq(call->name, "remove_item")) {
        EXPECT_POINTER();
        arg_t *arg_spec = new (
            arg_t, .name = "item", .type = item_t,
            .next = new (arg_t, .name = "max_count", .type = INT_TYPE, .default_val = FakeAST(Int, .i = I_small(-1))));
        return Texts("List$remove_item_value(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     compile_type_info(self_value_t), ")");
    } else if (streq(call->name, "has")) {
        self = compile_to_pointer_depth(env, call->self, 0, false);
        arg_t *arg_spec = new (arg_t, .name = "item", .type = item_t);
        return Texts("List$has_value(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     compile_type_info(self_value_t), ")");
    } else if (streq(call->name, "sample")) {
        type_t *random_num_type = parse_type_string(env, "func(->Num)?");
        self = compile_to_pointer_depth(env, call->self, 0, false);
        arg_t *arg_spec = new (
            arg_t, .name = "count", .type = INT_TYPE,
            .next = new (arg_t, .name = "weights", .type = Type(ListType, .item_type = Type(NumType)),
                         .default_val = FakeAST(None),
                         .next = new (arg_t, .name = "random", .type = random_num_type, .default_val = FakeAST(None))));
        return Texts("List$sample(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     padded_item_size, ")");
    } else if (streq(call->name, "shuffle")) {
        type_t *random_int64_type = parse_type_string(env, "func(min,max:Int64->Int64)?");
        EXPECT_POINTER();
        arg_t *arg_spec = new (arg_t, .name = "random", .type = random_int64_type, .default_val = FakeAST(None));
        return Texts("List$shuffle(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     padded_item_size, ")");
    } else if (streq(call->name, "shuffled")) {
        type_t *random_int64_type = parse_type_string(env, "func(min,max:Int64->Int64)?");
        self = compile_to_pointer_depth(env, call->self, 0, false);
        arg_t *arg_spec = new (arg_t, .name = "random", .type = random_int64_type, .default_val = FakeAST(None));
        return Texts("List$shuffled(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     padded_item_size, ")");
    } else if (streq(call->name, "random")) {
        type_t *random_int64_type = parse_type_string(env, "func(min,max:Int64->Int64)?");
        self = compile_to_pointer_depth(env, call->self, 0, false);
        arg_t *arg_spec = new (arg_t, .name = "random", .type = random_int64_type, .default_val = FakeAST(None));
        return Texts("List$random_value(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     compile_type(item_t), ", _, ", promote_to_optional(item_t, Text("_")), ", ", compile_none(item_t),
                     ")");
    } else if (streq(call->name, "swap")) {
        // Compiled inline (List_swap in stdlib/lists.h): two bounds checks
        // and one CoW guard per swap, no function call. Indexes compile to
        // native Int64 exactly like indexed assignment does.
        EXPECT_POINTER();
        arg_ast_t *i_arg = call->args;
        if (i_arg == NULL || i_arg->next == NULL || i_arg->next->next != NULL)
            code_err(ast, "swap() takes exactly two index arguments");
        if ((i_arg->name && !streq(i_arg->name, "i")) || (i_arg->next->name && !streq(i_arg->next->name, "j")))
            code_err(ast, "swap()'s arguments are named `i` and `j`");
        Text_t index_codes[2];
        arg_ast_t *arg = i_arg;
        for (int n = 0; n < 2; n++, arg = arg->next) {
            type_t *arg_t = get_type(env, arg->value);
            if (is_int_literal(arg->value, NULL))
                index_codes[n] = compile_int_to_type(env, arg->value, Type(IntType, .bits = TYPE_IBITS64));
            else if (arg_t->tag == BigIntType)
                index_codes[n] = Texts("Int64$from_int(", compile(env, arg->value), ", no)");
            else if (is_int_type(arg_t)) index_codes[n] = Texts("(Int64_t)(", compile(env, arg->value), ")");
            else code_err(arg->value, "swap() indexes must be integers, not ", type_to_text(arg_t));
        }
        // If an enclosing loop hoisted this list's header and CoW guard (see
        // cow_hoist_env in loops.c), swap through the hoisted locals: no
        // per-swap CoW check, and the header stays in registers.
        if (is_cow_hoisted(env, call->self))
            return Texts("List_swap_hoisted(", compile_type(item_t), ", ", cow_hoisted_local(call->self, "data"), ", ",
                         cow_hoisted_local(call->self, "stride"), ", ", cow_hoisted_local(call->self, "length"), ", ",
                         index_codes[0], ", ", index_codes[1], ", ", (int64_t)(ast->start - ast->file->text), ", ",
                         (int64_t)(ast->end - ast->file->text), ")");
        return Texts("List_swap(", compile_type(item_t), ", ", self, ", ", index_codes[0], ", ", index_codes[1], ", ",
                     (int64_t)(ast->start - ast->file->text), ", ", (int64_t)(ast->end - ast->file->text), ")");
    } else if (streq(call->name, "sort") || streq(call->name, "sorted")) {
        if (streq(call->name, "sort")) EXPECT_POINTER();
        else self = compile_to_pointer_depth(env, call->self, 0, false);
        if (call->args) {
            Text_t decl, cmp = compile_byval_closure(env, ast, item_t, true, "by", call->args, &decl);
            return Texts("({ ", decl, "List$", call->name, "(", self, ", ", cmp, ", ", padded_item_size, "); })");
        }
        Text_t comparison =
            Texts("((Closure_t){.fn=generic_compare, .userdata=(void*)", compile_type_info(item_t), "})");
        return Texts("List$", call->name, "(", self, ", ", comparison, ", ", padded_item_size, ")");
    } else if (streq(call->name, "heapify")) {
        EXPECT_POINTER();
        if (call->args) {
            Text_t decl, cmp = compile_byval_closure(env, ast, item_t, true, "by", call->args, &decl);
            return Texts("({ ", decl, "List$heapify(", self, ", ", cmp, ", ", padded_item_size, "); })");
        }
        Text_t comparison =
            Texts("((Closure_t){.fn=generic_compare, .userdata=(void*)", compile_type_info(item_t), "})");
        return Texts("List$heapify(", self, ", ", comparison, ", ", padded_item_size, ")");
    } else if (streq(call->name, "heap_push")) {
        EXPECT_POINTER();
        // Split the (required) `item` value from the optional `by` comparison so
        // the comparison can be adapted to by-value independently. Matching
        // mirrors compile_arguments: a keyword arg claims its slot; the rest fill
        // positionally in order.
        arg_ast_t *item_arg = NULL, *by_arg = NULL, *positional[2] = {NULL, NULL};
        int np = 0;
        for (arg_ast_t *a = call->args; a; a = a->next) {
            if (a->name) {
                if (streq(a->name, "item")) item_arg = a;
                else if (streq(a->name, "by")) by_arg = a;
                else code_err(a->value, "heap_push() has no argument named '", a->name, "'");
            } else if (np < 2) {
                positional[np++] = a;
            } else {
                code_err(a->value, "heap_push() takes at most two arguments");
            }
        }
        int p = 0;
        if (!item_arg) item_arg = positional[p++];
        if (!by_arg && p < np) by_arg = positional[p++];
        if (!item_arg) code_err(ast, "heap_push() requires an item to push");
        if (p < np)
            code_err(positional[p]->value, "This positional argument to heap_push() has no slot to fill "
                                           "(item and comparison are already provided)");

        arg_t *item_spec = new (arg_t, .name = "item", .type = item_t);
        Text_t item_code = compile_arguments(env, ast, item_spec, new (arg_ast_t, .value = item_arg->value));
        if (by_arg) {
            Text_t decl, cmp = compile_byval_closure(env, ast, item_t, true, "by",
                                                     new (arg_ast_t, .value = by_arg->value), &decl);
            return Texts("({ ", decl, "List$heap_push_value(", self, ", ", item_code, ", ", cmp, ", ", padded_item_size,
                         "); })");
        }
        Text_t default_cmp =
            Texts("((Closure_t){.fn=generic_compare, .userdata=(void*)", compile_type_info(item_t), "})");
        return Texts("List$heap_push_value(", self, ", ", item_code, ", ", default_cmp, ", ", padded_item_size, ")");
    } else if (streq(call->name, "heap_pop")) {
        EXPECT_POINTER();
        if (call->args) {
            Text_t decl, cmp = compile_byval_closure(env, ast, item_t, true, "by", call->args, &decl);
            return Texts("({ ", decl, "List$heap_pop_value(", self, ", ", cmp, ", ", compile_type(item_t), ", _, ",
                         promote_to_optional(item_t, Text("_")), ", ", compile_none(item_t), "); })");
        }
        Text_t cmp = Texts("((Closure_t){.fn=generic_compare, .userdata=(void*)", compile_type_info(item_t), "})");
        return Texts("List$heap_pop_value(", self, ", ", cmp, ", ", compile_type(item_t), ", _, ",
                     promote_to_optional(item_t, Text("_")), ", ", compile_none(item_t), ")");
    } else if (streq(call->name, "binary_search")) {
        self = compile_to_pointer_depth(env, call->self, 0, call->args != NULL);
        Text_t decl, pred = compile_byval_closure(env, ast, item_t, false, "predicate", call->args, &decl);
        return Texts("({ ", decl, "List$binary_search(", self, ", ", pred, "); })");
    } else if (streq(call->name, "clear")) {
        EXPECT_POINTER();
        (void)compile_arguments(env, ast, NULL, call->args);
        return Texts("List$clear(", self, ")");
    } else if (streq(call->name, "find")) {
        self = compile_to_pointer_depth(env, call->self, 0, false);
        arg_t *arg_spec = new (arg_t, .name = "item", .type = item_t);
        return Texts("List$find_value(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ",
                     compile_type_info(self_value_t), ")");
    } else if (streq(call->name, "where")) {
        self = compile_to_pointer_depth(env, call->self, 0, call->args != NULL);
        Text_t decl, pred = compile_byval_closure(env, ast, item_t, false, "predicate", call->args, &decl);
        return Texts("({ ", decl, "List$first(", self, ", ", pred, "); })");
    } else if (streq(call->name, "from")) {
        self = compile_to_pointer_depth(env, call->self, 0, true);
        arg_t *arg_spec = new (arg_t, .name = "first", .type = INT_TYPE);
        return Texts("List$from(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ")");
    } else if (streq(call->name, "to")) {
        self = compile_to_pointer_depth(env, call->self, 0, true);
        arg_t *arg_spec = new (arg_t, .name = "last", .type = INT_TYPE);
        return Texts("List$to(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ")");
    } else if (streq(call->name, "slice")) {
        self = compile_to_pointer_depth(env, call->self, 0, true);
        arg_t *arg_spec =
            new (arg_t, .name = "first", .type = INT_TYPE, .next = new (arg_t, .name = "last", .type = INT_TYPE));
        return Texts("List$slice(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ")");
    } else if (streq(call->name, "by")) {
        self = compile_to_pointer_depth(env, call->self, 0, true);
        arg_t *arg_spec = new (arg_t, .name = "stride", .type = INT_TYPE);
        return Texts("List$by(", self, ", ", compile_arguments(env, ast, arg_spec, call->args), ", ", padded_item_size,
                     ")");
    } else if (streq(call->name, "reversed")) {
        self = compile_to_pointer_depth(env, call->self, 0, true);
        (void)compile_arguments(env, ast, NULL, call->args);
        return Texts("List$reversed(", self, ", ", padded_item_size, ")");
    } else if (streq(call->name, "pairs")) {
        // Iterator over each unordered pair of distinct elements (i < j). The
        // incref gives it snapshot semantics: mutating the list after making
        // the iterator copies first, like other buffer-sharing methods.
        self = compile_to_pointer_depth(env, call->self, 0, true);
        (void)compile_arguments(env, ast, NULL, call->args);
        return Texts("List$pairs(", self, ", ", padded_item_size, ")");
    } else if (streq(call->name, "unique")) {
        self = compile_to_pointer_depth(env, call->self, 0, false);
        (void)compile_arguments(env, ast, NULL, call->args);
        return Texts("Table$from_entries(", self, ", Table$info(", compile_type_info(item_t), ", &Present$$info))");
    } else if (streq(call->name, "pop")) {
        EXPECT_POINTER();
        arg_t *arg_spec = new (arg_t, .name = "index", .type = INT_TYPE, .default_val = FakeAST(Int, I_small(-1)));
        Text_t index = compile_arguments(env, ast, arg_spec, call->args);
        return Texts("List$pop(", self, ", ", index, ", ", compile_type(item_t), ", _, ",
                     promote_to_optional(item_t, Text("_")), ", ", compile_none(item_t), ")");
    } else if (streq(call->name, "counts")) {
        self = compile_to_pointer_depth(env, call->self, 0, false);
        (void)compile_arguments(env, ast, NULL, call->args);
        return Texts("List$counts(", self, ", ", compile_type_info(self_value_t), ")");
    } else {
        code_err(ast, "There is no '", call->name, "' method for lists");
    }
}