use super::{
code::{Code, ConstPool, ExceptionEntry, LocationEntry},
compiler::CompileError,
op::{Opcode, Operand},
};
use crate::{intern::StringId, parse::CodeRange, value::Value};
#[derive(Debug, Default)]
pub struct CodeBuilder {
bytecode: Vec<u8>,
constants: Vec<Value>,
location_table: Vec<LocationEntry>,
exception_table: Vec<ExceptionEntry>,
current_location: Option<CodeRange>,
current_focus: Option<CodeRange>,
current_stack_depth: Option<u16>,
max_stack_depth: u16,
local_names: Vec<Option<StringId>>,
}
impl CodeBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_location(&mut self, range: CodeRange, focus: Option<CodeRange>) {
self.current_location = Some(range);
self.current_focus = focus;
}
pub fn emit(&mut self, op: Opcode) -> Result<(), CompileError> {
self.emit_with_operand(op, Operand::None)
}
pub fn emit_u8(&mut self, op: Opcode, operand: u8) -> Result<(), CompileError> {
self.emit_with_operand(op, Operand::U8(operand))
}
pub fn emit_i8(&mut self, op: Opcode, operand: i8) -> Result<(), CompileError> {
self.emit_with_operand(op, Operand::I8(operand))
}
pub fn emit_u8_u8(&mut self, op: Opcode, operand1: u8, operand2: u8) -> Result<(), CompileError> {
self.emit_with_operand(op, Operand::U8U8(operand1, operand2))
}
pub fn emit_u16(&mut self, op: Opcode, operand: u16) -> Result<(), CompileError> {
self.emit_with_operand(op, Operand::U16(operand))
}
pub fn emit_u16_u8(&mut self, op: Opcode, operand1: u16, operand2: u8) -> Result<(), CompileError> {
self.emit_with_operand(op, Operand::U16U8(operand1, operand2))
}
pub fn emit_u16_u8_u8(
&mut self,
op: Opcode,
operand1: u16,
operand2: u8,
operand3: u8,
) -> Result<(), CompileError> {
self.emit_with_operand(op, Operand::U16U8U8(operand1, operand2, operand3))
}
pub fn emit_call_builtin_function(&mut self, builtin_id: u8, arg_count: u8) -> Result<(), CompileError> {
self.emit_with_operand(Opcode::CallBuiltinFunction, Operand::U8U8(builtin_id, arg_count))
}
pub fn emit_call_builtin_type(&mut self, type_id: u8, arg_count: u8) -> Result<(), CompileError> {
self.emit_with_operand(Opcode::CallBuiltinType, Operand::U8U8(type_id, arg_count))
}
pub fn emit_call_function_kw(&mut self, pos_count: u8, kwname_ids: &[u16]) -> Result<(), CompileError> {
self.emit_with_operand(Opcode::CallFunctionKw, Operand::CallKw { pos_count, kwname_ids })
}
pub fn emit_call_attr_kw(
&mut self,
attr_name_id: u16,
pos_count: u8,
kwname_ids: &[u16],
) -> Result<(), CompileError> {
self.emit_with_operand(
Opcode::CallAttrKw,
Operand::CallAttrKw {
attr_name_id,
pos_count,
kwname_ids,
},
)
}
pub fn emit_jump(&mut self, op: Opcode) -> Result<JumpLabel, CompileError> {
let Some(pre_depth) = self.current_stack_depth else {
return Ok(JumpLabel { inner: None });
};
let offset = self.current_offset();
let source_position = self.current_location.unwrap_or_default();
let target_depth = u16::try_from(i32::from(pre_depth) + i32::from(op.jump_taken_stack_effect()))
.map_err(|_| self.stack_too_large())?;
self.emit_with_operand(op, Operand::Offset(RelativeOffset(0)))?;
Ok(JumpLabel {
inner: Some(JumpLabelInner {
offset,
stack_depth: target_depth,
source_position,
}),
})
}
pub fn patch_jump(&mut self, label: JumpLabel) -> Result<(), CompileError> {
let Some(label) = label.inner else {
return Ok(());
};
let stack_depth = self.current_stack_depth.unwrap_or_else(|| {
self.new_code_region(label.stack_depth);
label.stack_depth
});
let target = JumpTargetInner {
offset: self.current_offset(),
stack_depth,
};
let offset = calculate_jump_offset(label, target)
.ok_or_else(|| jump_too_large_at(label.source_position))?
.as_i16();
let bytes = offset.to_le_bytes();
self.bytecode[label.offset.0 + 1] = bytes[0];
self.bytecode[label.offset.0 + 2] = bytes[1];
Ok(())
}
pub fn emit_jump_to(&mut self, op: Opcode, target: JumpTarget) -> Result<(), CompileError> {
let Some(target_depth) = self.current_stack_depth else {
return Ok(());
};
let label = JumpLabelInner {
offset: self.current_offset(),
stack_depth: target_depth
.checked_add_signed(op.jump_taken_stack_effect())
.ok_or_else(|| self.stack_too_large())?,
source_position: self.current_location.unwrap_or_default(),
};
let Some(target) = target.0 else {
unreachable!("emit_jump_to: cannot jump from live code to dead code");
};
let offset = calculate_jump_offset(label, target).ok_or_else(|| self.jump_too_large())?;
self.emit_with_operand(op, Operand::Offset(offset))
}
#[must_use]
pub fn current_offset(&self) -> Offset {
Offset(self.bytecode.len())
}
#[must_use]
pub fn current_jump_target(&self) -> JumpTarget {
JumpTarget(self.current_stack_depth.map(|depth| JumpTargetInner {
offset: self.current_offset(),
stack_depth: depth,
}))
}
pub fn register_local_name(&mut self, slot: u16, name: StringId) {
let slot_idx = slot as usize;
if slot_idx >= self.local_names.len() {
self.local_names.resize(slot_idx + 1, None);
}
if self.local_names[slot_idx].is_none() {
self.local_names[slot_idx] = Some(name);
}
}
pub fn emit_raise_unbound_local(&mut self, name_id: StringId) -> Result<(), CompileError> {
let name_idx = u16::try_from(name_id.index()).map_err(|_| self.name_id_too_large())?;
self.emit_with_operand(Opcode::RaiseUnboundLocal, Operand::U16(name_idx))
}
pub fn emit_load_local(&mut self, slot: u16) -> Result<(), CompileError> {
match slot {
0 => self.emit(Opcode::LoadLocal0),
1 => self.emit(Opcode::LoadLocal1),
2 => self.emit(Opcode::LoadLocal2),
3 => self.emit(Opcode::LoadLocal3),
_ => {
if let Ok(s) = u8::try_from(slot) {
self.emit_u8(Opcode::LoadLocal, s)
} else {
self.emit_u16(Opcode::LoadLocalW, slot)
}
}
}
}
pub fn emit_load_global_callable(&mut self, slot: u16, name_id: StringId) -> Result<(), CompileError> {
let name_id_u16 = u16::try_from(name_id.index()).map_err(|_| self.name_id_too_large())?;
self.emit_with_operand(Opcode::LoadGlobalCallable, Operand::U16U16(slot, name_id_u16))
}
pub fn emit_store_local(&mut self, slot: u16) -> Result<(), CompileError> {
if let Ok(s) = u8::try_from(slot) {
self.emit_u8(Opcode::StoreLocal, s)
} else {
self.emit_u16(Opcode::StoreLocalW, slot)
}
}
pub fn add_const(&mut self, value: Value) -> Result<u16, CompileError> {
let idx_u16 = u16::try_from(self.constants.len()).map_err(|_| self.constant_pool_full())?;
self.constants.push(value);
Ok(idx_u16)
}
pub fn add_exception_entry(
&mut self,
start: Offset,
end: Offset,
handler: Offset,
stack_depth: u16,
exception_stack_count: u16,
) -> Result<(), CompileError> {
let start = start.as_u32().ok_or_else(|| self.bytecode_too_large())?;
let end = end.as_u32().ok_or_else(|| self.bytecode_too_large())?;
let handler = handler.as_u32().ok_or_else(|| self.bytecode_too_large())?;
let entry = ExceptionEntry::new(start, end, handler, stack_depth, exception_stack_count);
self.exception_table.push(entry);
Ok(())
}
#[must_use]
pub fn stack_depth(&self) -> Option<u16> {
self.current_stack_depth
}
#[must_use]
pub fn is_dead(&self) -> bool {
self.current_stack_depth.is_none()
}
#[must_use]
pub fn build(self, num_locals: u16) -> Code {
let local_names: Vec<StringId> = self.local_names.into_iter().map(Option::unwrap_or_default).collect();
Code::new(
self.bytecode,
ConstPool::from_vec(self.constants),
self.location_table,
self.exception_table,
num_locals,
self.max_stack_depth,
local_names,
)
}
fn record_location(&mut self) -> Result<(), CompileError> {
if let Some(range) = self.current_location {
let offset = u32::try_from(self.bytecode.len()).map_err(|_| self.bytecode_too_large())?;
self.location_table
.push(LocationEntry::new(offset, range, self.current_focus));
}
Ok(())
}
pub fn new_code_region(&mut self, depth: u16) {
match self.current_stack_depth {
Some(d) => {
panic!("enter_region: cannot start new code region at depth {depth} while currently at live depth {d}")
}
None => self.current_stack_depth = Some(depth),
}
self.max_stack_depth = self.max_stack_depth.max(depth);
}
fn adjust_stack(&mut self, delta: i32) -> Result<(), CompileError> {
let Some(depth) = self.current_stack_depth else {
return Ok(());
};
let new_depth = i32::from(depth) + delta;
debug_assert!(new_depth >= 0, "Stack depth went negative: {new_depth}");
let new_depth = u16::try_from(new_depth.max(0)).map_err(|_| self.stack_too_large())?;
self.current_stack_depth = Some(new_depth);
self.max_stack_depth = self.max_stack_depth.max(new_depth);
Ok(())
}
fn emit_with_operand(&mut self, op: Opcode, operand: Operand<'_>) -> Result<(), CompileError> {
if self.is_dead() {
return Ok(());
}
self.record_location()?;
self.bytecode.push(op as u8);
match operand {
Operand::None => {}
Operand::U8(b) => self.bytecode.push(b),
Operand::I8(b) => self.bytecode.push(b.to_ne_bytes()[0]),
Operand::U16(w) => self.bytecode.extend(w.to_le_bytes()),
Operand::Offset(relative) => self.bytecode.extend(relative.0.to_le_bytes()),
Operand::U8U8(a, b) => {
self.bytecode.push(a);
self.bytecode.push(b);
}
Operand::U16U8(w, b) => {
self.bytecode.extend(w.to_le_bytes());
self.bytecode.push(b);
}
Operand::U16U16(w1, w2) => {
self.bytecode.extend(w1.to_le_bytes());
self.bytecode.extend(w2.to_le_bytes());
}
Operand::U16U8U8(w, b1, b2) => {
self.bytecode.extend(w.to_le_bytes());
self.bytecode.push(b1);
self.bytecode.push(b2);
}
Operand::CallKw { pos_count, kwname_ids } => {
let kw_count = u8::try_from(kwname_ids.len()).map_err(|_| self.kw_count_too_large())?;
self.bytecode.push(pos_count);
self.bytecode.push(kw_count);
for &name_id in kwname_ids {
self.bytecode.extend(name_id.to_le_bytes());
}
}
Operand::CallAttrKw {
attr_name_id,
pos_count,
kwname_ids,
} => {
let kw_count = u8::try_from(kwname_ids.len()).map_err(|_| self.kw_count_too_large())?;
self.bytecode.extend(attr_name_id.to_le_bytes());
self.bytecode.push(pos_count);
self.bytecode.push(kw_count);
for &name_id in kwname_ids {
self.bytecode.extend(name_id.to_le_bytes());
}
}
}
self.adjust_stack(op.stack_effect(operand))?;
if matches!(
op,
Opcode::ReturnValue
| Opcode::Raise
| Opcode::Reraise
| Opcode::RaiseImportError
| Opcode::RaiseUnboundLocal
| Opcode::Jump
) {
self.current_stack_depth = None;
}
Ok(())
}
#[cold]
#[inline(never)]
fn jump_too_large(&self) -> CompileError {
jump_too_large_at(self.current_location.unwrap_or_default())
}
#[cold]
#[inline(never)]
fn name_id_too_large(&self) -> CompileError {
CompileError::new(
format!(
"module has too many distinct names; the bytecode format supports up to {} interned strings",
usize::from(u16::MAX) + 1,
),
self.current_location.unwrap_or_default(),
)
}
#[cold]
#[inline(never)]
fn kw_count_too_large(&self) -> CompileError {
CompileError::new(
format!("call has too many keyword arguments; maximum is {} per call", u8::MAX),
self.current_location.unwrap_or_default(),
)
}
#[cold]
#[inline(never)]
fn constant_pool_full(&self) -> CompileError {
CompileError::new(
format!(
"function has too many constants; maximum is {} per function",
usize::from(u16::MAX) + 1,
),
self.current_location.unwrap_or_default(),
)
}
#[cold]
#[inline(never)]
fn stack_too_large(&self) -> CompileError {
CompileError::new(
"function too large: required stack exceeds u16::MAX",
self.current_location.unwrap_or_default(),
)
}
#[cold]
#[inline(never)]
fn bytecode_too_large(&self) -> CompileError {
CompileError::new(
format!(
"function bytecode too large; maximum is {} bytes",
u64::from(u32::MAX) + 1,
),
self.current_location.unwrap_or_default(),
)
}
}
#[cold]
#[inline(never)]
fn jump_too_large_at(position: CodeRange) -> CompileError {
CompileError::new("function too large: jump offset exceeds i16 range", position)
}
#[derive(Debug, Clone, Copy)]
pub struct JumpLabel {
inner: Option<JumpLabelInner>,
}
#[derive(Debug, Clone, Copy)]
struct JumpLabelInner {
offset: Offset,
stack_depth: u16,
source_position: CodeRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Offset(usize);
impl Offset {
#[must_use]
pub fn as_u32(self) -> Option<u32> {
u32::try_from(self.0).ok()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RelativeOffset(i16);
impl RelativeOffset {
#[must_use]
pub fn as_i16(self) -> i16 {
self.0
}
}
fn calculate_jump_offset(from: JumpLabelInner, to: JumpTargetInner) -> Option<RelativeOffset> {
const JUMP_BYTECODE_SIZE: usize = size_of::<Opcode>() + size_of::<RelativeOffset>();
let from_i64 = i64::try_from(from.offset.0 + JUMP_BYTECODE_SIZE).expect("bytecode offset exceeds i64");
let to_i64 = i64::try_from(to.offset.0).expect("bytecode offset exceeds i64");
debug_assert_eq!(
from.stack_depth, to.stack_depth,
"jump merge: arriving with depth {} but jump target has depth {}",
from.stack_depth, to.stack_depth,
);
let raw_offset = to_i64 - from_i64;
i16::try_from(raw_offset).ok().map(RelativeOffset)
}
#[derive(Debug, Clone, Copy)]
pub struct JumpTarget(Option<JumpTargetInner>);
#[derive(Debug, Clone, Copy)]
struct JumpTargetInner {
offset: Offset,
stack_depth: u16,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emit_basic() {
let mut builder = CodeBuilder::new();
builder.new_code_region(0);
builder.emit(Opcode::LoadNone).unwrap();
builder.emit(Opcode::Pop).unwrap();
let code = builder.build(0);
assert_eq!(code.bytecode(), &[Opcode::LoadNone as u8, Opcode::Pop as u8]);
}
#[test]
fn test_emit_u8_operand() {
let mut builder = CodeBuilder::new();
builder.new_code_region(0);
builder.emit_u8(Opcode::LoadLocal, 42).unwrap();
let code = builder.build(0);
assert_eq!(code.bytecode(), &[Opcode::LoadLocal as u8, 42]);
}
#[test]
fn test_emit_u16_operand() {
let mut builder = CodeBuilder::new();
builder.new_code_region(0);
builder.emit_u16(Opcode::LoadConst, 0x1234).unwrap();
let code = builder.build(0);
assert_eq!(code.bytecode(), &[Opcode::LoadConst as u8, 0x34, 0x12]);
}
#[test]
fn test_forward_jump() {
let mut builder = CodeBuilder::new();
builder.new_code_region(0);
let jump = builder.emit_jump(Opcode::Jump).unwrap();
builder.new_code_region(0);
builder.emit(Opcode::LoadNone).unwrap();
builder.emit(Opcode::Pop).unwrap();
builder.patch_jump(jump).unwrap();
builder.emit(Opcode::LoadNone).unwrap(); builder.emit(Opcode::ReturnValue).unwrap();
let code = builder.build(0);
assert_eq!(
code.bytecode(),
&[
Opcode::Jump as u8,
2i16.to_le_bytes()[0],
2i16.to_le_bytes()[1], Opcode::LoadNone as u8,
Opcode::Pop as u8,
Opcode::LoadNone as u8,
Opcode::ReturnValue as u8,
]
);
}
#[test]
fn test_backward_jump() {
let mut builder = CodeBuilder::new();
builder.new_code_region(0);
let loop_start = builder.current_jump_target();
builder.emit(Opcode::LoadNone).unwrap(); builder.emit(Opcode::Pop).unwrap(); builder.emit_jump_to(Opcode::Jump, loop_start).unwrap();
let code = builder.build(0);
let expected_offset = (-5i16).to_le_bytes();
assert_eq!(
code.bytecode(),
&[
Opcode::LoadNone as u8,
Opcode::Pop as u8,
Opcode::Jump as u8,
expected_offset[0],
expected_offset[1],
]
);
}
#[test]
fn test_load_local_specialization() {
let mut builder = CodeBuilder::new();
builder.new_code_region(0);
builder.emit_load_local(0).unwrap();
builder.emit_load_local(1).unwrap();
builder.emit_load_local(2).unwrap();
builder.emit_load_local(3).unwrap();
builder.emit_load_local(4).unwrap();
builder.emit_load_local(256).unwrap();
let code = builder.build(0);
assert_eq!(
code.bytecode(),
&[
Opcode::LoadLocal0 as u8,
Opcode::LoadLocal1 as u8,
Opcode::LoadLocal2 as u8,
Opcode::LoadLocal3 as u8,
Opcode::LoadLocal as u8,
4,
Opcode::LoadLocalW as u8,
0,
1, ]
);
}
#[test]
fn test_add_const() {
let mut builder = CodeBuilder::new();
builder.new_code_region(0);
let idx1 = builder.add_const(Value::Int(42)).unwrap();
let idx2 = builder.add_const(Value::None).unwrap();
assert_eq!(idx1, 0);
assert_eq!(idx2, 1);
}
}