← На главную

Переписали замыкания: lower_to_ir после скоупов, помог Claude Fable 5

18.07.2026 21:28 · hackernews

В компиляторе переписали механизм замыканий (closures) — ту часть, что опускает код с замыканиями в промежуточное представление. Теперь lower_to_ir по умолчанию работает с деревом уже после разрешения областей видимости (resolve_scopes), а не после обработки замыканий. Локальные функции порождают настоящие регионы closure с параметрами в виде отложенных регионов (deferred-region args). Эмиттер перестал сам решать, что захватывать: изменяемые локальные переменные получают план cell_shared (создаётся новая упаковка на каждой итерации цикла, если переменная объявлена внутри цикла), переменные, которым присвоили значение ровно один раз и которые доминируют, остаются обычными SSA-значениями, а аргументы, которые только читаются в замыкании, захватываются напрямую как region_arg.

Дальше фиксированная точка продвижения (promote_capture_cells!) решает, останется значение в ячейке или станет плоским SSA. Анализ покрывает вложенные лямбды в методах как условные подпути. Фреймы входа/выхода получают собственные обработчики, break-target, sparam и return-type-области. Служебные переменные (SSA desugaring temps) не попадают в locals_capt — они привязаны к фрейму, в котором их увидел сканер.

Материализация (в materialize.jl) превращает остаточные closure-регионы обратно в рантайм-замыкания. Множество захваченного — это closure_environment. Тип берётся из готовой механики eval_closure_type: те, кто захватил значение без ячейки, получают его как type-параметр, ячейки превращаются в Core.Box или Base.RefValue{T} (если тип удалось доказать). Отложенный регион выделяется в отдельный метод IR, где регион 1 — это #self# + параметры, захваты читаются через getfield, ячейки — через операции контейнера, вложенные замыкания обрабатываются рекурсивно. В итоге операция подменяется на apply_type + new, а на новом типе вешается метод-трамплин, интерпретирующий извлечённый IR — так инстансы можно вызывать, и дифференциалы (differentials) пересекают границу вызова внутри UnifiedIR без алиасинга с нативными типами.

Уцелевшие ячейки в фрейме опускаются в те же контейнеры, что и раньше, а при использовании неопределённой переменной выбрасывается UndefVarError с правильным именем. promote_capture_cells! научился принимать проверки cell_isdefined внутри отложенного региона: если анализатор смог доказать определённость в точке создания, а при (a)+(b) это эквивалентно определённости в точке вызова, то защитный код (guard) перезаписывается в true. Для переменных, которые могут быть не определены, проверки и контейнер остаются.

Неподдерживаемые случаи версии 1 (много-методы, kwargs, where, varargs, объявленные типы результата, opaque closures, captured_local, подписи, ссылающиеся на локалы, захват enclosing-sparam) отваливаются на уровень каждого top-level statement обратно на старый путь convert_closures — с громким предупреждением о потере точности. Вся существующая кодовая база теперь проходит через новый регионный путь (параметр REGION=15/EAGER=0). К нему прилагаются тесты на дифференциалы: joins, try joins, счётчики, типизированные контейнеры, свежие переменные на каждой итерации цикла, многократные вызовы, рекурсия через Boxд самозахват (как fallback), вложенные замыкания и обращение к неопределённой переменной. Есть и тесты, проверяющие, что fallback действительно срабатывает в сложных случаях.

Изменение соавторствовано с Claude Fable 5 из Anthropic.

Читать оригинал →