lowerIrPrototypeToLuaBytecodePrototype function

LuaBytecodePrototype lowerIrPrototypeToLuaBytecodePrototype(
  1. LualikeIrPrototype prototype, {
  2. required String sourceName,
  3. bool isMainPrototype = false,
})

Lowers one finalized IR prototype to a Lua bytecode prototype.

Only remaps finalized IR instructions and metadata into bytecode fields. Do not add new analyses or heuristics here — put them in IR/SSA instead.

When isMainPrototype is true, LualikeIrPrototype.lineDefined is forced to 0 regardless of IR source lines (required for correct main-chunk debug behavior). The main IR prototype must also declare _ENV as upvalue 0 with inStack=1 and index=0; lowering validates this closure shape rather than inventing missing metadata.

Implementation

LuaBytecodePrototype lowerIrPrototypeToLuaBytecodePrototype(
  LualikeIrPrototype prototype, {
  required String sourceName,
  bool isMainPrototype = false,
}) {
  final debugInfo = prototype.debugInfo;
  final tempBase = prototype.registerCount;
  final loweredInstructions = _lowerInstructions(
    prototype.instructions,
    tempBase: tempBase,
  );
  final instructions = loweredInstructions.instructions;
  final pcMap = loweredInstructions.pcMap;
  final constants = List<LuaBytecodeConstant>.unmodifiable(
    prototype.constants.map(_lowerConstant),
  );
  final loweredUpvalueNames = List<String?>.from(
    debugInfo?.upvalueNames ?? const <String>[],
    growable: true,
  );
  final loweredUpvalues = List<LuaBytecodeUpvalueDescriptor>.generate(
    prototype.upvalueDescriptors.length,
    (index) => _lowerUpvalueDescriptor(
      prototype.upvalueDescriptors[index],
      name: index < loweredUpvalueNames.length
          ? loweredUpvalueNames[index]
          : null,
    ),
    growable: true,
  );
  if (isMainPrototype) {
    _validateRootEnvironmentUpvalue(prototype);
  }
  final upvalues = List<LuaBytecodeUpvalueDescriptor>.unmodifiable(
    loweredUpvalues,
  );
  final children = List<LuaBytecodePrototype>.unmodifiable(
    prototype.prototypes.map(
      (child) => lowerIrPrototypeToLuaBytecodePrototype(
        child,
        sourceName: child.debugInfo?.absoluteSourcePath ?? sourceName,
      ),
    ),
  );
  final debugLines = _lowerDebugLines(
    debugInfo?.lineInfo,
    instructions.length,
    pcMap,
  );
  // Keep register + remapped PCs. Serialize drops register; parse re-infers.
  final locals = List<LuaBytecodeLocalVariableDebugInfo>.unmodifiable(
    (debugInfo?.localNames ?? const <LocalDebugEntry>[]).map(
      (entry) => LuaBytecodeLocalVariableDebugInfo(
        name: entry.name,
        startPc: _remapPc(entry.startPc, pcMap),
        endPc: _remapPc(entry.endPc, pcMap),
        register: entry.register,
      ),
    ),
  );
  final upvalueNames = List<String?>.unmodifiable(loweredUpvalueNames);

  return LuaBytecodePrototype(
    // Official Lua main chunks always report linedefined=0. The debug VM
    // uses that to distinguish main from regular Lua functions (e.g. so
    // debug.getlocal does not inject a synthetic "(vararg table)" entry).
    lineDefined: isMainPrototype ? 0 : prototype.lineDefined,
    lastLineDefined: prototype.lastLineDefined,
    parameterCount: prototype.paramCount,
    flags: _prototypeFlags(prototype, isMainPrototype: isMainPrototype),
    // Reserve only scratch slots actually used by mechanical expansions.
    // Do not scan raw ABC fields as registers: immediate and constant fields
    // can contain values that look like register indices.
    maxStackSize: math.max(
      2,
      prototype.registerCount + loweredInstructions.tempSlots,
    ),
    code: List<LuaBytecodeInstructionWord>.unmodifiable(instructions),
    constants: constants,
    upvalues: upvalues,
    prototypes: children,
    source: debugInfo?.absoluteSourcePath ?? sourceName,
    lineInfo: debugLines.lineInfo,
    absoluteLineInfo: debugLines.absoluteLineInfo,
    localVariables: locals,
    upvalueNames: upvalueNames,
  );
}