extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use crate::ast::*;
use crate::bytecode::*;
use crate::layout_pass::LayoutContext;
use crate::token::Span;
#[derive(Debug, Clone)]
pub struct CompileError {
pub message: String,
pub span: Span,
}
struct Local {
name: String,
slot: u16,
depth: u32,
ty: Option<TypeExpr>,
}
#[derive(Default, Clone)]
struct TypeInfo {
structs: BTreeMap<String, BTreeMap<String, TypeExpr>>,
struct_field_order: BTreeMap<String, Vec<(String, TypeExpr)>>,
enums: BTreeMap<String, BTreeMap<String, Vec<TypeExpr>>>,
enum_variant_order: BTreeMap<String, Vec<(String, i64)>>,
struct_defs: BTreeMap<String, StructDef>,
enum_defs: BTreeMap<String, EnumDef>,
function_returns: BTreeMap<String, TypeExpr>,
data_field_types: BTreeMap<String, BTreeMap<String, TypeExpr>>,
newtype_names: alloc::collections::BTreeSet<String>,
newtype_refinements: BTreeMap<String, String>,
refinement_bodies: BTreeMap<String, (String, Expr)>,
function_return_ranges: BTreeMap<String, crate::interval::IntervalSet>,
word_bytes: usize,
float_bytes: usize,
}
impl TypeInfo {
fn layout_context(&self) -> LayoutContext<'_> {
LayoutContext::new(
&self.struct_defs,
&self.enum_defs,
self.word_bytes,
self.float_bytes,
)
.with_opaque_fallback(&self.newtype_names)
}
}
struct FuncCompiler {
chunk: Chunk,
locals: Vec<Local>,
scope_depth: u32,
next_slot: u16,
loop_breaks: Vec<Vec<usize>>,
function_map: BTreeMap<String, u16>,
native_map: BTreeMap<String, u16>,
native_externals: BTreeMap<String, bool>,
data_fields: BTreeMap<String, Vec<(String, u16)>>,
const_fields: BTreeMap<String, BTreeMap<String, crate::bytecode::ConstValue>>,
type_info: TypeInfo,
expr_types: BTreeMap<crate::token::Span, TypeExpr>,
local_const_values: BTreeMap<u16, i64>,
local_ranges: BTreeMap<u16, crate::interval::IntervalSet>,
emit_debug: bool,
pending_debug: Vec<PendingDebug>,
may_intern_opaque: bool,
}
enum PendingDebug {
CallSite {
op_index: usize,
span: crate::token::Span,
},
SourceSpan {
op_index: usize,
span: crate::token::Span,
},
LineNumber { op_index: usize, line: u32 },
BreakpointCandidate {
op_index: usize,
span: crate::token::Span,
},
VariableName {
op_index: usize,
slot: u16,
name: String,
},
Optimisation { op_index: usize, name: String },
Assert {
op_index: usize,
span: crate::token::Span,
message: Option<String>,
},
GenericInstantiation { origin: String, type_args: String },
IfcLabel {
op_index: usize,
labels: Vec<String>,
},
TypeAnnotation {
op_index: usize,
slot: u16,
type_repr: String,
},
}
fn push_debug_span(
pool: &mut crate::debug_meta::DebugPool,
file_idx: u16,
span: &crate::token::Span,
) -> u16 {
let idx = pool.span_pool.len() as u16;
let length = span.end.saturating_sub(span.start) as u32;
pool.span_pool.push((file_idx, span.start as u32, length));
idx
}
fn intern_debug_string(pool: &mut crate::debug_meta::DebugPool, s: &str) -> u16 {
if let Some(i) = pool.string_pool.iter().position(|x| x == s) {
return i as u16;
}
let i = pool.string_pool.len() as u16;
pool.string_pool.push(String::from(s));
i
}
fn intern_debug_type(pool: &mut crate::debug_meta::DebugPool, bytes: &[u8]) -> u16 {
if let Some(i) = pool.type_pool.iter().position(|x| x.as_slice() == bytes) {
return i as u16;
}
let i = pool.type_pool.len() as u16;
pool.type_pool.push(bytes.to_vec());
i
}
fn render_type_expr(ty: &TypeExpr) -> String {
use crate::ast::PrimType;
match ty {
TypeExpr::Prim(p, _) => match p {
PrimType::Byte => String::from("Byte"),
PrimType::Word => String::from("Word"),
PrimType::Float => String::from("Float"),
PrimType::Bool => String::from("bool"),
PrimType::Text => String::from("Text"),
PrimType::Fixed(None) => String::from("Fixed"),
PrimType::Fixed(Some(n)) => alloc::format!("Fixed<{}>", n),
},
TypeExpr::Multiword(n, f, _) => {
if f.as_lit() == Some(0) {
alloc::format!("Multiword<{}>", n)
} else {
alloc::format!("Multiword<{}, {}>", n, f)
}
}
TypeExpr::Named(name, args, _, _) => {
if args.is_empty() {
name.clone()
} else {
let inner: Vec<String> = args.iter().map(render_type_expr).collect();
alloc::format!("{}<{}>", name, inner.join(", "))
}
}
TypeExpr::Tuple(elems, _) => {
let inner: Vec<String> = elems.iter().map(render_type_expr).collect();
alloc::format!("({})", inner.join(", "))
}
TypeExpr::Array(inner, n, _) => alloc::format!("[{}; {}]", render_type_expr(inner), n),
TypeExpr::Option(inner, _) => alloc::format!("Option<{}>", render_type_expr(inner)),
TypeExpr::Unit(_) => String::from("()"),
TypeExpr::Labelled(inner, labels, _) => {
alloc::format!("{}@{{{}}}", render_type_expr(inner), labels.join(", "))
}
TypeExpr::NegativeLabelled(inner, labels, _) => {
let neg: Vec<String> = labels.iter().map(|l| alloc::format!("!{}", l)).collect();
alloc::format!("{}@{{{}}}", render_type_expr(inner), neg.join(", "))
}
}
}
fn build_debug_pool(pending: &[PendingDebug]) -> crate::debug_meta::DebugPool {
use crate::debug_meta::{DebugPool, DebugRecord, DebugRecordKind};
let mut pool = DebugPool::default();
pool.string_pool.push(String::new());
let file_idx: u16 = 0;
for item in pending {
match item {
PendingDebug::CallSite { op_index, span } => {
let span_idx = push_debug_span(&mut pool, file_idx, span);
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::CallSite,
operands: alloc::vec![span_idx],
});
}
PendingDebug::SourceSpan { op_index, span } => {
let span_idx = push_debug_span(&mut pool, file_idx, span);
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::SourceSpan,
operands: alloc::vec![span_idx],
});
}
PendingDebug::BreakpointCandidate { op_index, span } => {
let span_idx = push_debug_span(&mut pool, file_idx, span);
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::BreakpointCandidate,
operands: alloc::vec![span_idx],
});
}
PendingDebug::LineNumber { op_index, line } => {
let line_u16 = (*line).min(u32::from(u16::MAX)) as u16;
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::LineNumber,
operands: alloc::vec![line_u16],
});
}
PendingDebug::VariableName {
op_index,
slot,
name,
} => {
let name_idx = intern_debug_string(&mut pool, name);
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::VariableName,
operands: alloc::vec![*slot, name_idx],
});
}
PendingDebug::Optimisation { op_index, name } => {
let name_idx = intern_debug_string(&mut pool, name);
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::OptimisationMarker,
operands: alloc::vec![name_idx],
});
}
PendingDebug::Assert {
op_index,
span,
message,
} => {
let span_idx = push_debug_span(&mut pool, file_idx, span);
let mut operands = alloc::vec![span_idx];
if let Some(message) = message {
operands.push(intern_debug_string(&mut pool, message));
}
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::AssertionContext,
operands,
});
}
PendingDebug::GenericInstantiation { origin, type_args } => {
let origin_idx = intern_debug_string(&mut pool, origin);
let type_args_idx = intern_debug_string(&mut pool, type_args);
pool.records.push(DebugRecord {
op_index: 0,
kind: DebugRecordKind::GenericInstantiation,
operands: alloc::vec![origin_idx, type_args_idx],
});
}
PendingDebug::IfcLabel { op_index, labels } => {
let mut operands = alloc::vec::Vec::with_capacity(labels.len());
for label in labels {
operands.push(intern_debug_string(&mut pool, label));
}
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::IfcLabelAnnotation,
operands,
});
}
PendingDebug::TypeAnnotation {
op_index,
slot,
type_repr,
} => {
let type_idx = intern_debug_type(&mut pool, type_repr.as_bytes());
pool.records.push(DebugRecord {
op_index: *op_index as u32,
kind: DebugRecordKind::TypeAnnotation,
operands: alloc::vec![*slot, type_idx],
});
}
}
}
pool
}
impl FuncCompiler {
#[allow(clippy::too_many_arguments)]
fn new(
name: &str,
block_type: BlockType,
function_map: BTreeMap<String, u16>,
native_map: BTreeMap<String, u16>,
native_externals: BTreeMap<String, bool>,
data_fields: BTreeMap<String, Vec<(String, u16)>>,
const_fields: BTreeMap<String, BTreeMap<String, crate::bytecode::ConstValue>>,
type_info: TypeInfo,
expr_types: BTreeMap<crate::token::Span, TypeExpr>,
emit_debug: bool,
) -> Self {
Self {
chunk: Chunk {
name: String::from(name),
ops: Vec::new(),
constants: Vec::new(),
struct_templates: Vec::new(),
local_count: 0,
param_count: 0,
block_type,
param_types: Vec::new(),
debug_pool: None,
},
locals: Vec::new(),
scope_depth: 0,
next_slot: 0,
loop_breaks: Vec::new(),
function_map,
native_map,
native_externals,
data_fields,
const_fields,
type_info,
expr_types,
local_const_values: BTreeMap::new(),
local_ranges: BTreeMap::new(),
emit_debug,
pending_debug: Vec::new(),
may_intern_opaque: false,
}
}
fn record_call_site_last_op(&mut self, span: &crate::token::Span) {
if !self.emit_debug {
return;
}
if let Some(idx) = self.chunk.ops.len().checked_sub(1)
&& matches!(
self.chunk.ops[idx],
Op::Call(..) | Op::CallVerifiedNative(..) | Op::CallExternalNative(..)
)
{
self.pending_debug.push(PendingDebug::CallSite {
op_index: idx,
span: *span,
});
}
}
fn record_operator_site(&mut self, span: &crate::token::Span) {
if !self.emit_debug {
return;
}
if let Some(idx) = self.chunk.ops.len().checked_sub(1) {
self.pending_debug.push(PendingDebug::SourceSpan {
op_index: idx,
span: *span,
});
self.pending_debug.push(PendingDebug::BreakpointCandidate {
op_index: idx,
span: *span,
});
}
}
fn record_statement(&mut self, op_index: usize, span: &crate::token::Span) {
if !self.emit_debug || op_index >= self.chunk.ops.len() {
return;
}
self.pending_debug.push(PendingDebug::SourceSpan {
op_index,
span: *span,
});
self.pending_debug.push(PendingDebug::LineNumber {
op_index,
line: span.line,
});
self.pending_debug.push(PendingDebug::BreakpointCandidate {
op_index,
span: *span,
});
}
fn record_function_entry(&mut self, span: &crate::token::Span) {
if self.emit_debug && !self.chunk.ops.is_empty() {
self.pending_debug.push(PendingDebug::BreakpointCandidate {
op_index: 0,
span: *span,
});
}
}
fn record_assert(&mut self, op_index: usize, span: &crate::token::Span, message: Option<&str>) {
if self.emit_debug {
self.pending_debug.push(PendingDebug::Assert {
op_index,
span: *span,
message: message.map(String::from),
});
}
}
fn record_optimisation(&mut self, op_index: usize, name: &str) {
if self.emit_debug {
self.pending_debug.push(PendingDebug::Optimisation {
op_index,
name: String::from(name),
});
}
}
fn record_generic_instantiation(&mut self, origin: &str, type_args: &str) {
if self.emit_debug {
self.pending_debug.push(PendingDebug::GenericInstantiation {
origin: String::from(origin),
type_args: String::from(type_args),
});
}
}
fn record_ifc_labels(&mut self, op_index: usize, labels: &[String]) {
if self.emit_debug && !labels.is_empty() {
self.pending_debug.push(PendingDebug::IfcLabel {
op_index,
labels: labels.to_vec(),
});
}
}
fn static_for_in_length(&self, expr: &Expr) -> Option<i64> {
match expr {
Expr::ArrayLiteral { elements, .. } => Some(elements.len() as i64),
Expr::Call { name, .. } => {
let return_type = self.type_info.function_returns.get(name)?;
array_length_of_type(return_type)
}
Expr::FieldAccess { object, field, .. } => {
let owner = self.struct_name_of(object)?;
let field_type = self
.type_info
.structs
.get(&owner)
.or_else(|| self.type_info.data_field_types.get(&owner))
.and_then(|fields| fields.get(field))?;
array_length_of_type(field_type)
}
Expr::Ident { name, .. } => {
let ty = self.local_type(name)?;
array_length_of_type(ty)
}
Expr::ArrayIndex { object, .. } => {
let object_ty = infer_expr_type(self, object)?;
let elem_ty = element_type_of(&object_ty)?;
array_length_of_type(&elem_ty)
}
Expr::Match { arms, .. } => {
let first = arms.first()?;
let arm_ty = infer_expr_type(self, &first.expr)?;
array_length_of_type(&arm_ty)
}
_ => None,
}
}
fn struct_name_of(&self, expr: &Expr) -> Option<String> {
match expr {
Expr::Ident { name, .. } => {
if self.data_fields.contains_key(name) || self.const_fields.contains_key(name) {
return Some(name.clone());
}
let ty = self.local_type(name)?;
if let TypeExpr::Named(struct_name, _, _, _) = ty {
return Some(struct_name.clone());
}
None
}
Expr::FieldAccess { object, field, .. } => {
let owner = self.struct_name_of(object)?;
let field_ty = self
.type_info
.structs
.get(&owner)
.or_else(|| self.type_info.data_field_types.get(&owner))
.and_then(|fields| fields.get(field))?;
if let TypeExpr::Named(struct_name, _, _, _) = field_ty {
return Some(struct_name.clone());
}
None
}
_ => None,
}
}
fn emit(&mut self, op: Op) -> usize {
let idx = self.chunk.ops.len();
self.chunk.ops.push(op);
idx
}
fn emit_jump(&mut self, placeholder: Op) -> usize {
self.emit(placeholder)
}
fn patch_jump(&mut self, addr: usize) {
let target = self.chunk.ops.len() as u16;
match &mut self.chunk.ops[addr] {
Op::If(a)
| Op::Else(a)
| Op::Loop(a)
| Op::EndLoop(a)
| Op::Break(a)
| Op::BreakIf(a) => *a = target,
_ => {}
}
}
fn add_constant(&mut self, value: Value) -> u16 {
let cv = ConstValue::try_from_value(value).expect("compile-time constant only");
self.add_const_value(cv)
}
fn add_const_value(&mut self, mut cv: ConstValue) -> u16 {
resolve_const_enum_discriminants(&mut cv, &self.type_info.enum_variant_order);
for (i, c) in self.chunk.constants.iter().enumerate() {
if *c == cv {
return i as u16;
}
}
let idx = self.chunk.constants.len() as u16;
self.chunk.constants.push(cv);
idx
}
fn add_string_constant(&mut self, s: &str) -> u16 {
self.add_constant(Value::StaticStr(String::from(s)))
}
fn add_struct_template(&mut self, type_name: &str, field_names: Vec<String>) -> u16 {
let idx = self.chunk.struct_templates.len() as u16;
self.chunk.struct_templates.push(StructTemplate {
type_name: String::from(type_name),
field_names,
});
idx
}
fn resolve_local(&self, name: &str) -> Option<u16> {
for local in self.locals.iter().rev() {
if local.name == name {
return Some(local.slot);
}
}
Option::None
}
fn local_const_lookup(&self, name: &str) -> Option<EvalValue> {
let slot = self.resolve_local(name)?;
self.local_const_values
.get(&slot)
.copied()
.map(EvalValue::Int)
}
fn is_data_block(&self, name: &str) -> bool {
self.data_fields.contains_key(name) || self.const_fields.contains_key(name)
}
fn is_const_data_block(&self, name: &str) -> bool {
self.const_fields.contains_key(name)
}
fn const_data_field_value(
&self,
data_name: &str,
field_name: &str,
) -> Option<crate::bytecode::ConstValue> {
self.const_fields
.get(data_name)
.and_then(|m| m.get(field_name))
.cloned()
}
fn resolve_data_field(&self, data_name: &str, field: &str) -> Option<u16> {
self.data_fields.get(data_name).and_then(|fields| {
fields
.iter()
.find(|(name, _)| name == field)
.map(|(_, slot)| *slot)
})
}
fn declare_local(&mut self, name: &str) -> u16 {
self.declare_local_typed(name, None)
}
fn declare_local_typed(&mut self, name: &str, ty: Option<TypeExpr>) -> u16 {
let slot = self.next_slot;
self.next_slot += 1;
if self.next_slot > self.chunk.local_count {
self.chunk.local_count = self.next_slot;
}
let type_repr = if self.emit_debug {
ty.as_ref().map(render_type_expr)
} else {
None
};
self.locals.push(Local {
name: String::from(name),
slot,
depth: self.scope_depth,
ty,
});
if self.emit_debug {
let op_index = self.chunk.ops.len();
self.pending_debug.push(PendingDebug::VariableName {
op_index,
slot,
name: String::from(name),
});
if let Some(type_repr) = type_repr {
self.pending_debug.push(PendingDebug::TypeAnnotation {
op_index,
slot,
type_repr,
});
}
}
slot
}
fn local_type(&self, name: &str) -> Option<&TypeExpr> {
for local in self.locals.iter().rev() {
if local.name == name {
return local.ty.as_ref();
}
}
None
}
fn begin_scope(&mut self) {
self.scope_depth += 1;
}
fn end_scope(&mut self) {
while let Some(local) = self.locals.last() {
if local.depth < self.scope_depth {
break;
}
self.locals.pop();
}
self.scope_depth -= 1;
}
fn enter_loop(&mut self) {
self.loop_breaks.push(Vec::new());
}
fn exit_loop(&mut self) {
if let Some(breaks) = self.loop_breaks.pop() {
for addr in breaks {
self.patch_jump(addr);
}
}
}
fn finish(mut self) -> Chunk {
self.chunk.local_count = self.next_slot;
if self.emit_debug && !self.pending_debug.is_empty() {
self.chunk.debug_pool = Some(build_debug_pool(&self.pending_debug));
}
self.chunk
}
}
fn array_length_of_type(t: &TypeExpr) -> Option<i64> {
match t {
TypeExpr::Array(_, n, _) => n.as_lit(),
_ => None,
}
}
fn element_type_of(t: &TypeExpr) -> Option<TypeExpr> {
match t {
TypeExpr::Array(elem, _, _) => Some((**elem).clone()),
TypeExpr::Multiword(_, _, _) => Some(TypeExpr::Prim(PrimType::Word, Span::default())),
_ => None,
}
}
fn const_shift_amount(right: &Expr, max_bits: i64) -> Result<i64, CompileError> {
match right {
Expr::Literal {
value: crate::ast::Literal::Int(k),
span,
} => {
if *k < 0 || *k >= max_bits {
Err(CompileError {
message: alloc::format!(
"shift amount {} is out of range [0, {}) for the value width",
k,
max_bits
),
span: *span,
})
} else {
Ok(*k)
}
}
_ => Err(CompileError {
message: String::from(
"the overflow-checked arithmetic left shift requires a compile-time-constant amount; a variable amount is admissible only for the bare shift operators",
),
span: right.span(),
}),
}
}
fn classify_shift_amount(right: &Expr, max_bits: i64) -> Result<Option<i64>, CompileError> {
match right {
Expr::Literal {
value: crate::ast::Literal::Int(k),
span,
} => {
if *k < 0 || *k >= max_bits {
Err(CompileError {
message: alloc::format!(
"shift amount {} is out of range [0, {}) for the value width",
k,
max_bits
),
span: *span,
})
} else {
Ok(Some(*k))
}
}
_ => Ok(None),
}
}
fn compile_scalar_shift(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
left: &Expr,
right: &Expr,
) -> Result<(), CompileError> {
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let is_byte = matches!(
infer_expr_type(fc, left),
Some(TypeExpr::Prim(PrimType::Byte, _))
);
let value_bits = if is_byte { 8 } else { word_bits };
let amount = classify_shift_amount(right, value_bits)?;
compile_expr(fc, left)?;
if is_byte {
fc.emit(Op::ByteToWord);
}
match amount {
Some(k) => compile_scalar_shift_const(fc, op, k, word_bits),
None => compile_scalar_shift_variable(fc, op, right, word_bits)?,
}
if is_byte {
fc.emit(Op::WordToByte);
}
Ok(())
}
fn compile_scalar_shift_const(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
k: i64,
word_bits: i64,
) {
use crate::ast::BinOp;
let k_c = fc.add_constant(Value::Int(k));
fc.emit(Op::Const(k_c));
match op {
BinOp::Shl | BinOp::AShl => {
fc.emit(Op::Shl);
}
BinOp::ShrA => {
fc.emit(Op::Shr);
}
BinOp::ShrL => {
fc.emit(Op::Shr);
let mask = ((1i128 << (word_bits - k)) - 1) as i64;
let mask_c = fc.add_constant(Value::Int(mask));
fc.emit(Op::Const(mask_c));
fc.emit(Op::BitAnd);
}
_ => unreachable!("compile_scalar_shift_const called with a non-shift operator"),
}
}
fn compile_scalar_shift_variable(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
right: &Expr,
word_bits: i64,
) -> Result<(), CompileError> {
use crate::ast::BinOp;
match op {
BinOp::Shl | BinOp::AShl => {
compile_expr(fc, right)?;
fc.emit(Op::Shl);
}
BinOp::ShrA => {
compile_expr(fc, right)?;
fc.emit(Op::Shr);
}
BinOp::ShrL => {
let lv = fc.declare_local("__shr_v");
fc.emit(Op::SetLocal(lv));
compile_expr(fc, right)?;
let lc = fc.declare_local("__shr_c");
fc.emit(Op::SetLocal(lc));
let zero = fc.add_constant(Value::Int(0));
let one = fc.add_constant(Value::Int(1));
let wb_c = fc.add_constant(Value::Int(word_bits));
fc.emit(Op::GetLocal(lc));
fc.emit(Op::Const(zero));
fc.emit(Op::CmpEq);
let if_addr = fc.emit_jump(Op::If(0));
fc.emit(Op::GetLocal(lv));
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
fc.emit(Op::GetLocal(lv));
fc.emit(Op::GetLocal(lc));
fc.emit(Op::Shr); fc.emit(Op::Const(one));
fc.emit(Op::Const(wb_c));
fc.emit(Op::GetLocal(lc));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2)); fc.emit(Op::Shl); fc.emit(Op::Const(one));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2)); fc.emit(Op::BitAnd);
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
}
_ => unreachable!("compile_scalar_shift_variable called with a non-shift operator"),
}
Ok(())
}
fn compile_byte_bitwise(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
left: &Expr,
right: &Expr,
) -> Result<(), CompileError> {
use crate::ast::BinOp;
compile_expr(fc, left)?;
fc.emit(Op::ByteToWord);
compile_expr(fc, right)?;
fc.emit(Op::ByteToWord);
match op {
BinOp::Band => {
fc.emit(Op::BitAnd);
}
BinOp::Bor => {
fc.emit(Op::BitOr);
}
BinOp::Bxor => {
fc.emit(Op::BitXor);
}
_ => unreachable!("compile_byte_bitwise called with a non-bitwise operator"),
}
fc.emit(Op::WordToByte);
Ok(())
}
fn compile_multiword_shift(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
left: &Expr,
right: &Expr,
n: u16,
) -> Result<(), CompileError> {
use crate::ast::BinOp;
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let total_bits = n as i64 * word_bits;
let k = match classify_shift_amount(right, total_bits)? {
Some(k) => k,
None => return compile_multiword_variable_shift(fc, op, left, right, n, word_bits),
};
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let zero = fc.add_constant(Value::Int(0));
let shift_c = fc.add_constant(Value::Int(word_bits - 1));
let av = fc.declare_local("__mw_a");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
let mut wv: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
let wi = fc.declare_local("__mw_w");
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(wi));
wv.push(wi);
}
let q = (k / word_bits) as usize;
let r = k % word_bits;
let nn = n as usize;
let mut rwords: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
if matches!(op, BinOp::Shl | BinOp::AShl) {
let (r_c, wr_c, carry_mask) = if r != 0 {
let cm = ((1i128 << r) - 1) as i64;
(
Some(fc.add_constant(Value::Int(r))),
Some(fc.add_constant(Value::Int(word_bits - r))),
Some(fc.add_constant(Value::Int(cm))),
)
} else {
(None, None, None)
};
for j in 0..nn {
let rk = fc.declare_local("__mw_r");
if j >= q {
fc.emit(Op::GetLocal(wv[j - q]));
if r != 0 {
fc.emit(Op::Const(r_c.unwrap()));
fc.emit(Op::Shl);
}
} else {
fc.emit(Op::Const(zero));
}
if r != 0 && j > q {
fc.emit(Op::GetLocal(wv[j - q - 1]));
fc.emit(Op::Const(wr_c.unwrap()));
fc.emit(Op::Shr);
fc.emit(Op::Const(carry_mask.unwrap()));
fc.emit(Op::BitAnd);
fc.emit(Op::BitOr);
}
fc.emit(Op::SetLocal(rk));
rwords.push(rk);
}
} else {
let fill = fc.declare_local("__mw_fill");
if matches!(op, BinOp::ShrA) {
fc.emit(Op::GetLocal(wv[nn - 1]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(fill));
} else {
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(fill));
}
let vword = |m: usize| -> u16 { if m < nn { wv[m] } else { fill } };
let (r_c, wr_c, logmask) = if r != 0 {
let lm = ((1i128 << (word_bits - r)) - 1) as i64;
(
Some(fc.add_constant(Value::Int(r))),
Some(fc.add_constant(Value::Int(word_bits - r))),
Some(fc.add_constant(Value::Int(lm))),
)
} else {
(None, None, None)
};
for j in 0..nn {
let rk = fc.declare_local("__mw_r");
if r == 0 {
fc.emit(Op::GetLocal(vword(j + q)));
} else {
fc.emit(Op::GetLocal(vword(j + q)));
fc.emit(Op::Const(r_c.unwrap()));
fc.emit(Op::Shr);
fc.emit(Op::Const(logmask.unwrap()));
fc.emit(Op::BitAnd);
fc.emit(Op::GetLocal(vword(j + q + 1)));
fc.emit(Op::Const(wr_c.unwrap()));
fc.emit(Op::Shl);
fc.emit(Op::BitOr);
}
fc.emit(Op::SetLocal(rk));
rwords.push(rk);
}
}
for &rk in &rwords {
fc.emit(Op::GetLocal(rk));
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
Ok(())
}
fn emit_mw_index(fc: &mut FuncCompiler, base: i64, qv: u16, add: bool, iv: u16) {
let base_c = fc.add_constant(Value::Int(base));
fc.emit(Op::Const(base_c));
fc.emit(Op::GetLocal(qv));
if add {
fc.emit(Op::CheckedAdd);
} else {
fc.emit(Op::CheckedSub);
}
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(iv));
}
#[allow(clippy::too_many_arguments)]
fn emit_mw_guarded(
fc: &mut FuncCompiler,
av: u16,
iv: u16,
fillv: u16,
imv: u16,
siv: u16,
elem_op: crate::bytecode::ArrayElem,
neg1: u16,
wbm1_c: u16,
n_c: u16,
) {
fc.emit(Op::GetLocal(iv));
fc.emit(Op::Const(wbm1_c));
fc.emit(Op::Shr);
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor);
fc.emit(Op::GetLocal(iv));
fc.emit(Op::Const(n_c));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::Const(wbm1_c));
fc.emit(Op::Shr);
fc.emit(Op::BitAnd); fc.emit(Op::SetLocal(imv));
fc.emit(Op::GetLocal(iv));
fc.emit(Op::GetLocal(imv));
fc.emit(Op::BitAnd);
fc.emit(Op::SetLocal(siv));
fc.emit(Op::GetLocal(av));
fc.emit(Op::GetLocal(siv));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::GetLocal(imv));
fc.emit(Op::BitAnd);
fc.emit(Op::GetLocal(fillv));
fc.emit(Op::GetLocal(imv));
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor);
fc.emit(Op::BitAnd);
fc.emit(Op::BitOr);
}
fn compile_multiword_variable_shift(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
left: &Expr,
right: &Expr,
n: u16,
word_bits: i64,
) -> Result<(), CompileError> {
use crate::ast::BinOp;
let nn = n as usize;
let log2_wb = word_bits.trailing_zeros() as i64;
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let is_left = matches!(op, BinOp::Shl | BinOp::AShl);
let zero = fc.add_constant(Value::Int(0));
let one = fc.add_constant(Value::Int(1));
let neg1 = fc.add_constant(Value::Int(-1));
let wbm1_c = fc.add_constant(Value::Int(word_bits - 1));
let log2_c = fc.add_constant(Value::Int(log2_wb));
let wb_c = fc.add_constant(Value::Int(word_bits));
let n_c = fc.add_constant(Value::Int(n as i64));
let av = fc.declare_local("__mwv_a");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
let cv = fc.declare_local("__mwv_c");
compile_expr(fc, right)?;
fc.emit(Op::SetLocal(cv));
let qv = fc.declare_local("__mwv_q");
fc.emit(Op::GetLocal(cv));
fc.emit(Op::Const(log2_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(qv));
let qsignv = fc.declare_local("__mwv_qsign");
fc.emit(Op::GetLocal(qv));
fc.emit(Op::Const(wbm1_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(qsignv));
fc.emit(Op::GetLocal(qv));
fc.emit(Op::GetLocal(qsignv));
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor);
fc.emit(Op::BitAnd);
fc.emit(Op::Const(n_c));
fc.emit(Op::GetLocal(qsignv));
fc.emit(Op::BitAnd);
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(qv));
let rv = fc.declare_local("__mwv_r");
fc.emit(Op::GetLocal(cv));
fc.emit(Op::Const(wbm1_c));
fc.emit(Op::BitAnd);
fc.emit(Op::SetLocal(rv));
let fillv = fc.declare_local("__mwv_fill");
if matches!(op, BinOp::ShrA) {
let top_c = fc.add_constant(Value::Int((nn - 1) as i64));
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(top_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::Const(wbm1_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(fillv));
} else {
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(fillv));
}
let iv = fc.declare_local("__mwv_i");
let imv = fc.declare_local("__mwv_im");
let siv = fc.declare_local("__mwv_si");
fc.emit(Op::GetLocal(rv));
fc.emit(Op::Const(zero));
fc.emit(Op::CmpEq);
let if_addr = fc.emit_jump(Op::If(0));
for j in 0..nn {
emit_mw_index(fc, j as i64, qv, !is_left, iv);
emit_mw_guarded(fc, av, iv, fillv, imv, siv, elem_op, neg1, wbm1_c, n_c);
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
let wbrv = fc.declare_local("__mwv_wbr");
fc.emit(Op::Const(wb_c));
fc.emit(Op::GetLocal(rv));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(wbrv));
if is_left {
let maskr = fc.declare_local("__mwv_mr");
fc.emit(Op::Const(one));
fc.emit(Op::GetLocal(rv));
fc.emit(Op::Shl);
fc.emit(Op::Const(one));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(maskr));
for j in 0..nn {
emit_mw_index(fc, j as i64, qv, false, iv);
emit_mw_guarded(fc, av, iv, fillv, imv, siv, elem_op, neg1, wbm1_c, n_c);
fc.emit(Op::GetLocal(rv));
fc.emit(Op::Shl);
emit_mw_index(fc, j as i64 - 1, qv, false, iv);
emit_mw_guarded(fc, av, iv, fillv, imv, siv, elem_op, neg1, wbm1_c, n_c);
fc.emit(Op::GetLocal(wbrv));
fc.emit(Op::Shr);
fc.emit(Op::GetLocal(maskr));
fc.emit(Op::BitAnd);
fc.emit(Op::BitOr);
}
} else {
let lomask = fc.declare_local("__mwv_lm");
fc.emit(Op::Const(one));
fc.emit(Op::GetLocal(wbrv));
fc.emit(Op::Shl);
fc.emit(Op::Const(one));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(lomask));
for j in 0..nn {
emit_mw_index(fc, j as i64, qv, true, iv);
emit_mw_guarded(fc, av, iv, fillv, imv, siv, elem_op, neg1, wbm1_c, n_c);
fc.emit(Op::GetLocal(rv));
fc.emit(Op::Shr);
fc.emit(Op::GetLocal(lomask));
fc.emit(Op::BitAnd);
emit_mw_index(fc, j as i64 + 1, qv, true, iv);
emit_mw_guarded(fc, av, iv, fillv, imv, siv, elem_op, neg1, wbm1_c, n_c);
fc.emit(Op::GetLocal(wbrv));
fc.emit(Op::Shl);
fc.emit(Op::BitOr);
}
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
Ok(())
}
fn compile_multiword_bitwise(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
left: &Expr,
right: &Expr,
n: u16,
) -> Result<(), CompileError> {
use crate::ast::BinOp;
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let bit_op = match op {
BinOp::Band => Op::BitAnd,
BinOp::Bor => Op::BitOr,
BinOp::Bxor => Op::BitXor,
_ => unreachable!("compile_multiword_bitwise called with a non-bitwise operator"),
};
let av = fc.declare_local("__mw_a");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
let bv = fc.declare_local("__mw_b");
compile_expr(fc, right)?;
fc.emit(Op::SetLocal(bv));
let mut rwords: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
let rk = fc.declare_local("__mw_r");
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::GetLocal(bv));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(bit_op);
fc.emit(Op::SetLocal(rk));
rwords.push(rk);
}
for &rk in &rwords {
fc.emit(Op::GetLocal(rk));
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
Ok(())
}
fn compile_multiword_bnot(
fc: &mut FuncCompiler,
operand: &Expr,
n: u16,
) -> Result<(), CompileError> {
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let neg1 = fc.add_constant(Value::Int(-1));
let av = fc.declare_local("__mw_a");
compile_expr(fc, operand)?;
fc.emit(Op::SetLocal(av));
let mut rwords: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
let rk = fc.declare_local("__mw_r");
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor);
fc.emit(Op::SetLocal(rk));
rwords.push(rk);
}
for &rk in &rwords {
fc.emit(Op::GetLocal(rk));
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
Ok(())
}
fn compile_multiword_binop(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
left: &Expr,
right: &Expr,
n: u16,
f: u16,
) -> Result<(), CompileError> {
use crate::ast::BinOp;
match op {
BinOp::Add => compile_multiword_add_sub(fc, left, right, n, false),
BinOp::Sub => compile_multiword_add_sub(fc, left, right, n, true),
BinOp::Mul if f == 0 => compile_multiword_mul(fc, left, right, n),
BinOp::Mul => compile_multiword_fixed_mul(fc, left, right, n, f),
BinOp::Div => compile_multiword_div(fc, left, right, n, f, false),
BinOp::Mod => compile_multiword_div(fc, left, right, n, f, true),
BinOp::Shl | BinOp::AShl | BinOp::ShrA | BinOp::ShrL => {
compile_multiword_shift(fc, op, left, right, n)
}
BinOp::Band | BinOp::Bor | BinOp::Bxor => compile_multiword_bitwise(fc, op, left, right, n),
BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => {
compile_multiword_compare(fc, op, left, right, n)
}
_ => Err(CompileError {
message: alloc::format!(
"Multiword<{}> does not yet support this operator (later phase)",
n
),
span: left.span(),
}),
}
}
#[allow(clippy::too_many_arguments)]
fn emit_limb_carry(
fc: &mut FuncCompiler,
is_sub: bool,
x: u16,
y: u16,
s: u16,
neg1: u16,
shift_c: u16,
one: u16,
) {
if is_sub {
fc.emit(Op::GetLocal(x));
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor); fc.emit(Op::GetLocal(y));
fc.emit(Op::BitAnd); fc.emit(Op::GetLocal(x));
fc.emit(Op::GetLocal(y));
fc.emit(Op::BitXor);
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor); fc.emit(Op::GetLocal(s));
fc.emit(Op::BitAnd); fc.emit(Op::BitOr);
} else {
fc.emit(Op::GetLocal(x));
fc.emit(Op::GetLocal(y));
fc.emit(Op::BitAnd); fc.emit(Op::GetLocal(x));
fc.emit(Op::GetLocal(y));
fc.emit(Op::BitXor); fc.emit(Op::GetLocal(s));
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor); fc.emit(Op::BitAnd); fc.emit(Op::BitOr);
}
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::Const(one));
fc.emit(Op::BitAnd);
}
fn compile_multiword_add_sub(
fc: &mut FuncCompiler,
left: &Expr,
right: &Expr,
n: u16,
is_sub: bool,
) -> Result<(), CompileError> {
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let neg1 = fc.add_constant(Value::Int(-1));
let one = fc.add_constant(Value::Int(1));
let zero = fc.add_constant(Value::Int(0));
let shift_c = fc.add_constant(Value::Int(word_bits - 1));
let av = fc.declare_local("__mw_a");
let bv = fc.declare_local("__mw_b");
let cv = fc.declare_local("__mw_carry");
let xi = fc.declare_local("__mw_x");
let yi = fc.declare_local("__mw_y");
let s1 = fc.declare_local("__mw_s1");
let c1 = fc.declare_local("__mw_c1");
let s2 = fc.declare_local("__mw_s2");
let c2 = fc.declare_local("__mw_c2");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
compile_expr(fc, right)?;
fc.emit(Op::SetLocal(bv));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(cv));
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(xi));
fc.emit(Op::GetLocal(bv));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(yi));
fc.emit(Op::GetLocal(xi));
fc.emit(Op::GetLocal(yi));
if is_sub {
fc.emit(Op::CheckedSub);
} else {
fc.emit(Op::CheckedAdd);
}
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s1));
emit_limb_carry(fc, is_sub, xi, yi, s1, neg1, shift_c, one);
fc.emit(Op::SetLocal(c1));
fc.emit(Op::GetLocal(s1));
fc.emit(Op::GetLocal(cv));
if is_sub {
fc.emit(Op::CheckedSub);
} else {
fc.emit(Op::CheckedAdd);
}
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s2));
emit_limb_carry(fc, is_sub, s1, cv, s2, neg1, shift_c, one);
fc.emit(Op::SetLocal(c2));
fc.emit(Op::GetLocal(c1));
fc.emit(Op::GetLocal(c2));
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(cv));
fc.emit(Op::GetLocal(s2));
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
Ok(())
}
fn compile_multiword_compare(
fc: &mut FuncCompiler,
op: crate::ast::BinOp,
left: &Expr,
right: &Expr,
n: u16,
) -> Result<(), CompileError> {
use crate::ast::BinOp;
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let neg1 = fc.add_constant(Value::Int(-1));
let one = fc.add_constant(Value::Int(1));
let zero = fc.add_constant(Value::Int(0));
let shift_c = fc.add_constant(Value::Int(word_bits - 1));
let sign = fc.add_constant(Value::Int(1i64 << (word_bits - 1)));
let av = fc.declare_local("__mw_a");
let bv = fc.declare_local("__mw_b");
let xi = fc.declare_local("__mw_x");
let yi = fc.declare_local("__mw_y");
let s = fc.declare_local("__mw_s");
let li = fc.declare_local("__mw_lt_i");
let gi = fc.declare_local("__mw_gt_i");
let eqi = fc.declare_local("__mw_eq_i");
let lt = fc.declare_local("__mw_lt");
let gt = fc.declare_local("__mw_gt");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
compile_expr(fc, right)?;
fc.emit(Op::SetLocal(bv));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(lt));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(gt));
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
let is_top = i == n - 1;
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
if is_top {
fc.emit(Op::Const(sign));
fc.emit(Op::BitXor);
}
fc.emit(Op::SetLocal(xi));
fc.emit(Op::GetLocal(bv));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
if is_top {
fc.emit(Op::Const(sign));
fc.emit(Op::BitXor);
}
fc.emit(Op::SetLocal(yi));
fc.emit(Op::GetLocal(xi));
fc.emit(Op::GetLocal(yi));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s));
emit_limb_carry(fc, true, xi, yi, s, neg1, shift_c, one);
fc.emit(Op::SetLocal(li));
fc.emit(Op::GetLocal(yi));
fc.emit(Op::GetLocal(xi));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s));
emit_limb_carry(fc, true, yi, xi, s, neg1, shift_c, one);
fc.emit(Op::SetLocal(gi));
fc.emit(Op::GetLocal(li));
fc.emit(Op::GetLocal(gi));
fc.emit(Op::BitOr);
fc.emit(Op::Const(one));
fc.emit(Op::BitXor);
fc.emit(Op::SetLocal(eqi));
fc.emit(Op::GetLocal(lt));
fc.emit(Op::GetLocal(eqi));
fc.emit(Op::BitAnd);
fc.emit(Op::GetLocal(li));
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(lt));
fc.emit(Op::GetLocal(gt));
fc.emit(Op::GetLocal(eqi));
fc.emit(Op::BitAnd);
fc.emit(Op::GetLocal(gi));
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(gt));
}
match op {
BinOp::Lt => {
fc.emit(Op::GetLocal(lt));
fc.emit(Op::Const(zero));
fc.emit(Op::CmpNe);
}
BinOp::Gt => {
fc.emit(Op::GetLocal(gt));
fc.emit(Op::Const(zero));
fc.emit(Op::CmpNe);
}
BinOp::LtEq => {
fc.emit(Op::GetLocal(gt));
fc.emit(Op::Const(zero));
fc.emit(Op::CmpEq);
}
BinOp::GtEq => {
fc.emit(Op::GetLocal(lt));
fc.emit(Op::Const(zero));
fc.emit(Op::CmpEq);
}
BinOp::Eq => {
fc.emit(Op::GetLocal(lt));
fc.emit(Op::GetLocal(gt));
fc.emit(Op::BitOr);
fc.emit(Op::Const(zero));
fc.emit(Op::CmpEq);
}
BinOp::NotEq => {
fc.emit(Op::GetLocal(lt));
fc.emit(Op::GetLocal(gt));
fc.emit(Op::BitOr);
fc.emit(Op::Const(zero));
fc.emit(Op::CmpNe);
}
_ => unreachable!("compile_multiword_compare called with a non-comparison operator"),
}
Ok(())
}
fn compile_multiword_mul(
fc: &mut FuncCompiler,
left: &Expr,
right: &Expr,
n: u16,
) -> Result<(), CompileError> {
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let column_modulus = 1u128 << (word_bits as u32);
if 2u128 * n as u128 + 1 >= column_modulus {
return Err(CompileError {
message: alloc::format!(
"Multiword<{}> multiply exceeds the multi-word accumulator capacity at a {}-bit word; reduce the word count",
n,
word_bits
),
span: left.span(),
});
}
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let neg1 = fc.add_constant(Value::Int(-1));
let one = fc.add_constant(Value::Int(1));
let zero = fc.add_constant(Value::Int(0));
let shift_c = fc.add_constant(Value::Int(word_bits - 1));
let av = fc.declare_local("__mw_a");
let bv = fc.declare_local("__mw_b");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
compile_expr(fc, right)?;
fc.emit(Op::SetLocal(bv));
let mut adig: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
let mut bdig: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
let ai = fc.declare_local("__mw_ai");
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(ai));
adig.push(ai);
let bj = fc.declare_local("__mw_bj");
fc.emit(Op::GetLocal(bv));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(bj));
bdig.push(bj);
}
let hs = fc.declare_local("__mw_hs");
let mut prods: alloc::vec::Vec<(u16, u16, u16, u16)> = alloc::vec::Vec::new();
for i in 0..n {
for j in 0..n {
if i as u32 + j as u32 > (n - 1) as u32 {
continue;
}
let lo = fc.declare_local("__mw_plo");
let hi = fc.declare_local("__mw_phi");
fc.emit(Op::GetLocal(adig[i as usize]));
fc.emit(Op::GetLocal(bdig[j as usize]));
fc.emit(Op::CheckedMul(0));
fc.emit(Op::PopN(1));
fc.emit(Op::SetLocal(hs));
fc.emit(Op::SetLocal(lo));
fc.emit(Op::GetLocal(hs));
fc.emit(Op::GetLocal(adig[i as usize]));
fc.emit(Op::GetLocal(bdig[j as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::BitAnd);
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::GetLocal(bdig[j as usize]));
fc.emit(Op::GetLocal(adig[i as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::BitAnd);
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(hi));
prods.push((i, j, lo, hi));
}
}
let acc_lo = fc.declare_local("__mw_acc_lo");
let acc_hi = fc.declare_local("__mw_acc_hi");
let tmp_s = fc.declare_local("__mw_tmp_s");
let carry = fc.declare_local("__mw_carry");
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(acc_lo));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(acc_hi));
let mut rwords: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for k in 0..n {
let mut terms: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for &(i, j, lo, hi) in &prods {
if i as u32 + j as u32 == k as u32 {
terms.push(lo);
}
if k > 0 && i as u32 + j as u32 == (k - 1) as u32 {
terms.push(hi);
}
}
for v in terms {
fc.emit(Op::GetLocal(acc_lo));
fc.emit(Op::GetLocal(v));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(tmp_s));
emit_limb_carry(fc, false, acc_lo, v, tmp_s, neg1, shift_c, one);
fc.emit(Op::SetLocal(carry));
fc.emit(Op::GetLocal(tmp_s));
fc.emit(Op::SetLocal(acc_lo));
fc.emit(Op::GetLocal(acc_hi));
fc.emit(Op::GetLocal(carry));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(acc_hi));
}
let rk = fc.declare_local("__mw_r");
fc.emit(Op::GetLocal(acc_lo));
fc.emit(Op::SetLocal(rk));
rwords.push(rk);
fc.emit(Op::GetLocal(acc_hi));
fc.emit(Op::SetLocal(acc_lo));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(acc_hi));
}
for &rk in &rwords {
fc.emit(Op::GetLocal(rk));
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn emit_multiword_highword_subtract(
fc: &mut FuncCompiler,
pwords: &[u16],
dig: &[u16],
mask: u16,
n: u16,
neg1: u16,
shift_c: u16,
one: u16,
) {
let zero = fc.add_constant(Value::Int(0));
let borrow = fc.declare_local("__mw_hb");
let term = fc.declare_local("__mw_ht");
let s1 = fc.declare_local("__mw_hs1");
let s2 = fc.declare_local("__mw_hs2");
let bo1 = fc.declare_local("__mw_hbo1");
let bo2 = fc.declare_local("__mw_hbo2");
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(borrow));
for i in 0..n as usize {
let hi_word = pwords[n as usize + i];
fc.emit(Op::GetLocal(dig[i]));
fc.emit(Op::GetLocal(mask));
fc.emit(Op::BitAnd);
fc.emit(Op::SetLocal(term));
fc.emit(Op::GetLocal(hi_word));
fc.emit(Op::GetLocal(term));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s1));
emit_limb_carry(fc, true, hi_word, term, s1, neg1, shift_c, one);
fc.emit(Op::SetLocal(bo1));
fc.emit(Op::GetLocal(s1));
fc.emit(Op::GetLocal(borrow));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s2));
emit_limb_carry(fc, true, s1, borrow, s2, neg1, shift_c, one);
fc.emit(Op::SetLocal(bo2));
fc.emit(Op::GetLocal(s2));
fc.emit(Op::SetLocal(hi_word));
fc.emit(Op::GetLocal(bo1));
fc.emit(Op::GetLocal(bo2));
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(borrow));
}
}
fn compile_multiword_fixed_mul(
fc: &mut FuncCompiler,
left: &Expr,
right: &Expr,
n: u16,
f: u16,
) -> Result<(), CompileError> {
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let total_bits = n as u128 * word_bits as u128;
if f as u128 > total_bits {
return Err(CompileError {
message: alloc::format!(
"Multiword<{}, {}> declares more fraction bits than the {}-bit N-word width holds",
n,
f,
total_bits
),
span: left.span(),
});
}
let column_modulus = 1u128 << (word_bits as u32);
if 2u128 * n as u128 + 1 >= column_modulus {
return Err(CompileError {
message: alloc::format!(
"Multiword<{}> multiply exceeds the multi-word accumulator capacity at a {}-bit word; reduce the word count",
n,
word_bits
),
span: left.span(),
});
}
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let neg1 = fc.add_constant(Value::Int(-1));
let one = fc.add_constant(Value::Int(1));
let zero = fc.add_constant(Value::Int(0));
let shift_c = fc.add_constant(Value::Int(word_bits - 1));
let av = fc.declare_local("__mw_a");
let bv = fc.declare_local("__mw_b");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
compile_expr(fc, right)?;
fc.emit(Op::SetLocal(bv));
let mut adig: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
let mut bdig: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
let ai = fc.declare_local("__mw_ai");
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(ai));
adig.push(ai);
let bj = fc.declare_local("__mw_bj");
fc.emit(Op::GetLocal(bv));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(bj));
bdig.push(bj);
}
let hs = fc.declare_local("__mw_hs");
let mut prods: alloc::vec::Vec<(u16, u16, u16, u16)> = alloc::vec::Vec::new();
for i in 0..n {
for j in 0..n {
let lo = fc.declare_local("__mw_plo");
let hi = fc.declare_local("__mw_phi");
fc.emit(Op::GetLocal(adig[i as usize]));
fc.emit(Op::GetLocal(bdig[j as usize]));
fc.emit(Op::CheckedMul(0));
fc.emit(Op::PopN(1));
fc.emit(Op::SetLocal(hs));
fc.emit(Op::SetLocal(lo));
fc.emit(Op::GetLocal(hs));
fc.emit(Op::GetLocal(adig[i as usize]));
fc.emit(Op::GetLocal(bdig[j as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::BitAnd);
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::GetLocal(bdig[j as usize]));
fc.emit(Op::GetLocal(adig[i as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::BitAnd);
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(hi));
prods.push((i, j, lo, hi));
}
}
let two_n = 2 * n as usize;
let acc_lo = fc.declare_local("__mw_acc_lo");
let acc_hi = fc.declare_local("__mw_acc_hi");
let tmp_s = fc.declare_local("__mw_tmp_s");
let carry = fc.declare_local("__mw_carry");
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(acc_lo));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(acc_hi));
let mut pwords: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for k in 0..two_n {
let mut terms: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for &(i, j, lo, hi) in &prods {
if i as usize + j as usize == k {
terms.push(lo);
}
if k > 0 && i as usize + j as usize == k - 1 {
terms.push(hi);
}
}
for v in terms {
fc.emit(Op::GetLocal(acc_lo));
fc.emit(Op::GetLocal(v));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(tmp_s));
emit_limb_carry(fc, false, acc_lo, v, tmp_s, neg1, shift_c, one);
fc.emit(Op::SetLocal(carry));
fc.emit(Op::GetLocal(tmp_s));
fc.emit(Op::SetLocal(acc_lo));
fc.emit(Op::GetLocal(acc_hi));
fc.emit(Op::GetLocal(carry));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(acc_hi));
}
let pk = fc.declare_local("__mw_p");
fc.emit(Op::GetLocal(acc_lo));
fc.emit(Op::SetLocal(pk));
pwords.push(pk);
fc.emit(Op::GetLocal(acc_hi));
fc.emit(Op::SetLocal(acc_lo));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(acc_hi));
}
let a_neg = fc.declare_local("__mw_aneg");
let b_neg = fc.declare_local("__mw_bneg");
fc.emit(Op::GetLocal(adig[(n - 1) as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(a_neg));
fc.emit(Op::GetLocal(bdig[(n - 1) as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(b_neg));
emit_multiword_highword_subtract(fc, &pwords, &adig, b_neg, n, neg1, shift_c, one);
emit_multiword_highword_subtract(fc, &pwords, &bdig, a_neg, n, neg1, shift_c, one);
let q = (f as usize) / (word_bits as usize);
let r = (f as usize) % (word_bits as usize);
debug_assert!(q + n as usize <= two_n, "shift window escapes the product");
let mut rwords: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
let (r_c, wr_c, mask_c) = if r != 0 {
let mask = ((1i128 << (word_bits as i128 - r as i128)) - 1) as i64;
(
Some(fc.add_constant(Value::Int(r as i64))),
Some(fc.add_constant(Value::Int(word_bits - r as i64))),
Some(fc.add_constant(Value::Int(mask))),
)
} else {
(None, None, None)
};
for k in 0..n as usize {
let rk = fc.declare_local("__mw_r");
if r == 0 {
fc.emit(Op::GetLocal(pwords[k + q]));
} else {
fc.emit(Op::GetLocal(pwords[k + q]));
fc.emit(Op::Const(r_c.unwrap()));
fc.emit(Op::Shr);
fc.emit(Op::Const(mask_c.unwrap()));
fc.emit(Op::BitAnd);
fc.emit(Op::GetLocal(pwords[k + q + 1]));
fc.emit(Op::Const(wr_c.unwrap()));
fc.emit(Op::Shl);
fc.emit(Op::BitOr);
}
fc.emit(Op::SetLocal(rk));
rwords.push(rk);
}
for &rk in &rwords {
fc.emit(Op::GetLocal(rk));
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn emit_multiword_cond_negate(
fc: &mut FuncCompiler,
words: &[u16],
mask: u16,
n: u16,
neg1: u16,
shift_c: u16,
one: u16,
) {
let carry = fc.declare_local("__mw_cn_c");
let t = fc.declare_local("__mw_cn_t");
let s = fc.declare_local("__mw_cn_s");
let bo = fc.declare_local("__mw_cn_bo");
fc.emit(Op::GetLocal(mask));
fc.emit(Op::Const(one));
fc.emit(Op::BitAnd);
fc.emit(Op::SetLocal(carry));
for &w in words.iter().take(n as usize) {
fc.emit(Op::GetLocal(w));
fc.emit(Op::GetLocal(mask));
fc.emit(Op::BitXor);
fc.emit(Op::SetLocal(t));
fc.emit(Op::GetLocal(t));
fc.emit(Op::GetLocal(carry));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s));
emit_limb_carry(fc, false, t, carry, s, neg1, shift_c, one);
fc.emit(Op::SetLocal(bo));
fc.emit(Op::GetLocal(s));
fc.emit(Op::SetLocal(w));
fc.emit(Op::GetLocal(bo));
fc.emit(Op::SetLocal(carry));
}
}
fn compile_multiword_div(
fc: &mut FuncCompiler,
left: &Expr,
right: &Expr,
n: u16,
f: u16,
is_mod: bool,
) -> Result<(), CompileError> {
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let n_bits = n as u128 * word_bits as u128;
let shift = if !is_mod && f > 0 { f as u128 } else { 0 };
if shift > n_bits {
return Err(CompileError {
message: alloc::format!(
"Multiword<{}, {}> declares more fraction bits than the {}-bit N-word width holds",
n,
f,
n_bits
),
span: left.span(),
});
}
let dividend_bits = n_bits + shift;
if dividend_bits > 512 {
return Err(CompileError {
message: alloc::format!(
"Multiword<{}> division at a {}-bit word unrolls to {} bit-steps, beyond the compiler's limit; reduce the word count",
n,
word_bits,
dividend_bits
),
span: left.span(),
});
}
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
let word_ty = TypeExpr::Prim(PrimType::Word, Span::default());
let elem_op = array_elem_operand(Some(&word_ty), &fc.type_info);
let neg1 = fc.add_constant(Value::Int(-1));
let one = fc.add_constant(Value::Int(1));
let zero = fc.add_constant(Value::Int(0));
let shift_c = fc.add_constant(Value::Int(word_bits - 1));
let av = fc.declare_local("__mw_a");
let bv = fc.declare_local("__mw_b");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(av));
compile_expr(fc, right)?;
fc.emit(Op::SetLocal(bv));
let mut adig: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
let mut bdig: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for i in 0..n {
let idx_c = fc.add_constant(Value::Int(i as i64));
let ai = fc.declare_local("__mw_ai");
fc.emit(Op::GetLocal(av));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(ai));
adig.push(ai);
let bj = fc.declare_local("__mw_bj");
fc.emit(Op::GetLocal(bv));
fc.emit(Op::Const(idx_c));
fc.emit(Op::GetIndex(elem_op));
fc.emit(Op::SetLocal(bj));
bdig.push(bj);
}
let b_or = fc.declare_local("__mw_bor");
fc.emit(Op::GetLocal(bdig[0]));
for w in bdig.iter().skip(1) {
fc.emit(Op::GetLocal(*w));
fc.emit(Op::BitOr);
}
fc.emit(Op::SetLocal(b_or));
fc.emit(Op::Const(one));
fc.emit(Op::GetLocal(b_or));
fc.emit(Op::Div);
fc.emit(Op::PopN(1));
let sign_a = fc.declare_local("__mw_sga");
let sign_b = fc.declare_local("__mw_sgb");
fc.emit(Op::GetLocal(adig[(n - 1) as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(sign_a));
fc.emit(Op::GetLocal(bdig[(n - 1) as usize]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::SetLocal(sign_b));
emit_multiword_cond_negate(fc, &adig, sign_a, n, neg1, shift_c, one);
emit_multiword_cond_negate(fc, &bdig, sign_b, n, neg1, shift_c, one);
let mut rword: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
let mut qword: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
let mut tent: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for _ in 0..n {
let rk = fc.declare_local("__mw_rr");
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(rk));
rword.push(rk);
let qk = fc.declare_local("__mw_qq");
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(qk));
qword.push(qk);
tent.push(fc.declare_local("__mw_tt"));
}
let s1 = fc.declare_local("__mw_ds1");
let s2 = fc.declare_local("__mw_ds2");
let bo1 = fc.declare_local("__mw_dbo1");
let bo2 = fc.declare_local("__mw_dbo2");
let borrow = fc.declare_local("__mw_dbor");
let mask = fc.declare_local("__mw_dmask");
let notmask = fc.declare_local("__mw_dnmask");
for i in (0..dividend_bits).rev() {
for j in (1..n as usize).rev() {
fc.emit(Op::GetLocal(rword[j]));
fc.emit(Op::Const(one));
fc.emit(Op::Shl);
fc.emit(Op::GetLocal(rword[j - 1]));
fc.emit(Op::Const(shift_c));
fc.emit(Op::Shr);
fc.emit(Op::Const(one));
fc.emit(Op::BitAnd);
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(rword[j]));
}
fc.emit(Op::GetLocal(rword[0]));
fc.emit(Op::Const(one));
fc.emit(Op::Shl);
let src = i as i128 - shift as i128;
if src >= 0 && (src as u128) < n_bits {
let src = src as u128;
let swi = (src / word_bits as u128) as usize;
let sbp = fc.add_constant(Value::Int((src % word_bits as u128) as i64));
fc.emit(Op::GetLocal(adig[swi]));
fc.emit(Op::Const(sbp));
fc.emit(Op::Shr);
fc.emit(Op::Const(one));
fc.emit(Op::BitAnd);
fc.emit(Op::BitOr);
}
fc.emit(Op::SetLocal(rword[0]));
fc.emit(Op::Const(zero));
fc.emit(Op::SetLocal(borrow));
for j in 0..n as usize {
fc.emit(Op::GetLocal(rword[j]));
fc.emit(Op::GetLocal(bdig[j]));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s1));
emit_limb_carry(fc, true, rword[j], bdig[j], s1, neg1, shift_c, one);
fc.emit(Op::SetLocal(bo1));
fc.emit(Op::GetLocal(s1));
fc.emit(Op::GetLocal(borrow));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(s2));
emit_limb_carry(fc, true, s1, borrow, s2, neg1, shift_c, one);
fc.emit(Op::SetLocal(bo2));
fc.emit(Op::GetLocal(s2));
fc.emit(Op::SetLocal(tent[j]));
fc.emit(Op::GetLocal(bo1));
fc.emit(Op::GetLocal(bo2));
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(borrow));
}
fc.emit(Op::Const(zero));
fc.emit(Op::GetLocal(borrow));
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(mask));
fc.emit(Op::GetLocal(mask));
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor);
fc.emit(Op::SetLocal(notmask));
for j in 0..n as usize {
fc.emit(Op::GetLocal(rword[j]));
fc.emit(Op::GetLocal(mask));
fc.emit(Op::BitAnd);
fc.emit(Op::GetLocal(tent[j]));
fc.emit(Op::GetLocal(notmask));
fc.emit(Op::BitAnd);
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(rword[j]));
}
if i < n_bits {
let qwi = (i / word_bits as u128) as usize;
let qbp = fc.add_constant(Value::Int((i % word_bits as u128) as i64));
fc.emit(Op::GetLocal(qword[qwi]));
fc.emit(Op::GetLocal(borrow));
fc.emit(Op::Const(one));
fc.emit(Op::BitXor);
fc.emit(Op::Const(qbp));
fc.emit(Op::Shl);
fc.emit(Op::BitOr);
fc.emit(Op::SetLocal(qword[qwi]));
}
}
let result = if is_mod {
emit_multiword_cond_negate(fc, &rword, sign_a, n, neg1, shift_c, one);
rword
} else {
let qsign = fc.declare_local("__mw_qsg");
fc.emit(Op::GetLocal(sign_a));
fc.emit(Op::GetLocal(sign_b));
fc.emit(Op::BitXor);
fc.emit(Op::SetLocal(qsign));
emit_multiword_cond_negate(fc, &qword, qsign, n, neg1, shift_c, one);
qword
};
for &w in &result {
fc.emit(Op::GetLocal(w));
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
Ok(())
}
pub fn compile(program: &Program) -> Result<Module, CompileError> {
compile_with_target(program, &crate::target::Target::host())
}
fn type_tag_for_param(param: &Param) -> crate::bytecode::TypeTag {
use crate::bytecode::TypeTag;
let Some(type_expr) = ¶m.type_expr else {
return TypeTag::Composite;
};
match type_expr {
TypeExpr::Prim(PrimType::Byte, _) => TypeTag::Byte,
TypeExpr::Prim(PrimType::Word, _) => TypeTag::Word,
TypeExpr::Prim(PrimType::Fixed(_), _) => TypeTag::Fixed,
TypeExpr::Prim(PrimType::Float, _) => TypeTag::Float,
TypeExpr::Prim(PrimType::Bool, _) => TypeTag::Bool,
TypeExpr::Prim(PrimType::Text, _) => TypeTag::Text,
TypeExpr::Unit(_) => TypeTag::Unit,
_ => TypeTag::Composite,
}
}
#[derive(Debug, Clone)]
pub struct CompileWarning {
pub message: String,
pub chunk_name: String,
}
pub const CHUNK_SIZE_HARD_LIMIT: usize = u16::MAX as usize;
pub const CHUNK_SIZE_SOFT_WARN_THRESHOLD: usize = (CHUNK_SIZE_HARD_LIMIT * 80) / 100;
pub fn check_chunk_size_against_limits(
chunk: &Chunk,
span: crate::token::Span,
warnings: &mut Vec<CompileWarning>,
) -> Result<(), CompileError> {
let op_count = chunk.ops.len();
if op_count > CHUNK_SIZE_HARD_LIMIT {
return Err(CompileError {
message: format!(
"chunk `{}` emitted {} ops, exceeding the V0.2.0 limit of {} \
(u16 control-flow target width); decompose the function into helpers",
chunk.name, op_count, CHUNK_SIZE_HARD_LIMIT,
),
span,
});
}
if op_count > CHUNK_SIZE_SOFT_WARN_THRESHOLD {
warnings.push(CompileWarning {
message: format!(
"chunk `{}` has {} ops, crossing the 80% soft-warning threshold of \
{} against the {} cap; consider decomposing the function into helpers",
chunk.name, op_count, CHUNK_SIZE_SOFT_WARN_THRESHOLD, CHUNK_SIZE_HARD_LIMIT,
),
chunk_name: chunk.name.clone(),
});
}
Ok(())
}
pub fn compile_with_target(
program: &Program,
target: &crate::target::Target,
) -> Result<Module, CompileError> {
compile_with_warnings(program, target).map(|(module, _warnings)| module)
}
pub fn compile_with_warnings(
program: &Program,
target: &crate::target::Target,
) -> Result<(Module, Vec<CompileWarning>), CompileError> {
compile_with_options(program, target, &CompileOptions::default())
}
#[derive(Debug, Clone, Default)]
pub struct CompileOptions {
pub emit_debug: bool,
}
pub fn compile_with_options(
program: &Program,
target: &crate::target::Target,
options: &CompileOptions,
) -> Result<(Module, Vec<CompileWarning>), CompileError> {
let mut warnings: Vec<CompileWarning> = Vec::new();
target.validate_against_runtime()?;
crate::target::validate_program_for_target(program, target)?;
let mut owned = program.clone();
normalize_fixed_defaults(&mut owned, target.fixed_default_frac_bits());
crate::typecheck::check_with_target(&mut owned, *target).map_err(|e| CompileError {
message: format!("type error: {}", e.message),
span: e.span,
})?;
let (mut owned, generic_provenance) = crate::monomorphize::monomorphize_with_provenance(owned);
crate::typecheck::check_with_target_recording(&mut owned, *target).map_err(|e| {
CompileError {
message: format!("type error after monomorphization: {}", e.message),
span: e.span,
}
})?;
let program = &owned;
let mut native_names: Vec<String> = Vec::new();
let mut native_return_types: Vec<Option<TypeExpr>> = Vec::new();
let mut native_map: BTreeMap<String, u16> = BTreeMap::new();
let mut native_externals: BTreeMap<String, bool> = BTreeMap::new();
for use_decl in &program.uses {
match &use_decl.import {
ImportItem::Name(name) => {
let full = if use_decl.path.is_empty() {
name.clone()
} else {
let mut full = String::new();
for (i, seg) in use_decl.path.iter().enumerate() {
if i > 0 {
full.push_str("::");
}
full.push_str(seg);
}
full.push_str("::");
full.push_str(name);
full
};
let idx = native_names.len() as u16;
native_map.insert(full.clone(), idx);
native_externals.insert(full.clone(), use_decl.is_external);
native_names.push(full);
native_return_types
.push(use_decl.signature.as_ref().map(|s| s.return_type.clone()));
}
ImportItem::Wildcard => {
}
}
}
let mut seen_shared_span: Option<crate::token::Span> = None;
let mut seen_private_span: Option<crate::token::Span> = None;
let mut seen_const_span: Option<crate::token::Span> = None;
for decl in &program.data_decls {
let dup_slot = match decl.visibility {
DataVisibility::Shared => &mut seen_shared_span,
DataVisibility::Private => &mut seen_private_span,
DataVisibility::Const => &mut seen_const_span,
};
if dup_slot.is_some() {
return Err(CompileError {
message: format!(
"at most one {} data block per program (R28); found duplicate `{}`",
match decl.visibility {
DataVisibility::Shared => "shared",
DataVisibility::Private => "private",
DataVisibility::Const => "const",
},
decl.name
),
span: decl.span,
});
}
*dup_slot = Some(decl.span);
}
let mut const_fields: BTreeMap<String, BTreeMap<String, crate::bytecode::ConstValue>> =
BTreeMap::new();
for decl in &program.data_decls {
match decl.visibility {
DataVisibility::Const => {
let mut block: BTreeMap<String, crate::bytecode::ConstValue> = BTreeMap::new();
for field in &decl.fields {
let lit = field.initializer.as_ref().ok_or_else(|| CompileError {
message: format!(
"const data field `{}.{}` is missing an initializer; const data fields require `= literal` initializers",
decl.name, field.name
),
span: field.span,
})?;
let cv = const_value_from_literal_for_field(
lit,
&field.type_expr,
decl.name.as_str(),
field.name.as_str(),
field.span,
)?;
block.insert(field.name.clone(), cv);
}
const_fields.insert(decl.name.clone(), block);
}
DataVisibility::Shared | DataVisibility::Private => {
for field in &decl.fields {
if field.initializer.is_some() {
return Err(CompileError {
message: format!(
"{} data field `{}.{}` has an initializer; only `const data` fields admit initializers",
match decl.visibility {
DataVisibility::Shared => "shared",
DataVisibility::Private => "private",
DataVisibility::Const => unreachable!(),
},
decl.name,
field.name
),
span: field.span,
});
}
}
}
}
}
let mut data_fields: BTreeMap<String, Vec<(String, u16)>> = BTreeMap::new();
let mut shared_slots: Vec<DataSlot> = Vec::new();
let mut private_slots: Vec<DataSlot> = Vec::new();
let mut data_slot_idx: u16 = 0;
for pass_visibility in [DataVisibility::Shared, DataVisibility::Private] {
for decl in &program.data_decls {
if decl.visibility != pass_visibility {
continue;
}
let mut fields = Vec::new();
for field in &decl.fields {
let mut visiting: BTreeSet<String> = BTreeSet::new();
validate_data_field_type(
&field.type_expr,
&program.types,
pass_visibility,
&mut visiting,
)?;
fields.push((field.name.clone(), data_slot_idx));
let n_slots = slots_for_data_type(&field.type_expr);
let visibility = match decl.visibility {
DataVisibility::Shared => crate::bytecode::SlotVisibility::Shared,
DataVisibility::Private => crate::bytecode::SlotVisibility::Private,
DataVisibility::Const => unreachable!(
"const data does not produce runtime slots; const fields handled separately"
),
};
let target = match pass_visibility {
DataVisibility::Shared => &mut shared_slots,
DataVisibility::Private => &mut private_slots,
DataVisibility::Const => unreachable!(),
};
if n_slots == 1 {
target.push(DataSlot {
name: format!("{}.{}", decl.name, field.name),
visibility,
});
} else {
for k in 0..n_slots {
target.push(DataSlot {
name: format!("{}.{}[{}]", decl.name, field.name, k),
visibility,
});
}
}
data_slot_idx = data_slot_idx
.checked_add(n_slots)
.ok_or_else(|| CompileError {
message: format!(
"data segment field `{}.{}` overflows the 16-bit slot index space",
decl.name, field.name
),
span: field.span,
})?;
}
data_fields.insert(decl.name.clone(), fields);
}
}
let shared_count = shared_slots.len() as u32;
let private_count = private_slots.len() as u32;
let mut data_layout_slots: Vec<DataSlot> = shared_slots;
data_layout_slots.append(&mut private_slots);
let mut data_layout = if data_layout_slots.is_empty() {
None
} else {
Some(DataLayout {
slots: data_layout_slots,
shared_layout: Vec::new(),
private_composite_layout: Vec::new(),
})
};
let mut synth_impl_methods: Vec<FunctionDef> = Vec::new();
for impl_block in &program.impls {
let head = type_expr_head_name(&impl_block.for_type);
for method in &impl_block.methods {
let mut renamed = method.clone();
renamed.name = format!("{}::{}::{}", impl_block.trait_name, head, method.name);
synth_impl_methods.push(renamed);
}
}
let mut groups: BTreeMap<String, Vec<&FunctionDef>> = BTreeMap::new();
for func in &program.functions {
groups.entry(func.name.clone()).or_default().push(func);
}
for func in &synth_impl_methods {
groups.entry(func.name.clone()).or_default().push(func);
}
let mut function_map: BTreeMap<String, u16> = BTreeMap::new();
for (chunk_idx, name) in groups.keys().enumerate() {
function_map.insert(name.clone(), chunk_idx as u16);
}
let mut type_info = TypeInfo {
word_bytes: (1usize << target.word_bits_log2) / 8,
float_bytes: (1usize << target.float_bits_log2) / 8,
..TypeInfo::default()
};
for type_def in &program.types {
match type_def {
TypeDef::Struct(s) => {
let mut fields = BTreeMap::new();
let mut order = Vec::with_capacity(s.fields.len());
for f in &s.fields {
fields.insert(f.name.clone(), f.type_expr.clone());
order.push((f.name.clone(), f.type_expr.clone()));
}
type_info.structs.insert(s.name.clone(), fields);
type_info.struct_field_order.insert(s.name.clone(), order);
type_info.struct_defs.insert(s.name.clone(), s.clone());
}
TypeDef::Enum(e) => {
let mut variants = BTreeMap::new();
let mut ordered: Vec<(String, i64)> = Vec::new();
for v in &e.variants {
variants.insert(v.name.clone(), v.fields.clone());
ordered.push((v.name.clone(), v.discriminant_value));
}
type_info.enums.insert(e.name.clone(), variants);
type_info.enum_variant_order.insert(e.name.clone(), ordered);
type_info.enum_defs.insert(e.name.clone(), e.clone());
}
TypeDef::Newtype(n) => {
type_info.newtype_names.insert(n.name.clone());
if let Some(pred) = &n.refinement {
type_info
.newtype_refinements
.insert(n.name.clone(), pred.clone());
}
}
}
}
for func in &program.functions {
type_info
.function_returns
.insert(func.name.clone(), func.return_type.clone());
}
for use_decl in &program.uses {
if let (ImportItem::Name(name), Some(sig)) = (&use_decl.import, &use_decl.signature) {
type_info
.function_returns
.entry(name.clone())
.or_insert_with(|| sig.return_type.clone());
if !use_decl.path.is_empty() {
let mut full = use_decl.path.join("::");
full.push_str("::");
full.push_str(name);
type_info
.function_returns
.entry(full)
.or_insert_with(|| sig.return_type.clone());
}
}
}
for func in &program.functions {
if !type_info
.newtype_refinements
.values()
.any(|p| p == &func.name)
{
continue;
}
if func.params.len() != 1 {
continue;
}
let param_name = match &func.params[0].pattern {
crate::ast::Pattern::Variable(name, _) => name.clone(),
_ => continue,
};
if !func.body.stmts.is_empty() {
continue;
}
let Some(tail) = func.body.tail_expr.as_ref() else {
continue;
};
type_info
.refinement_bodies
.insert(func.name.clone(), (param_name, (**tail).clone()));
}
for decl in &program.data_decls {
let mut fields = BTreeMap::new();
for f in &decl.fields {
fields.insert(f.name.clone(), f.type_expr.clone());
}
type_info.data_field_types.insert(decl.name.clone(), fields);
}
let function_summaries = compute_function_return_ranges(program, &type_info);
type_info.function_return_ranges = function_summaries;
let (persistent_composite_bytes, private_composite_layout) = {
let mut total: u32 = 0;
let mut table: Vec<crate::bytecode::PrivateCompositeSlot> = Vec::new();
let mut slot_idx: u32 = shared_count;
for decl in &program.data_decls {
if !matches!(decl.visibility, crate::ast::DataVisibility::Private) {
continue;
}
for field in &decl.fields {
let n = slots_for_data_type(&field.type_expr) as u32;
let leaf = innermost_non_array_type(&field.type_expr);
let body = data_field_pool_bytes(leaf, &type_info);
if body > 0 {
if n == 1 {
if let Ok(slot_u16) = u16::try_from(slot_idx) {
table.push(crate::bytecode::PrivateCompositeSlot {
slot: slot_u16,
offset: total,
});
}
total = total.saturating_add(u32::try_from(body).unwrap_or(u32::MAX));
} else {
for k in 0..n {
if let Ok(slot_u16) = u16::try_from(slot_idx + k) {
table.push(crate::bytecode::PrivateCompositeSlot {
slot: slot_u16,
offset: total,
});
}
total = total.saturating_add(u32::try_from(body).unwrap_or(u32::MAX));
}
}
}
slot_idx = slot_idx.saturating_add(n);
}
}
(total, table)
};
let (shared_data_flat_bytes, shared_slot_layout): (
u32,
Vec<crate::bytecode::SharedSlotLayout>,
) = {
let mut entries: Vec<crate::bytecode::SharedSlotLayout> = Vec::new();
let mut offset: u16 = 0;
for decl in &program.data_decls {
if !matches!(decl.visibility, crate::ast::DataVisibility::Shared) {
continue;
}
for field in &decl.fields {
let consumed = push_shared_slot_layout(
&field.type_expr,
&type_info,
offset,
field.span,
&mut entries,
)?;
offset = offset.checked_add(consumed).ok_or_else(|| CompileError {
message: String::from(
"shared data segment exceeds the 64KB flat host-buffer limit",
),
span: field.span,
})?;
}
}
(offset as u32, entries)
};
if let Some(dl) = data_layout.as_mut() {
dl.shared_layout = shared_slot_layout;
dl.private_composite_layout = private_composite_layout;
}
let mut chunks: Vec<Chunk> = Vec::new();
let mut signatures: Vec<crate::bytecode::ChunkSignature> = Vec::new();
#[cfg_attr(not(feature = "verify"), allow(unused_assignments, unused_variables))]
let mut may_intern_opaque = false;
for (name, defs) in &groups {
let generic_origin = generic_provenance
.get(name)
.map(|(origin, type_args)| (origin.as_str(), type_args.as_str()));
let (chunk, chunk_may_intern_opaque, signature) = compile_function_group(
name,
defs,
&function_map,
&native_map,
&native_externals,
&data_fields,
&const_fields,
&type_info,
&program.fn_expr_types,
options.emit_debug,
generic_origin,
)?;
#[cfg_attr(not(feature = "verify"), allow(unused_assignments))]
{
may_intern_opaque |= chunk_may_intern_opaque;
}
let span = defs
.first()
.map(|d| d.span)
.unwrap_or_else(crate::token::Span::default);
check_chunk_size_against_limits(&chunk, span, &mut warnings)?;
chunks.push(chunk);
signatures.push(signature);
}
let entry_point = function_map.get("main").map(|&idx| idx as usize);
if shared_count + private_count > 0 {
let mut private_slot_indices: Vec<u16> = Vec::new();
for decl in &program.data_decls {
if decl.visibility != DataVisibility::Private {
continue;
}
for field in &decl.fields {
if let Some(field_list) = data_fields.get(&decl.name)
&& let Some((_, slot_idx)) =
field_list.iter().find(|(name, _)| name == &field.name)
{
let n = slots_for_data_type(&field.type_expr);
for k in 0..n {
private_slot_indices.push(slot_idx.saturating_add(k));
}
}
}
}
if !private_slot_indices.is_empty() {
let mut written: alloc::collections::BTreeSet<u16> =
alloc::collections::BTreeSet::new();
for chunk in &chunks {
for op in &chunk.ops {
match op {
crate::bytecode::Op::SetData(slot) => {
written.insert(*slot);
}
crate::bytecode::Op::SetDataIndexed(base, len) => {
for k in 0..*len {
written.insert(base.saturating_add(k));
}
}
_ => {}
}
}
}
let all_unmutated = private_slot_indices
.iter()
.all(|slot| !written.contains(slot));
if all_unmutated {
let private_block_name = program
.data_decls
.iter()
.find(|d| d.visibility == DataVisibility::Private)
.map(|d| d.name.clone())
.unwrap_or_else(|| String::from("<unknown>"));
let private_span = program
.data_decls
.iter()
.find(|d| d.visibility == DataVisibility::Private)
.map(|d| d.span)
.unwrap_or_else(crate::token::Span::default);
return Err(CompileError {
message: format!(
"private data block `{}` is never mutated; declare it as `const data` with literal initializers instead (private data carries runtime cost that immutable fields do not need)",
private_block_name
),
span: private_span,
});
}
}
}
let native_return_shapes: Vec<crate::bytecode::WireShape> = native_return_types
.iter()
.map(|ret| match ret {
Some(ty) => wire_shape_of_type(ty, &type_info),
None => crate::bytecode::WireShape::Top,
})
.collect();
#[cfg_attr(not(feature = "verify"), allow(unused_mut))]
let mut module = Module {
schema_hash: crate::bytecode::compute_schema_hash(data_layout.as_ref()),
enum_layouts: build_enum_layouts(&type_info),
signatures,
native_return_shapes,
chunks,
native_names,
entry_point,
data_layout,
word_bits_log2: target.word_bits_log2,
addr_bits_log2: target.addr_bits_log2,
float_bits_log2: target.float_bits_log2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes,
flags: 0,
shared_data_bytes: shared_data_flat_bytes,
private_data_bytes: private_count.saturating_mul(crate::bytecode::VALUE_SLOT_SIZE_BYTES),
};
#[cfg(feature = "verify")]
{
let mut chunk_spans: BTreeMap<String, crate::token::Span> = BTreeMap::new();
for func in &program.functions {
chunk_spans.entry(func.name.clone()).or_insert(func.span);
}
for func in &synth_impl_methods {
chunk_spans.entry(func.name.clone()).or_insert(func.span);
}
let span_for = |name: &str| -> crate::token::Span {
chunk_spans
.get(name)
.copied()
.unwrap_or_else(crate::token::Span::default)
};
crate::verify::verify(&module).map_err(|e| CompileError {
message: format!("structural verification: {}: {}", e.chunk_name, e.message),
span: span_for(&e.chunk_name),
})?;
let mut max_wcet: u32 = 0;
let mut max_wcmu: u32 = 0;
let mut max_heap: u32 = 0;
let mut wcet_overflow = false;
let mut wcmu_overflow = false;
let per_chunk_obligations: alloc::vec::Vec<
alloc::vec::Vec<crate::verify::VerificationObligation>,
> = if options.emit_debug {
module
.chunks
.iter()
.map(|c| crate::verify::chunk_verification_obligations(c, &module))
.collect()
} else {
alloc::vec::Vec::new()
};
for (chunk_idx, chunk) in module.chunks.iter_mut().enumerate() {
if options.emit_debug {
let obligations = &per_chunk_obligations[chunk_idx];
let pool = chunk
.debug_pool
.get_or_insert_with(crate::debug_meta::DebugPool::default);
for ob in obligations {
let pass = intern_debug_string(pool, ob.pass);
let property = intern_debug_string(pool, ob.property);
pool.records.push(crate::debug_meta::DebugRecord {
op_index: ob.op_index,
kind: crate::debug_meta::DebugRecordKind::VerifierWitness,
operands: alloc::vec![pass, property],
});
}
}
if matches!(chunk.block_type, crate::bytecode::BlockType::Stream) {
match crate::verify::wcet_stream_iteration(chunk) {
Ok(c) => {
max_wcet = max_wcet.max(c);
if options.emit_debug {
let pool = chunk
.debug_pool
.get_or_insert_with(crate::debug_meta::DebugPool::default);
pool.records.push(crate::debug_meta::DebugRecord {
op_index: 0,
kind: crate::debug_meta::DebugRecordKind::WcetMarker,
operands: alloc::vec![0u16, (c & 0xFFFF) as u16, (c >> 16) as u16],
});
let pass = intern_debug_string(pool, "resource-bounds");
let property =
intern_debug_string(pool, "wcet-per-iteration-bound-proven");
pool.records.push(crate::debug_meta::DebugRecord {
op_index: 0,
kind: crate::debug_meta::DebugRecordKind::VerifierWitness,
operands: alloc::vec![pass, property],
});
}
}
Err(_) => {
wcet_overflow = true;
}
}
match crate::verify::wcmu_stream_iteration(chunk) {
Ok((stack, heap)) => {
let total = stack.saturating_add(heap);
if total == u32::MAX {
wcmu_overflow = true;
} else {
max_wcmu = max_wcmu.max(total);
max_heap = max_heap.max(heap);
if options.emit_debug {
let pool = chunk
.debug_pool
.get_or_insert_with(crate::debug_meta::DebugPool::default);
let pass = intern_debug_string(pool, "resource-bounds");
let property =
intern_debug_string(pool, "wcmu-per-iteration-bound-proven");
pool.records.push(crate::debug_meta::DebugRecord {
op_index: 0,
kind: crate::debug_meta::DebugRecordKind::VerifierWitness,
operands: alloc::vec![pass, property],
});
}
}
}
Err(_) => {
wcmu_overflow = true;
}
}
} else if options.emit_debug
&& matches!(
chunk.block_type,
crate::bytecode::BlockType::Func | crate::bytecode::BlockType::Reentrant
)
{
if crate::verify::wcet_whole_chunk(chunk).is_ok() {
let pool = chunk
.debug_pool
.get_or_insert_with(crate::debug_meta::DebugPool::default);
let pass = intern_debug_string(pool, "resource-bounds");
let property = intern_debug_string(pool, "wcet-per-chunk-bound-proven");
pool.records.push(crate::debug_meta::DebugRecord {
op_index: 0,
kind: crate::debug_meta::DebugRecordKind::VerifierWitness,
operands: alloc::vec![pass, property],
});
}
if let Ok((stack, heap)) = crate::verify::wcmu_whole_chunk(chunk)
&& stack.saturating_add(heap) != u32::MAX
{
let pool = chunk
.debug_pool
.get_or_insert_with(crate::debug_meta::DebugPool::default);
let pass = intern_debug_string(pool, "resource-bounds");
let property = intern_debug_string(pool, "wcmu-per-chunk-bound-proven");
pool.records.push(crate::debug_meta::DebugRecord {
op_index: 0,
kind: crate::debug_meta::DebugRecordKind::VerifierWitness,
operands: alloc::vec![pass, property],
});
}
}
}
module.wcet_cycles = if wcet_overflow { u32::MAX } else { max_wcet };
module.wcmu_bytes = if wcmu_overflow { u32::MAX } else { max_wcmu };
let word_bytes = (1usize << module.word_bits_log2) / 8;
let arc_bytes = core::mem::size_of::<alloc::sync::Arc<dyn crate::opaque::HostOpaque>>();
module.aux_arena_bytes = if wcmu_overflow || word_bytes == 0 || !may_intern_opaque {
0
} else {
let max_interns = (max_heap as usize).div_ceil(word_bytes);
max_interns.saturating_mul(arc_bytes).min(u32::MAX as usize) as u32
};
let entry_name = "main";
let entry_decl = program.functions.iter().find(|f| f.name == entry_name);
let entry_chunk_idx = module
.chunks
.iter()
.position(|c| c.name == entry_name)
.or(module.entry_point);
let entry_boundary_carries_text = match (
entry_chunk_idx,
crate::verify::module_chunk_text_analyses(&module),
) {
(Some(idx), Ok(analyses)) => analyses
.get(idx)
.map(|a| a.returns_text || a.yields_text)
.unwrap_or(true),
_ => true,
};
let signature_uses_text = entry_decl
.map(|f| {
let unused_text_params: alloc::collections::BTreeSet<String> = f
.params
.iter()
.filter(|p| {
p.type_expr
.as_ref()
.map(type_expr_carries_text)
.unwrap_or(false)
})
.filter_map(|p| param_binding_name(&p.pattern))
.filter(|name| !param_name_is_used(&f.body, name))
.collect();
let any_used_text_param = f.params.iter().any(|p| {
let text = p
.type_expr
.as_ref()
.map(type_expr_carries_text)
.unwrap_or(false);
if !text {
return false;
}
match param_binding_name(&p.pattern) {
Some(name) => !unused_text_params.contains(&name),
None => true,
}
});
let declared_return_carries_text =
type_expr_carries_text(&f.return_type) && entry_boundary_carries_text;
any_used_text_param || declared_return_carries_text
})
.unwrap_or(false);
let provably_ephemeral = module.private_data_bytes == 0 && !signature_uses_text;
if provably_ephemeral {
module.flags |= crate::bytecode::FLAG_EPHEMERAL;
}
if let Some(decl) = entry_decl
&& decl.ephemeral
&& !provably_ephemeral
{
let mut reason = alloc::string::String::new();
if module.private_data_bytes != 0 {
reason.push_str("module declares `private data` which persists across resets");
} else if signature_uses_text {
if !reason.is_empty() {
reason.push_str(" and ");
}
reason.push_str(
"entry function signature carries `Text` which is arena-resident at runtime",
);
}
return Err(CompileError {
message: alloc::format!(
"`ephemeral` modifier on `{}` is not provable: {}",
decl.name,
reason
),
span: decl.span,
});
}
}
{
let entry_name = "main";
for func in &program.functions {
if func.signed && func.name != entry_name {
return Err(CompileError {
message: alloc::format!(
"`signed` modifier on `{}` is invalid: the modifier is admissible only on the module's entry function (`main`)",
func.name,
),
span: func.span,
});
}
}
let entry_signed = program
.functions
.iter()
.find(|f| f.name == entry_name)
.map(|f| f.signed)
.unwrap_or(false);
if entry_signed {
module.flags |= crate::wire_format::FLAG_REQUIRES_SIGNATURE;
}
}
Ok((module, warnings))
}
#[cfg(feature = "verify")]
fn type_expr_carries_text(t: &TypeExpr) -> bool {
match t {
TypeExpr::Prim(PrimType::Text, _) => true,
TypeExpr::Tuple(parts, _) => parts.iter().any(type_expr_carries_text),
TypeExpr::Array(elem, _, _) => type_expr_carries_text(elem),
TypeExpr::Option(inner, _) => type_expr_carries_text(inner),
TypeExpr::Labelled(inner, _, _) => type_expr_carries_text(inner),
TypeExpr::NegativeLabelled(inner, _, _) => type_expr_carries_text(inner),
_ => false,
}
}
fn type_expr_head_name(t: &TypeExpr) -> String {
match t {
TypeExpr::Prim(p, _) => match p {
PrimType::Byte => String::from("Byte"),
PrimType::Word => String::from("Word"),
PrimType::Fixed(_) => String::from("Fixed"),
PrimType::Float => String::from("Float"),
PrimType::Bool => String::from("bool"),
PrimType::Text => String::from("Text"),
},
TypeExpr::Unit(_) => String::from("()"),
TypeExpr::Multiword(_, _, _) => String::from("Multiword"),
TypeExpr::Named(name, _, _, _) => name.clone(),
TypeExpr::Tuple(_, _) => String::from("tuple"),
TypeExpr::Array(_, _, _) => String::from("array"),
TypeExpr::Option(_, _) => String::from("Option"),
TypeExpr::Labelled(inner, _, _) => type_expr_head_name(inner),
TypeExpr::NegativeLabelled(inner, _, _) => type_expr_head_name(inner),
}
}
#[cfg(feature = "verify")]
fn param_binding_name(pattern: &Pattern) -> Option<String> {
match pattern {
Pattern::Variable(name, _) => Some(name.clone()),
_ => None,
}
}
#[cfg(feature = "verify")]
fn param_name_is_used(body: &Block, name: &str) -> bool {
fn expr_uses(expr: &Expr, name: &str) -> bool {
match expr {
Expr::Ident { name: n, .. } => n == name,
Expr::Literal { .. } | Expr::Placeholder { .. } => false,
Expr::BinOp { left, right, .. } => expr_uses(left, name) || expr_uses(right, name),
Expr::UnaryOp { operand, .. } => expr_uses(operand, name),
Expr::Call { args, .. } => args.iter().any(|e| expr_uses(e, name)),
Expr::Pipeline { left, args, .. } => {
expr_uses(left, name) || args.iter().any(|e| expr_uses(e, name))
}
Expr::MethodCall { receiver, args, .. } => {
expr_uses(receiver, name) || args.iter().any(|e| expr_uses(e, name))
}
Expr::FieldAccess { object, .. } => expr_uses(object, name),
Expr::TupleIndex { object, .. } => expr_uses(object, name),
Expr::ArrayIndex { object, index, .. } => {
expr_uses(object, name) || expr_uses(index, name)
}
Expr::If {
condition,
then_block,
else_block,
..
} => {
expr_uses(condition, name)
|| block_uses(then_block, name)
|| else_block
.as_ref()
.map(|b| block_uses(b, name))
.unwrap_or(false)
}
Expr::Match {
scrutinee, arms, ..
} => {
expr_uses(scrutinee, name)
|| arms.iter().any(|arm| {
arm.guard
.as_ref()
.map(|g| expr_uses(g, name))
.unwrap_or(false)
|| expr_uses(&arm.expr, name)
})
}
Expr::TupleLiteral { elements, .. } => elements.iter().any(|e| expr_uses(e, name)),
Expr::ArrayLiteral { elements, .. } => elements.iter().any(|e| expr_uses(e, name)),
Expr::StructInit { fields, .. } => fields.iter().any(|f| expr_uses(&f.value, name)),
Expr::EnumVariant { args, .. } => args.iter().any(|e| expr_uses(e, name)),
Expr::Yield { value, .. } => expr_uses(value, name),
Expr::Cast { expr: inner, .. } => expr_uses(inner, name),
Expr::Loop { body, .. } => block_uses(body, name),
Expr::Closure { body, .. } => block_uses(body, name),
Expr::ClosureRef { captures, .. } => captures.iter().any(|c| c == name),
Expr::Checked { op_expr, arms, .. } => {
expr_uses(op_expr, name) || arms.iter().any(|arm| expr_uses(&arm.body, name))
}
Expr::SaturateMax { .. } | Expr::SaturateMin { .. } => false,
Expr::Classify { value, .. } | Expr::Declassify { value, .. } => expr_uses(value, name),
}
}
fn block_uses(block: &Block, name: &str) -> bool {
for stmt in &block.stmts {
match stmt {
Stmt::Let(l) => {
if expr_uses(&l.value, name) {
return true;
}
}
Stmt::For(f) => {
let iter_uses = match &f.iterable {
Iterable::Expr(e) => expr_uses(e, name),
Iterable::Range(lo, hi) => expr_uses(lo, name) || expr_uses(hi, name),
};
if iter_uses || block_uses(&f.body, name) {
return true;
}
}
Stmt::Break(_) => {}
Stmt::DataFieldAssign { value, .. } => {
if expr_uses(value, name) {
return true;
}
}
Stmt::DataFieldIndexAssign { indices, value, .. } => {
if indices.iter().any(|e| expr_uses(e, name)) || expr_uses(value, name) {
return true;
}
}
Stmt::Expr(e) => {
if expr_uses(e, name) {
return true;
}
}
Stmt::Assert { cond, .. } => {
if expr_uses(cond, name) {
return true;
}
}
}
}
if let Some(tail) = &block.tail_expr {
return expr_uses(tail, name);
}
false
}
block_uses(body, name)
}
fn resolve_const_enum_discriminants(
cv: &mut crate::bytecode::ConstValue,
order: &BTreeMap<String, Vec<(String, i64)>>,
) {
use crate::bytecode::ConstValue;
match cv {
ConstValue::Enum {
type_name,
variant,
discriminant,
fields,
} => {
if discriminant.is_none() {
*discriminant = order
.get(type_name)
.and_then(|vs| vs.iter().find(|(n, _)| n == variant).map(|(_, d)| *d));
}
for f in fields {
resolve_const_enum_discriminants(f, order);
}
}
ConstValue::Tuple(items) | ConstValue::Array(items) => {
for i in items {
resolve_const_enum_discriminants(i, order);
}
}
ConstValue::Struct { fields, .. } => {
for (_, f) in fields {
resolve_const_enum_discriminants(f, order);
}
}
_ => {}
}
}
fn const_value_any(init: &ConstInitializer) -> crate::bytecode::ConstValue {
use crate::bytecode::ConstValue;
match init {
ConstInitializer::Scalar(Literal::Int(n)) => ConstValue::Int(*n),
ConstInitializer::Scalar(Literal::Byte(b)) => ConstValue::Byte(*b),
ConstInitializer::Scalar(Literal::Fixed { raw, .. }) => ConstValue::Fixed(*raw),
#[cfg(feature = "floats")]
ConstInitializer::Scalar(Literal::Float(f)) => ConstValue::Float(*f),
#[cfg(not(feature = "floats"))]
ConstInitializer::Scalar(Literal::Float(_)) => {
unreachable!("float literals are rejected at lex time when the `floats` feature is off")
}
ConstInitializer::Scalar(Literal::Bool(b)) => ConstValue::Bool(*b),
ConstInitializer::Scalar(Literal::String(s)) => ConstValue::StaticStr(s.clone()),
ConstInitializer::Scalar(Literal::Unit) => ConstValue::Unit,
ConstInitializer::Tuple(elements) => {
let out: Vec<ConstValue> = elements.iter().map(const_value_any).collect();
ConstValue::Tuple(out)
}
ConstInitializer::Array(elements) => {
let out: Vec<ConstValue> = elements.iter().map(const_value_any).collect();
ConstValue::Array(out)
}
ConstInitializer::Struct { name, fields } => {
let out: Vec<(String, ConstValue)> = fields
.iter()
.map(|(fname, finit)| (fname.clone(), const_value_any(finit)))
.collect();
ConstValue::Struct {
type_name: name.clone(),
fields: out,
}
}
ConstInitializer::Enum {
enum_name,
variant,
args,
} => {
let out: Vec<ConstValue> = args.iter().map(const_value_any).collect();
ConstValue::Enum {
type_name: enum_name.clone(),
variant: variant.clone(),
discriminant: None,
fields: out,
}
}
}
}
fn const_value_from_literal_for_field(
init: &ConstInitializer,
field_type: &TypeExpr,
data_name: &str,
field_name: &str,
span: crate::token::Span,
) -> Result<crate::bytecode::ConstValue, CompileError> {
use crate::bytecode::ConstValue;
match (init, field_type) {
(ConstInitializer::Scalar(lit), TypeExpr::Prim(p, _)) => match (lit, p) {
(Literal::Int(n), PrimType::Word) => Ok(ConstValue::Int(*n)),
(Literal::Int(n), PrimType::Byte) => {
if !(0..=0xFF).contains(n) {
return Err(CompileError {
message: format!(
"const data field `{}.{}` initializer {} does not fit in `Byte` (range 0..=255)",
data_name, field_name, n
),
span,
});
}
Ok(ConstValue::Byte(*n as u8))
}
#[cfg(feature = "floats")]
(Literal::Float(f), PrimType::Float) => Ok(ConstValue::Float(*f)),
(Literal::Bool(b), PrimType::Bool) => Ok(ConstValue::Bool(*b)),
(Literal::String(s), PrimType::Text) => Ok(ConstValue::StaticStr(s.clone())),
_ => Err(CompileError {
message: format!(
"const data field `{}.{}` initializer is incompatible with the declared field type",
data_name, field_name
),
span,
}),
},
(ConstInitializer::Scalar(Literal::Unit), TypeExpr::Unit(_)) => Ok(ConstValue::Unit),
(ConstInitializer::Tuple(elements), TypeExpr::Tuple(elem_types, _)) => {
if elements.len() != elem_types.len() {
return Err(CompileError {
message: format!(
"const data field `{}.{}` tuple initializer has {} element(s), expected {}",
data_name,
field_name,
elements.len(),
elem_types.len()
),
span,
});
}
let mut out: Vec<ConstValue> = Vec::with_capacity(elements.len());
for (e, t) in elements.iter().zip(elem_types.iter()) {
out.push(const_value_from_literal_for_field(
e, t, data_name, field_name, span,
)?);
}
Ok(ConstValue::Tuple(out))
}
(ConstInitializer::Array(elements), TypeExpr::Array(elem_type, len, _)) => {
if elements.len() != len.as_lit().unwrap_or(-1) as usize {
return Err(CompileError {
message: format!(
"const data field `{}.{}` array initializer has {} element(s), expected {}",
data_name,
field_name,
elements.len(),
len
),
span,
});
}
let mut out: Vec<ConstValue> = Vec::with_capacity(elements.len());
for e in elements {
out.push(const_value_from_literal_for_field(
e, elem_type, data_name, field_name, span,
)?);
}
Ok(ConstValue::Array(out))
}
(ConstInitializer::Struct { name, fields }, TypeExpr::Named(decl_name, _, _, _)) => {
if decl_name != name {
return Err(CompileError {
message: format!(
"const data field `{}.{}` initializer is `{}` but declared type is `{}`",
data_name, field_name, name, decl_name
),
span,
});
}
let mut out: Vec<(String, ConstValue)> = Vec::with_capacity(fields.len());
for (fname, finit) in fields {
let cv = const_value_from_literal_for_field(
finit,
&TypeExpr::Prim(PrimType::Word, span),
data_name,
field_name,
span,
)
.unwrap_or_else(|_| {
const_value_any(finit)
});
out.push((fname.clone(), cv));
}
Ok(ConstValue::Struct {
type_name: name.clone(),
fields: out,
})
}
(
ConstInitializer::Enum {
enum_name,
variant,
args,
},
TypeExpr::Named(decl_name, _, _, _),
) => {
if decl_name != enum_name {
return Err(CompileError {
message: format!(
"const data field `{}.{}` initializer is `{}::{}` but declared type is `{}`",
data_name, field_name, enum_name, variant, decl_name
),
span,
});
}
let out: Vec<ConstValue> = args.iter().map(const_value_any).collect();
Ok(ConstValue::Enum {
type_name: enum_name.clone(),
variant: variant.clone(),
discriminant: None,
fields: out,
})
}
_ => Err(CompileError {
message: format!(
"const data field `{}.{}` initializer is incompatible with the declared field type",
data_name, field_name
),
span,
}),
}
}
fn slots_for_data_type(type_expr: &TypeExpr) -> u16 {
match type_expr {
TypeExpr::Array(elem, len, _) => {
let elem_slots = slots_for_data_type(elem) as u32;
let total = elem_slots.saturating_mul(len.as_lit().unwrap_or(0) as u32);
total.min(u16::MAX as u32) as u16
}
_ => 1,
}
}
struct DataIndexedChain<'a> {
data_name: &'a str,
field: &'a str,
indices: Vec<&'a Expr>,
}
fn data_indexed_chain<'a>(object: &'a Expr, last_index: &'a Expr) -> Option<DataIndexedChain<'a>> {
let mut indices: Vec<&'a Expr> = Vec::new();
indices.push(last_index);
let mut current = object;
loop {
match current {
Expr::ArrayIndex { object, index, .. } => {
indices.push(index);
current = object.as_ref();
}
Expr::FieldAccess { object, field, .. } => {
if let Expr::Ident { name, .. } = object.as_ref() {
indices.reverse();
return Some(DataIndexedChain {
data_name: name.as_str(),
field: field.as_str(),
indices,
});
}
return None;
}
_ => return None,
}
}
}
fn emit_indexed_offset(
fc: &mut FuncCompiler,
field_type: &TypeExpr,
indices: &[&Expr],
span: Span,
) -> Result<u16, CompileError> {
let single_level = indices.len() == 1;
let mut current_type = field_type.clone();
let mut emitted_first = false;
for idx_expr in indices {
let (elem_type, len) = match current_type {
TypeExpr::Array(elem, len, _) => {
let len = len.as_lit().ok_or_else(|| CompileError {
message: alloc::format!("data array size `{}` is not a resolved constant", len),
span,
})?;
(*elem, len)
}
_ => {
return Err(CompileError {
message: String::from(
"indexed access on a non-array data field; only `[T; N]` data fields admit `field[i]`",
),
span,
});
}
};
if len < 0 {
return Err(CompileError {
message: format!("data array length must be non-negative, got {}", len),
span,
});
}
if len > u16::MAX as i64 {
return Err(CompileError {
message: format!(
"data array length {} exceeds the 16-bit bound the bytecode supports",
len
),
span,
});
}
let len_u16 = len as u16;
let stride = slots_for_data_type(&elem_type);
compile_expr(fc, idx_expr)?;
if !single_level {
fc.emit(Op::BoundsCheck(len_u16));
}
if stride != 1 {
let stride_const = fc.add_constant(Value::Int(stride as i64));
fc.emit(Op::Const(stride_const));
fc.emit(Op::CheckedMul(0));
fc.emit(Op::PopN(2));
}
if emitted_first {
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
} else {
emitted_first = true;
}
current_type = elem_type;
}
if matches!(current_type, TypeExpr::Array(_, _, _)) {
return Err(CompileError {
message: String::from(
"indexed access does not descend to a scalar; provide one index per array level",
),
span,
});
}
Ok(slots_for_data_type(field_type))
}
fn emit_data_indexed_read(
fc: &mut FuncCompiler,
chain: DataIndexedChain<'_>,
span: Span,
) -> Result<(), CompileError> {
if !fc.is_data_block(chain.data_name) {
return Err(CompileError {
message: format!("unknown data block: {}", chain.data_name),
span,
});
}
if fc.is_const_data_block(chain.data_name) {
let cv = fc
.const_data_field_value(chain.data_name, chain.field)
.ok_or_else(|| CompileError {
message: format!(
"unknown const data field: {}.{}",
chain.data_name, chain.field
),
span,
})?;
let idx = fc.add_const_value(cv);
fc.emit(Op::Const(idx));
let mut cur_ty = fc
.type_info
.data_field_types
.get(chain.data_name)
.and_then(|m| m.get(chain.field))
.cloned();
for index_expr in chain.indices {
compile_expr(fc, index_expr)?;
let elem_ty = cur_ty.as_ref().and_then(element_type_of);
fc.emit(Op::GetIndex(array_elem_operand(
elem_ty.as_ref(),
&fc.type_info,
)));
cur_ty = elem_ty;
fc.record_operator_site(&span);
}
return Ok(());
}
let base = fc
.resolve_data_field(chain.data_name, chain.field)
.ok_or_else(|| CompileError {
message: format!("unknown data field: {}.{}", chain.data_name, chain.field),
span,
})?;
let field_type = fc
.type_info
.data_field_types
.get(chain.data_name)
.and_then(|fields| fields.get(chain.field))
.cloned()
.ok_or_else(|| CompileError {
message: format!(
"data field {}.{} has no recorded type",
chain.data_name, chain.field
),
span,
})?;
let total = emit_indexed_offset(fc, &field_type, &chain.indices, span)?;
fc.emit(Op::GetDataIndexed(base, total));
fc.record_operator_site(&span);
Ok(())
}
fn emit_data_indexed_write(
fc: &mut FuncCompiler,
chain: DataIndexedChain<'_>,
span: Span,
) -> Result<(), CompileError> {
if !fc.is_data_block(chain.data_name) {
return Err(CompileError {
message: format!("unknown data block: {}", chain.data_name),
span,
});
}
if fc.is_const_data_block(chain.data_name) {
return Err(CompileError {
message: format!(
"cannot assign to `{}.{}` because `{}` is declared `const data`; const data is immutable",
chain.data_name, chain.field, chain.data_name
),
span,
});
}
let base = fc
.resolve_data_field(chain.data_name, chain.field)
.ok_or_else(|| CompileError {
message: format!("unknown data field: {}.{}", chain.data_name, chain.field),
span,
})?;
let field_type = fc
.type_info
.data_field_types
.get(chain.data_name)
.and_then(|fields| fields.get(chain.field))
.cloned()
.ok_or_else(|| CompileError {
message: format!(
"data field {}.{} has no recorded type",
chain.data_name, chain.field
),
span,
})?;
let total = emit_indexed_offset(fc, &field_type, &chain.indices, span)?;
fc.emit(Op::SetDataIndexed(base, total));
fc.record_operator_site(&span);
Ok(())
}
fn validate_data_field_type(
type_expr: &TypeExpr,
types: &[TypeDef],
visibility: DataVisibility,
visiting: &mut BTreeSet<String>,
) -> Result<(), CompileError> {
match type_expr {
TypeExpr::Multiword(_, _, _) => Ok(()),
TypeExpr::Prim(prim, span) => match prim {
PrimType::Byte
| PrimType::Word
| PrimType::Fixed(_)
| PrimType::Float
| PrimType::Bool => Ok(()),
PrimType::Text => match visibility {
DataVisibility::Private | DataVisibility::Const => Ok(()),
DataVisibility::Shared => Err(CompileError {
message: String::from(
"data field type Text is not admissible in a `shared` data \
segment: a host-owned text slot is not yet supported",
),
span: *span,
}),
},
},
TypeExpr::Unit(_) => Ok(()),
TypeExpr::Tuple(elems, _) => {
for elem in elems {
validate_data_field_type(elem, types, visibility, visiting)?;
}
Ok(())
}
TypeExpr::Array(elem, _len, _) => {
validate_data_field_type(elem, types, visibility, visiting)
}
TypeExpr::Option(inner, _) => validate_data_field_type(inner, types, visibility, visiting),
TypeExpr::Labelled(inner, _, _) => {
validate_data_field_type(inner, types, visibility, visiting)
}
TypeExpr::NegativeLabelled(inner, _, _) => {
validate_data_field_type(inner, types, visibility, visiting)
}
TypeExpr::Named(name, _args, _, span) => {
if visiting.contains(name) {
return Err(CompileError {
message: format!(
"recursive type {} cannot appear in a data segment field: \
the data segment requires statically known fixed size",
name
),
span: *span,
});
}
let type_def = types.iter().find(|td| match td {
TypeDef::Struct(s) => &s.name == name,
TypeDef::Enum(e) => &e.name == name,
TypeDef::Newtype(n) => &n.name == name,
});
match type_def {
Some(TypeDef::Struct(s)) => {
visiting.insert(name.clone());
for field in &s.fields {
validate_data_field_type(&field.type_expr, types, visibility, visiting)?;
}
visiting.remove(name);
Ok(())
}
Some(TypeDef::Enum(e)) => {
visiting.insert(name.clone());
for variant in &e.variants {
for ftype in &variant.fields {
validate_data_field_type(ftype, types, visibility, visiting)?;
}
}
visiting.remove(name);
Ok(())
}
Some(TypeDef::Newtype(n)) => {
visiting.insert(name.clone());
validate_data_field_type(&n.underlying, types, visibility, visiting)?;
visiting.remove(name);
Ok(())
}
None => Err(CompileError {
message: format!(
"data field type {} is not a struct or enum: opaque types \
are not yet admissible in data segment fields",
name
),
span: *span,
}),
}
}
}
}
fn pattern_shape_eq(a: &Pattern, b: &Pattern) -> bool {
match (a, b) {
(
Pattern::Wildcard(_) | Pattern::Variable(_, _),
Pattern::Wildcard(_) | Pattern::Variable(_, _),
) => true,
(Pattern::Literal(la, _), Pattern::Literal(lb, _)) => la == lb,
(Pattern::Tuple(pa, _), Pattern::Tuple(pb, _)) => {
pa.len() == pb.len()
&& pa
.iter()
.zip(pb.iter())
.all(|(a, b)| pattern_shape_eq(a, b))
}
(Pattern::Enum(ea, va, sa, _), Pattern::Enum(eb, vb, sb, _)) => {
ea == eb
&& va == vb
&& sa.len() == sb.len()
&& sa
.iter()
.zip(sb.iter())
.all(|(a, b)| pattern_shape_eq(a, b))
}
(Pattern::Struct(na, fa, _), Pattern::Struct(nb, fb, _)) => {
na == nb
&& fa.len() == fb.len()
&& fa.iter().zip(fb.iter()).all(|(a, b)| {
a.name == b.name
&& match (&a.pattern, &b.pattern) {
(Some(pa), Some(pb)) => pattern_shape_eq(pa, pb),
(None, None) => true,
_ => false,
}
})
}
_ => false,
}
}
#[allow(clippy::too_many_arguments)]
fn compile_function_group(
name: &str,
defs: &[&FunctionDef],
function_map: &BTreeMap<String, u16>,
native_map: &BTreeMap<String, u16>,
native_externals: &BTreeMap<String, bool>,
data_fields: &BTreeMap<String, Vec<(String, u16)>>,
const_fields: &BTreeMap<String, BTreeMap<String, crate::bytecode::ConstValue>>,
type_info: &TypeInfo,
fn_expr_types: &BTreeMap<String, BTreeMap<crate::token::Span, TypeExpr>>,
emit_debug: bool,
generic_origin: Option<(&str, &str)>,
) -> Result<(Chunk, bool, crate::bytecode::ChunkSignature), CompileError> {
for (i, later) in defs.iter().enumerate().skip(1) {
for earlier in &defs[..i] {
if later.params.len() != earlier.params.len() {
continue;
}
let shape_match = later
.params
.iter()
.zip(earlier.params.iter())
.all(|(l, e)| pattern_shape_eq(&l.pattern, &e.pattern));
if shape_match && earlier.guard.is_none() && later.guard.is_none() {
return Err(CompileError {
message: alloc::format!(
"function head `{}` is dead code: an earlier head with the same pattern shape and no guard already matches every value reaching it",
name
),
span: later.span,
});
}
}
}
let first = defs[0];
let block_type = match first.category {
FunctionCategory::Fn => BlockType::Func,
FunctionCategory::Yield => BlockType::Reentrant,
FunctionCategory::Loop => BlockType::Stream,
};
let param_count = first.params.len() as u8;
let signature = chunk_signature_for(first, block_type, type_info);
let mut fc = FuncCompiler::new(
name,
block_type,
function_map.clone(),
native_map.clone(),
native_externals.clone(),
data_fields.clone(),
const_fields.clone(),
type_info.clone(),
fn_expr_types.get(name).cloned().unwrap_or_default(),
emit_debug,
);
fc.chunk.param_count = param_count;
fc.chunk.param_types = first.params.iter().map(type_tag_for_param).collect();
if let Some((origin, type_args)) = generic_origin {
fc.record_generic_instantiation(origin, type_args);
}
let mut param_slots = Vec::new();
for i in 0..param_count {
let slot = fc.declare_local(&format!("__param{}", i));
param_slots.push(slot);
}
if defs.len() == 1 && !has_non_trivial_pattern(&first.params) && first.guard.is_none() {
for (i, param) in first.params.iter().enumerate() {
bind_param_pattern(
&mut fc,
¶m.pattern,
param_slots[i],
param.type_expr.clone(),
);
if let crate::ast::Pattern::Variable(_, _) = ¶m.pattern {
if let Some(crate::ast::TypeExpr::Named(type_name, _, _, _)) = ¶m.type_expr
&& let Some(pred_name) =
fc.type_info.newtype_refinements.get(type_name).cloned()
&& let Some((pred_param, body)) =
fc.type_info.refinement_bodies.get(&pred_name).cloned()
&& let Some(range) = predicate_true_set(&body, &pred_param)
{
fc.local_ranges.insert(param_slots[i], range);
} else if let Some(natural) = natural_range_of_type_expr(¶m.type_expr) {
fc.local_ranges.insert(param_slots[i], natural);
}
}
}
if block_type == BlockType::Stream {
fc.emit(Op::Stream);
compile_block(&mut fc, &first.body)?;
fc.emit(Op::PopN(1)); fc.emit(Op::Reset);
} else {
compile_block(&mut fc, &first.body)?;
fc.emit(Op::Return);
}
} else {
let stream_dispatch = block_type == BlockType::Stream;
let mut loop_marker: Option<usize> = None;
let mut stream_break_addrs: Vec<usize> = Vec::new();
if stream_dispatch {
fc.emit(Op::Stream);
loop_marker = Some(fc.emit_jump(Op::Loop(0)));
}
let mut fail_jumps: Vec<usize> = Vec::new();
for def in defs {
for addr in fail_jumps.drain(..).rev() {
fc.patch_jump(addr);
fc.emit(Op::EndIf);
}
fc.begin_scope();
for (i, param) in def.params.iter().enumerate() {
let fail = compile_pattern_test(
&mut fc,
¶m.pattern,
param_slots[i],
param.type_expr.as_ref(),
)?;
fail_jumps.extend(fail);
}
for (i, param) in def.params.iter().enumerate() {
compile_pattern_bind_typed(
&mut fc,
¶m.pattern,
param_slots[i],
param.type_expr.clone(),
)?;
}
if let Some(guard) = &def.guard {
compile_expr(&mut fc, guard)?;
let fail = fc.emit_jump(Op::If(0));
fail_jumps.push(fail);
}
compile_block(&mut fc, &def.body)?;
if stream_dispatch {
fc.emit(Op::PopN(1));
let break_addr = fc.emit_jump(Op::Break(0));
stream_break_addrs.push(break_addr);
} else {
fc.emit(Op::Return);
}
fc.end_scope();
}
for addr in fail_jumps.drain(..).rev() {
fc.patch_jump(addr);
fc.emit(Op::EndIf);
}
fc.emit(Op::Trap(crate::bytecode::TrapKind::NoMatchingHead.code()));
if stream_dispatch {
let loop_ip = loop_marker.expect("Stream dispatch sets loop_marker");
fc.emit(Op::EndLoop((loop_ip + 1) as u16));
fc.patch_jump(loop_ip);
for addr in &stream_break_addrs {
fc.patch_jump(*addr);
}
fc.emit(Op::Reset);
}
}
fc.record_function_entry(&first.span);
let may_intern_opaque = fc.may_intern_opaque;
Ok((fc.finish(), may_intern_opaque, signature))
}
fn has_non_trivial_pattern(params: &[Param]) -> bool {
params
.iter()
.any(|p| !matches!(p.pattern, Pattern::Variable(_, _)))
}
fn bind_param_pattern(fc: &mut FuncCompiler, pattern: &Pattern, slot: u16, ty: Option<TypeExpr>) {
if let Pattern::Variable(name, _) = pattern {
fc.locals.push(Local {
name: name.clone(),
slot,
depth: fc.scope_depth,
ty,
});
}
}
fn compile_block(fc: &mut FuncCompiler, block: &Block) -> Result<(), CompileError> {
fc.begin_scope();
for stmt in &block.stmts {
compile_stmt(fc, stmt)?;
}
if let Some(tail) = &block.tail_expr {
let tail_start_op = fc.chunk.ops.len();
compile_expr(fc, tail)?;
fc.record_statement(tail_start_op, &tail.span());
} else {
fc.emit(Op::PushImmediate(0));
}
fc.end_scope();
Ok(())
}
fn compile_data_field_assign(
fc: &mut FuncCompiler,
data_name: &str,
field: &str,
value: &Expr,
span: Span,
) -> Result<(), CompileError> {
if fc.is_const_data_block(data_name) {
return Err(CompileError {
message: format!(
"cannot assign to `{}.{}` because `{}` is declared `const data`; const data is immutable",
data_name, field, data_name
),
span,
});
}
let slot = fc
.resolve_data_field(data_name, field)
.ok_or_else(|| CompileError {
message: format!("unknown data field: {}.{}", data_name, field),
span,
})?;
compile_expr(fc, value)?;
fc.emit(Op::SetData(slot));
Ok(())
}
fn stmt_span(stmt: &Stmt) -> crate::token::Span {
match stmt {
Stmt::Let(l) => l.span,
Stmt::For(f) => f.span,
Stmt::Break(s) => *s,
Stmt::DataFieldAssign { span, .. } => *span,
Stmt::DataFieldIndexAssign { span, .. } => *span,
Stmt::Expr(e) => e.span(),
Stmt::Assert { span, .. } => *span,
}
}
fn compile_stmt(fc: &mut FuncCompiler, stmt: &Stmt) -> Result<(), CompileError> {
let stmt_start_op = fc.chunk.ops.len();
match stmt {
Stmt::Let(let_stmt) => {
let ty = let_stmt
.type_expr
.clone()
.or_else(|| infer_expr_type(fc, &let_stmt.value));
let folded = fold_to_int(&let_stmt.value, &|n| fc.local_const_lookup(n));
compile_expr(fc, &let_stmt.value)?;
compile_let_pattern_typed(fc, &let_stmt.pattern, ty)?;
if let (Some(value), crate::ast::Pattern::Variable(name, _)) =
(folded, &let_stmt.pattern)
&& let Some(slot) = fc.resolve_local(name.as_str())
{
fc.local_const_values.insert(slot, value);
}
}
Stmt::For(for_stmt) => {
compile_for(fc, for_stmt)?;
}
Stmt::Break(span) => {
if fc.loop_breaks.is_empty() {
return Err(CompileError {
message: String::from("break outside of loop"),
span: *span,
});
}
let addr = fc.emit_jump(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(addr);
}
}
Stmt::DataFieldAssign {
data_name,
field,
value,
span,
} => {
compile_data_field_assign(fc, data_name, field, value, *span)?;
}
Stmt::DataFieldIndexAssign {
data_name,
field,
indices,
value,
span,
} => {
compile_expr(fc, value)?;
let index_refs: Vec<&Expr> = indices.iter().collect();
emit_data_indexed_write(
fc,
DataIndexedChain {
data_name: data_name.as_str(),
field: field.as_str(),
indices: index_refs,
},
*span,
)?;
}
Stmt::Expr(expr) => {
compile_expr(fc, expr)?;
fc.emit(Op::PopN(1));
}
Stmt::Assert {
cond,
message,
span,
} => {
if fc.emit_debug {
compile_expr(fc, cond)?;
fc.emit(Op::Not);
let skip = fc.emit_jump(Op::If(0));
let trap_op = fc.emit(Op::Trap(crate::bytecode::TrapKind::AssertionFailed.code()));
fc.patch_jump(skip);
fc.emit(Op::EndIf);
fc.record_assert(trap_op, span, message.as_deref());
}
}
}
fc.record_statement(stmt_start_op, &stmt_span(stmt));
Ok(())
}
fn collect_checked_arm_bindings(kind: &crate::ast::CheckedArmKind, out: &mut Vec<String>) {
use crate::ast::CheckedArmKind as K;
let mut push = |p: &Pattern| {
if let Pattern::Variable(name, _) = p {
out.push(name.clone());
}
};
match kind {
K::Ok(p) | K::ZeroDivisor(p) => push(p),
K::Overflow(h, l) | K::Underflow(h, l) => {
push(h);
if let Some(l) = l {
push(l);
}
}
_ => {}
}
}
fn infer_arm_body_type(
fc: &FuncCompiler,
body: &Expr,
bound: &[String],
operand_ty: &TypeExpr,
) -> Option<TypeExpr> {
match body {
Expr::TupleLiteral { elements, span } => {
let mut tys = Vec::with_capacity(elements.len());
for e in elements {
tys.push(infer_arm_body_type(fc, e, bound, operand_ty)?);
}
Some(TypeExpr::Tuple(tys, *span))
}
Expr::Ident { name, .. } if bound.iter().any(|b| b == name) => Some(operand_ty.clone()),
_ => infer_expr_type(fc, body),
}
}
fn infer_expr_type(fc: &FuncCompiler, expr: &Expr) -> Option<TypeExpr> {
if let Some(ty) = fc.expr_types.get(&expr.span()) {
return Some(ty.clone());
}
match expr {
Expr::StructInit { name, span, .. } => {
Some(TypeExpr::Named(name.clone(), Vec::new(), Vec::new(), *span))
}
Expr::EnumVariant {
enum_name,
variant,
args,
span,
..
} => {
if enum_name == "Option" && variant == "Some" {
let payload = args.first().and_then(|a| infer_expr_type(fc, a))?;
Some(TypeExpr::Option(Box::new(payload), *span))
} else {
Some(TypeExpr::Named(
enum_name.clone(),
Vec::new(),
Vec::new(),
*span,
))
}
}
Expr::Call { name, .. } => fc.type_info.function_returns.get(name).cloned(),
Expr::Ident { name, .. } => fc.local_type(name).cloned(),
Expr::FieldAccess { object, field, .. } => {
let owner = fc.struct_name_of(object)?;
let field_type = fc
.type_info
.structs
.get(&owner)
.or_else(|| fc.type_info.data_field_types.get(&owner))
.and_then(|fields| fields.get(field))?;
Some(field_type.clone())
}
Expr::ArrayLiteral { elements, span } => {
let elem_ty = elements.first().and_then(|e| infer_expr_type(fc, e))?;
Some(TypeExpr::array_lit(
Box::new(elem_ty),
elements.len() as i64,
*span,
))
}
Expr::TupleLiteral { elements, span } => {
let mut elem_tys: Vec<TypeExpr> = Vec::with_capacity(elements.len());
for e in elements {
elem_tys.push(infer_expr_type(fc, e)?);
}
Some(TypeExpr::Tuple(elem_tys, *span))
}
Expr::ArrayIndex { object, .. } => {
let object_ty = infer_expr_type(fc, object)?;
element_type_of(&object_ty)
}
Expr::TupleIndex { object, index, .. } => {
let object_ty = infer_expr_type(fc, object)?;
match object_ty {
TypeExpr::Tuple(elems, _) => elems.get(*index as usize).cloned(),
_ => None,
}
}
Expr::Match { arms, .. } => {
let first = arms.first()?;
infer_expr_type(fc, &first.expr)
}
Expr::Literal { value, span } => Some(match value {
Literal::Int(_) => TypeExpr::Prim(PrimType::Word, *span),
Literal::Float(_) => TypeExpr::Prim(PrimType::Float, *span),
Literal::Byte(_) => TypeExpr::Prim(PrimType::Byte, *span),
Literal::Fixed { frac_bits, .. } => {
TypeExpr::Prim(PrimType::Fixed(Some(*frac_bits)), *span)
}
Literal::Bool(_) => TypeExpr::Prim(PrimType::Bool, *span),
Literal::String(_) => TypeExpr::Prim(PrimType::Text, *span),
Literal::Unit => TypeExpr::Unit(*span),
}),
Expr::Cast { target, .. } => Some(target.clone()),
Expr::BinOp {
left,
right,
op,
span,
..
} => {
use crate::ast::BinOp;
match op {
BinOp::Add
| BinOp::Sub
| BinOp::Mul
| BinOp::Div
| BinOp::Mod
| BinOp::Band
| BinOp::Bor
| BinOp::Bxor => infer_expr_type(fc, left).or_else(|| infer_expr_type(fc, right)),
BinOp::Shl | BinOp::AShl | BinOp::ShrA | BinOp::ShrL => infer_expr_type(fc, left),
BinOp::Eq
| BinOp::NotEq
| BinOp::Lt
| BinOp::Gt
| BinOp::LtEq
| BinOp::GtEq
| BinOp::And
| BinOp::Or
| BinOp::Xor
| BinOp::Andalso
| BinOp::Orelse => Some(TypeExpr::Prim(PrimType::Bool, *span)),
}
}
Expr::UnaryOp { operand, .. } => infer_expr_type(fc, operand),
Expr::Checked { op_expr, arms, .. } => match op_expr.as_ref() {
Expr::Cast {
target: TypeExpr::Named(n, args, _, sp),
..
} if fc.type_info.enum_variant_order.contains_key(n) => {
Some(TypeExpr::Named(n.clone(), args.clone(), Vec::new(), *sp))
}
_ => {
let operand_ty = infer_expr_type(fc, op_expr)?;
let first = arms.first()?;
let mut bound: Vec<String> = Vec::new();
collect_checked_arm_bindings(&first.kind, &mut bound);
infer_arm_body_type(fc, &first.body, &bound, &operand_ty)
}
},
_ => None,
}
}
fn type_expr_head(ty: &TypeExpr) -> Option<String> {
use alloc::string::ToString;
match ty {
TypeExpr::Prim(p, _) => Some(
match p {
PrimType::Byte => "Byte",
PrimType::Word => "Word",
PrimType::Fixed(_) => "Fixed",
PrimType::Float => "Float",
PrimType::Bool => "bool",
PrimType::Text => "Text",
}
.to_string(),
),
TypeExpr::Unit(_) => Some("()".to_string()),
TypeExpr::Multiword(_, _, _) => Some("Multiword".to_string()),
TypeExpr::Tuple(_, _) => Some("tuple".to_string()),
TypeExpr::Array(_, _, _) => Some("array".to_string()),
TypeExpr::Option(_, _) => Some("Option".to_string()),
TypeExpr::Named(name, _, _, _) => Some(name.clone()),
TypeExpr::Labelled(inner, _, _) => type_expr_head(inner),
TypeExpr::NegativeLabelled(inner, _, _) => type_expr_head(inner),
}
}
fn type_flat_size(ty: &TypeExpr, ti: &TypeInfo) -> Option<usize> {
if ti.word_bytes == 0 {
return None;
}
ti.layout_context()
.layout_for(ty)
.ok()?
.flat_byte_size(ti.word_bytes, ti.float_bytes)
}
fn build_enum_layouts(ti: &TypeInfo) -> Vec<crate::bytecode::EnumLayout> {
use crate::bytecode::{EnumLayout, EnumVariantDisc};
ti.enum_variant_order
.iter()
.map(|(enum_name, variants)| {
let ty = TypeExpr::Named(enum_name.clone(), Vec::new(), Vec::new(), Span::default());
let min_payload = type_flat_size(&ty, ti)
.and_then(|total| total.checked_sub(ti.word_bytes))
.and_then(|payload_max| u32::try_from(payload_max).ok())
.unwrap_or(0);
EnumLayout {
type_name: enum_name.clone(),
variants: variants
.iter()
.map(|(name, disc)| EnumVariantDisc {
name: name.clone(),
disc: *disc,
})
.collect(),
min_payload,
}
})
.collect()
}
fn chunk_signature_for(
first: &FunctionDef,
block_type: BlockType,
ti: &TypeInfo,
) -> crate::bytecode::ChunkSignature {
let params: Vec<crate::bytecode::WireShape> = first
.params
.iter()
.map(|p| wire_shape_of_type_opt(p.type_expr.as_ref(), ti))
.collect();
let ret = wire_shape_of_type(&first.return_type, ti);
let resume = if matches!(block_type, BlockType::Stream) {
params
.first()
.copied()
.unwrap_or(crate::bytecode::WireShape::Top)
} else {
crate::bytecode::WireShape::Top
};
crate::bytecode::ChunkSignature {
params,
ret,
resume,
}
}
fn wire_shape_of_type_opt(ty: Option<&TypeExpr>, ti: &TypeInfo) -> crate::bytecode::WireShape {
match ty {
Some(ty) => wire_shape_of_type(ty, ti),
None => crate::bytecode::WireShape::Top,
}
}
fn wire_shape_of_type(ty: &TypeExpr, ti: &TypeInfo) -> crate::bytecode::WireShape {
use crate::bytecode::WireShape;
use crate::value_layout::{CompositeKind, LayoutDescriptor};
let Ok(desc) = ti.layout_context().layout_for(ty) else {
return WireShape::Top;
};
let composite_kind = match &desc {
LayoutDescriptor::Scalar(k) => return WireShape::Scalar { kind: k.to_tag() },
LayoutDescriptor::Tuple(_) => CompositeKind::Tuple,
LayoutDescriptor::Array { .. } => CompositeKind::Array,
LayoutDescriptor::Struct { .. } => CompositeKind::Struct,
LayoutDescriptor::Enum { .. } => CompositeKind::Enum,
};
match u32::try_from(desc.size_in_bytes(ti.word_bytes, ti.float_bytes)) {
Ok(size) => WireShape::Flat {
kind: composite_kind.to_tag(),
size,
},
Err(_) => WireShape::Top,
}
}
fn innermost_non_array_type(ty: &TypeExpr) -> &TypeExpr {
let mut cur = ty;
while let TypeExpr::Array(elem, _, _) = cur {
cur = elem;
}
cur
}
fn data_field_pool_bytes(ty: &TypeExpr, ti: &TypeInfo) -> usize {
match ty {
TypeExpr::Array(_, _, _) => 0,
_ => match ti.layout_context().layout_for(ty) {
Ok(crate::value_layout::LayoutDescriptor::Scalar(_)) => 0,
Ok(layout) => layout
.flat_byte_size(ti.word_bytes, ti.float_bytes)
.unwrap_or(0),
Err(_) => 0,
},
}
}
fn push_shared_slot_layout(
ty: &TypeExpr,
ti: &TypeInfo,
base_offset: u16,
span: crate::token::Span,
out: &mut Vec<crate::bytecode::SharedSlotLayout>,
) -> Result<u16, CompileError> {
let layout = ti
.layout_context()
.layout_for(ty)
.map_err(|_| CompileError {
message: String::from(
"shared data field has no statically known flat layout for the host buffer",
),
span,
})?;
push_shared_layout_desc(&layout, ti, base_offset, span, out)
}
fn push_shared_layout_desc(
layout: &crate::value_layout::LayoutDescriptor,
ti: &TypeInfo,
base_offset: u16,
span: crate::token::Span,
out: &mut Vec<crate::bytecode::SharedSlotLayout>,
) -> Result<u16, CompileError> {
use crate::value_layout::{CompositeKind, LayoutDescriptor as LD, ScalarKind};
let overflow = || CompileError {
message: String::from("shared data segment exceeds the 64KB flat host-buffer limit"),
span,
};
match layout {
LD::Scalar(kind) => {
if matches!(kind, ScalarKind::Text | ScalarKind::Opaque) {
return Err(CompileError {
message: String::from(
"shared data field of reference type (Text or opaque) cannot live in a \
flat host buffer",
),
span,
});
}
let size = u16::try_from(kind.size_in_bytes(ti.word_bytes, ti.float_bytes))
.map_err(|_| overflow())?;
out.push(crate::bytecode::SharedSlotLayout {
offset: base_offset,
kind: kind.to_tag(),
len: 0,
});
Ok(size)
}
LD::Array { element, count } => {
let mut off = base_offset;
let mut total: u16 = 0;
for _ in 0..*count {
let consumed = push_shared_layout_desc(element, ti, off, span, out)?;
off = off.checked_add(consumed).ok_or_else(overflow)?;
total = total.checked_add(consumed).ok_or_else(overflow)?;
}
Ok(total)
}
LD::Tuple(_) | LD::Struct { .. } | LD::Enum { .. } => {
let composite_kind = match layout {
LD::Tuple(_) => CompositeKind::Tuple,
LD::Struct { .. } => CompositeKind::Struct,
LD::Enum { .. } => CompositeKind::Enum,
_ => unreachable!("matched Tuple/Struct/Enum above"),
};
let len = layout
.flat_byte_size(ti.word_bytes, ti.float_bytes)
.ok_or_else(|| CompileError {
message: String::from(
"shared data composite field is not flat (it carries a reference); not \
admissible in a flat host buffer",
),
span,
})?;
let len_u16 = u16::try_from(len).map_err(|_| overflow())?;
out.push(crate::bytecode::SharedSlotLayout {
offset: base_offset,
kind: crate::bytecode::SHARED_SLOT_COMPOSITE_FLAG | composite_kind.to_tag(),
len: len_u16,
});
Ok(len_u16)
}
}
}
fn flat_alloc_bytes(ty: &TypeExpr, ti: &TypeInfo) -> Option<u16> {
let size = type_flat_size(ty, ti)?;
if size <= u16::MAX as usize {
Some(size as u16)
} else {
None
}
}
fn conservative_alloc_bytes(count: u16) -> u16 {
(count as u32 * crate::bytecode::VALUE_SLOT_SIZE_BYTES).min(u16::MAX as u32) as u16
}
enum FlatFieldForm {
Scalar(crate::value_layout::ScalarKind),
Nested(u16, crate::value_layout::CompositeKind),
NotFlat,
}
fn classify_flat_field(ty: &TypeExpr, ti: &TypeInfo) -> FlatFieldForm {
if ti.word_bytes == 0 {
return FlatFieldForm::NotFlat;
}
let Ok(descriptor) = ti.layout_context().layout_for(ty) else {
return FlatFieldForm::NotFlat;
};
if let Some(kind) = descriptor.flat_scalar_kind() {
if matches!(kind, crate::value_layout::ScalarKind::Text)
&& ti.word_bytes < core::mem::size_of::<usize>()
{
return FlatFieldForm::NotFlat;
}
return FlatFieldForm::Scalar(kind);
}
if let (Some(size), Some(variant)) = (
descriptor.flat_byte_size(ti.word_bytes, ti.float_bytes),
descriptor.flat_composite_kind(),
) && size <= u16::MAX as usize
{
return FlatFieldForm::Nested(size as u16, variant);
}
FlatFieldForm::NotFlat
}
fn layout_has_opaque_leaf(d: &crate::value_layout::LayoutDescriptor) -> bool {
use crate::value_layout::{LayoutDescriptor as L, ScalarKind};
match d {
L::Scalar(ScalarKind::Opaque) => true,
L::Scalar(_) => false,
L::Struct { fields, .. } => fields.iter().any(|(_, f)| layout_has_opaque_leaf(f)),
L::Enum { variants, .. } => variants
.iter()
.any(|(_, ps)| ps.iter().any(layout_has_opaque_leaf)),
L::Tuple(elems) => elems.iter().any(layout_has_opaque_leaf),
L::Array { element, .. } => layout_has_opaque_leaf(element),
}
}
fn type_may_intern_opaque(ty: &TypeExpr, ti: &TypeInfo) -> bool {
ti.layout_context()
.layout_for(ty)
.map(|d| layout_has_opaque_leaf(&d))
.unwrap_or(true)
}
fn elem_may_intern_opaque(fc: &FuncCompiler, elem: &Expr) -> bool {
match infer_expr_type(fc, elem) {
None => true,
Some(ty) => type_may_intern_opaque(&ty, &fc.type_info),
}
}
fn array_elem_operand(elem_ty: Option<&TypeExpr>, ti: &TypeInfo) -> ArrayElem {
match elem_ty.map(|ty| classify_flat_field(ty, ti)) {
Some(FlatFieldForm::Scalar(kind)) => ArrayElem::Flat { kind },
Some(FlatFieldForm::Nested(size, variant)) => ArrayElem::FlatNested { size, variant },
_ => ArrayElem::Boxed,
}
}
fn tuple_field_access(
fc: &FuncCompiler,
elem_types: &[Option<TypeExpr>],
index: usize,
) -> TupleField {
let boxed = TupleField::Boxed { index: index as u8 };
let wb = fc.type_info.word_bytes;
if wb == 0 {
return boxed;
}
let mut offset = 0usize;
let mut field_at = None;
for (i, ty) in elem_types.iter().enumerate() {
let Some(ty) = ty else { return boxed };
let Some(size) = type_flat_size(ty, &fc.type_info) else {
return boxed;
};
if i == index {
field_at = Some((offset, classify_flat_field(ty, &fc.type_info)));
}
offset += size;
}
if offset > u16::MAX as usize {
return boxed;
}
match field_at {
Some((off, FlatFieldForm::Scalar(kind))) if off <= u16::MAX as usize => TupleField::Flat {
offset: off as u16,
kind,
},
Some((off, FlatFieldForm::Nested(size, variant))) if off <= u16::MAX as usize => {
TupleField::FlatNested {
offset: off as u16,
size,
variant,
}
}
_ => boxed,
}
}
fn tuple_elem_types_of(ty: Option<&TypeExpr>, arity: usize) -> Vec<Option<TypeExpr>> {
match ty {
Some(TypeExpr::Tuple(ts, _)) if ts.len() == arity => ts.iter().cloned().map(Some).collect(),
Some(TypeExpr::Labelled(inner, _, _)) | Some(TypeExpr::NegativeLabelled(inner, _, _)) => {
tuple_elem_types_of(Some(inner), arity)
}
_ => core::iter::repeat_with(|| None).take(arity).collect(),
}
}
fn struct_field_access(fc: &mut FuncCompiler, type_name: &str, field_name: &str) -> StructField {
let wb = fc.type_info.word_bytes;
let order = fc.type_info.struct_field_order.get(type_name).cloned();
let flat = order.and_then(|order| {
if wb == 0 {
return None;
}
let mut offset = 0usize;
let mut field_at = None;
for (fname, ty) in &order {
let size = type_flat_size(ty, &fc.type_info)?;
if fname == field_name {
field_at = Some((offset, classify_flat_field(ty, &fc.type_info)));
}
offset += size;
}
if offset > u16::MAX as usize {
return None;
}
match field_at {
Some((off, form)) if off <= u16::MAX as usize => Some((off as u16, form)),
_ => None,
}
});
match flat {
Some((offset, FlatFieldForm::Scalar(kind))) => StructField::Flat { offset, kind },
Some((offset, FlatFieldForm::Nested(size, variant))) => StructField::FlatNested {
offset,
size,
variant,
},
_ => StructField::Boxed {
name_const: fc.add_string_constant(field_name),
},
}
}
fn strip_type_labels(ty: &TypeExpr) -> TypeExpr {
match ty {
TypeExpr::Labelled(inner, _, _) | TypeExpr::NegativeLabelled(inner, _, _) => {
strip_type_labels(inner)
}
other => other.clone(),
}
}
fn composite_needs_fieldwise_eq(ty: &TypeExpr, ti: &TypeInfo) -> bool {
match strip_type_labels(ty) {
TypeExpr::Tuple(_, _) | TypeExpr::Array(_, _, _) => true,
TypeExpr::Option(_, _) => true,
TypeExpr::Named(name, _, _, _) => {
ti.struct_field_order.contains_key(&name) || ti.enum_variant_order.contains_key(&name)
}
_ => false,
}
}
enum FieldAccessOp {
Struct(crate::bytecode::StructField),
Tuple(crate::bytecode::TupleField),
Array {
elem: crate::bytecode::ArrayElem,
index_const: u16,
},
}
fn emit_field_extract(fc: &mut FuncCompiler, slot: u16, access: &FieldAccessOp) {
fc.emit(Op::GetLocal(slot));
match access {
FieldAccessOp::Struct(f) => {
fc.emit(Op::GetField(*f));
}
FieldAccessOp::Tuple(f) => {
fc.emit(Op::GetTupleField(*f));
}
FieldAccessOp::Array { elem, index_const } => {
fc.emit(Op::Const(*index_const));
fc.emit(Op::GetIndex(*elem));
}
}
}
fn composite_field_accessors(
fc: &mut FuncCompiler,
ty: &TypeExpr,
) -> Result<Vec<(FieldAccessOp, TypeExpr)>, CompileError> {
match strip_type_labels(ty) {
TypeExpr::Named(name, _, _, span) => {
let order = fc
.type_info
.struct_field_order
.get(&name)
.cloned()
.ok_or_else(|| CompileError {
message: alloc::format!(
"field-wise equality requires a struct layout for `{}`",
name
),
span,
})?;
let mut out = Vec::with_capacity(order.len());
for (fname, fty) in &order {
let access = FieldAccessOp::Struct(struct_field_access(fc, &name, fname));
out.push((access, fty.clone()));
}
Ok(out)
}
TypeExpr::Tuple(elems, _) => {
let opt_types: Vec<Option<TypeExpr>> = elems.iter().cloned().map(Some).collect();
let mut out = Vec::with_capacity(elems.len());
for (i, ety) in elems.iter().enumerate() {
let access = FieldAccessOp::Tuple(tuple_field_access(fc, &opt_types, i));
out.push((access, ety.clone()));
}
Ok(out)
}
TypeExpr::Array(elem, count, span) => {
let count = count.as_lit().ok_or_else(|| CompileError {
message: alloc::format!("array size `{}` is not a resolved constant", count),
span,
})?;
if count < 0 {
return Err(CompileError {
message: String::from("field-wise equality on a negative-length array"),
span,
});
}
let mut out = Vec::with_capacity(count as usize);
for i in 0..count {
let elem_op = array_elem_operand(Some(&elem), &fc.type_info);
let index_const = fc.add_constant(Value::Int(i));
out.push((
FieldAccessOp::Array {
elem: elem_op,
index_const,
},
(*elem).clone(),
));
}
Ok(out)
}
other => Err(CompileError {
message: alloc::format!(
"field-wise equality on an unsupported composite type {:?}",
other
),
span: Span::default(),
}),
}
}
fn emit_composite_fieldwise_eq(
fc: &mut FuncCompiler,
ty: &TypeExpr,
ltmp: u16,
rtmp: u16,
) -> Result<(), CompileError> {
if matches!(strip_type_labels(ty), TypeExpr::Option(_, _)) {
return emit_option_fieldwise_eq(fc, ty, ltmp, rtmp);
}
if let TypeExpr::Named(name, _, _, _) = &strip_type_labels(ty)
&& fc.type_info.enum_variant_order.contains_key(name)
{
return emit_enum_fieldwise_eq(fc, name, ltmp, rtmp);
}
let fields = composite_field_accessors(fc, ty)?;
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for (access, fty) in fields {
fc.begin_scope();
emit_field_extract(fc, ltmp, &access);
emit_field_extract(fc, rtmp, &access);
if composite_needs_fieldwise_eq(&fty, &fc.type_info) {
let r2 = fc.declare_local("__eqf_r");
let l2 = fc.declare_local("__eqf_l");
fc.emit(Op::SetLocal(r2));
fc.emit(Op::SetLocal(l2));
emit_composite_fieldwise_eq(fc, &fty, l2, r2)?;
} else {
fc.emit(Op::CmpEq);
}
fc.emit(Op::Not);
let skip = fc.emit_jump(Op::If(0));
let f_const = fc.add_constant(Value::Bool(false));
fc.emit(Op::Const(f_const));
let br = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside field-wise eq loop")
.push(br);
fc.patch_jump(skip);
fc.emit(Op::EndIf);
fc.end_scope();
}
let t_const = fc.add_constant(Value::Bool(true));
fc.emit(Op::Const(t_const));
let br = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside field-wise eq loop")
.push(br);
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn emit_option_fieldwise_eq(
fc: &mut FuncCompiler,
ty: &TypeExpr,
ltmp: u16,
rtmp: u16,
) -> Result<(), CompileError> {
let e_const = fc.add_string_constant("Option");
let v_const = fc.add_string_constant("Some");
let some_disc = fc.add_constant(Value::Int(1));
let opt_ty = Some(strip_type_labels(ty));
let payload_ty = option_inner(opt_ty.as_ref());
let some_field = option_some_field(fc, opt_ty.as_ref(), 0);
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
fc.begin_scope();
fc.emit(Op::GetLocal(ltmp));
fc.emit(Op::IsEnum(e_const, v_const, some_disc));
let a_not_some = fc.emit_jump(Op::If(0));
fc.emit(Op::PopN(1)); fc.emit(Op::GetLocal(rtmp));
fc.emit(Op::IsEnum(e_const, v_const, some_disc));
let b_not_some = fc.emit_jump(Op::If(0));
fc.emit(Op::PopN(1)); fc.emit(Op::GetLocal(ltmp));
fc.emit(Op::GetEnumField(some_field));
fc.emit(Op::GetLocal(rtmp));
fc.emit(Op::GetEnumField(some_field));
match &payload_ty {
Some(t) if composite_needs_fieldwise_eq(t, &fc.type_info) => {
let r2 = fc.declare_local("__eqo_r");
let l2 = fc.declare_local("__eqo_l");
fc.emit(Op::SetLocal(r2));
fc.emit(Op::SetLocal(l2));
emit_composite_fieldwise_eq(fc, t, l2, r2)?;
}
_ => {
fc.emit(Op::CmpEq);
}
}
let br_both = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside option eq loop")
.push(br_both);
fc.patch_jump(b_not_some);
fc.emit(Op::EndIf);
fc.emit(Op::PopN(1)); fc.emit(Op::GetLocal(ltmp));
fc.emit(Op::GetLocal(rtmp));
fc.emit(Op::CmpEq);
let br_anb = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside option eq loop")
.push(br_anb);
fc.patch_jump(a_not_some);
fc.emit(Op::EndIf);
fc.emit(Op::PopN(1)); fc.emit(Op::GetLocal(ltmp));
fc.emit(Op::GetLocal(rtmp));
fc.emit(Op::CmpEq);
let br_an = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside option eq loop")
.push(br_an);
fc.end_scope();
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn emit_enum_fieldwise_eq(
fc: &mut FuncCompiler,
enum_name: &str,
ltmp: u16,
rtmp: u16,
) -> Result<(), CompileError> {
let variants: Vec<(String, i64)> = fc
.type_info
.enum_variant_order
.get(enum_name)
.cloned()
.unwrap_or_default();
let e_const = fc.add_string_constant(enum_name);
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for (variant, disc) in &variants {
fc.begin_scope();
let v_const = fc.add_string_constant(variant);
let d_const = fc.add_constant(Value::Int(*disc));
fc.emit(Op::GetLocal(ltmp));
fc.emit(Op::IsEnum(e_const, v_const, d_const));
let l_not_v = fc.emit_jump(Op::If(0)); fc.emit(Op::PopN(1)); fc.emit(Op::GetLocal(rtmp));
fc.emit(Op::IsEnum(e_const, v_const, d_const));
let r_not_v = fc.emit_jump(Op::If(0)); fc.emit(Op::PopN(1)); let payload: Vec<TypeExpr> = fc
.type_info
.enums
.get(enum_name)
.and_then(|vs| vs.get(variant))
.cloned()
.unwrap_or_default();
for (i, fty) in payload.iter().enumerate() {
let access = enum_field_access(fc, enum_name, variant, i);
fc.emit(Op::GetLocal(ltmp));
fc.emit(Op::GetEnumField(access));
fc.emit(Op::GetLocal(rtmp));
fc.emit(Op::GetEnumField(access));
if composite_needs_fieldwise_eq(fty, &fc.type_info) {
let r2 = fc.declare_local("__eqe_r");
let l2 = fc.declare_local("__eqe_l");
fc.emit(Op::SetLocal(r2));
fc.emit(Op::SetLocal(l2));
emit_composite_fieldwise_eq(fc, fty, l2, r2)?;
} else {
fc.emit(Op::CmpEq);
}
fc.emit(Op::Not);
let field_eq = fc.emit_jump(Op::If(0));
let f_const = fc.add_constant(Value::Bool(false));
fc.emit(Op::Const(f_const));
let br = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside enum eq loop")
.push(br);
fc.patch_jump(field_eq);
fc.emit(Op::EndIf);
}
let t_const = fc.add_constant(Value::Bool(true));
fc.emit(Op::Const(t_const));
let br = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside enum eq loop")
.push(br);
fc.patch_jump(r_not_v);
fc.emit(Op::EndIf);
fc.emit(Op::PopN(1)); let f_const = fc.add_constant(Value::Bool(false));
fc.emit(Op::Const(f_const));
let br = fc.emit_jump(Op::Break(0));
fc.loop_breaks
.last_mut()
.expect("inside enum eq loop")
.push(br);
fc.patch_jump(l_not_v);
fc.emit(Op::EndIf);
fc.emit(Op::PopN(1));
fc.end_scope();
}
fc.emit(Op::Trap(
crate::bytecode::TrapKind::EnumVariantUnmapped.code(),
));
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn named_type_name(ty: Option<&TypeExpr>) -> Option<&str> {
match ty {
Some(TypeExpr::Named(name, _, _, _)) => Some(name),
Some(TypeExpr::Labelled(inner, _, _)) | Some(TypeExpr::NegativeLabelled(inner, _, _)) => {
named_type_name(Some(inner))
}
_ => None,
}
}
fn option_inner(ty: Option<&TypeExpr>) -> Option<TypeExpr> {
match ty {
Some(TypeExpr::Option(inner, _)) => Some((**inner).clone()),
_ => None,
}
}
fn option_some_field(fc: &FuncCompiler, ty: Option<&TypeExpr>, index: usize) -> EnumField {
let boxed = EnumField::Boxed { index: index as u8 };
let wb = fc.type_info.word_bytes;
if wb == 0 || index != 0 || wb > u16::MAX as usize {
return boxed;
}
let Some(t) = option_inner(ty) else {
return boxed;
};
match classify_flat_field(&t, &fc.type_info) {
FlatFieldForm::Scalar(kind) => EnumField::Flat {
offset: wb as u16,
kind,
},
FlatFieldForm::Nested(size, variant) => EnumField::FlatNested {
offset: wb as u16,
size,
variant,
},
FlatFieldForm::NotFlat => boxed,
}
}
fn enum_field_access(
fc: &mut FuncCompiler,
type_name: &str,
variant: &str,
index: usize,
) -> EnumField {
let wb = fc.type_info.word_bytes;
let payload = fc
.type_info
.enums
.get(type_name)
.and_then(|vs| vs.get(variant))
.cloned();
let flat = payload.and_then(|payload| {
if wb == 0 {
return None;
}
let mut offset = wb;
let mut field_at = None;
for (i, ty) in payload.iter().enumerate() {
let size = type_flat_size(ty, &fc.type_info)?;
if i == index {
field_at = Some((offset, classify_flat_field(ty, &fc.type_info)));
}
offset += size;
}
if offset > u16::MAX as usize {
return None;
}
match field_at {
Some((off, form)) if off <= u16::MAX as usize => Some((off as u16, form)),
_ => None,
}
});
match flat {
Some((offset, FlatFieldForm::Scalar(kind))) => EnumField::Flat { offset, kind },
Some((offset, FlatFieldForm::Nested(size, variant))) => EnumField::FlatNested {
offset,
size,
variant,
},
_ => EnumField::Boxed { index: index as u8 },
}
}
fn compile_let_pattern_typed(
fc: &mut FuncCompiler,
pattern: &Pattern,
ty: Option<TypeExpr>,
) -> Result<(), CompileError> {
match pattern {
Pattern::Variable(name, _) => {
let slot = fc.declare_local_typed(name, ty);
fc.emit(Op::SetLocal(slot));
}
Pattern::Wildcard(_) => {
fc.emit(Op::PopN(1));
}
Pattern::Tuple(pats, _) => {
let elem_types: Vec<Option<TypeExpr>> = match &ty {
Some(TypeExpr::Tuple(ts, _)) if ts.len() == pats.len() => {
ts.iter().cloned().map(Some).collect()
}
_ => pats.iter().map(|_| None).collect(),
};
let temp = fc.declare_local("__let_tmp");
fc.emit(Op::SetLocal(temp));
for (i, pat) in pats.iter().enumerate() {
fc.emit(Op::GetLocal(temp));
fc.emit(Op::GetTupleField(tuple_field_access(fc, &elem_types, i)));
let sub_ty = elem_types.get(i).cloned().unwrap_or(None);
compile_let_pattern_typed(fc, pat, sub_ty)?;
}
}
_ => {
let slot = fc.declare_local("_");
fc.emit(Op::SetLocal(slot));
}
}
Ok(())
}
fn compile_for_in_data_array(
fc: &mut FuncCompiler,
for_stmt: &ForStmt,
data_name: &str,
field: &str,
elem_type: &TypeExpr,
len: i64,
) -> Result<(), CompileError> {
if matches!(elem_type, TypeExpr::Array(_, _, _)) {
return Err(CompileError {
message: format!(
"for-in iteration over multi-dimensional data-segment array `{}.{}` is not supported; iterate the outer dimension by index and the inner explicitly",
data_name, field
),
span: for_stmt.span,
});
}
if !(0..=u16::MAX as i64).contains(&len) {
return Err(CompileError {
message: format!(
"data array length {} is outside the supported 16-bit bound",
len
),
span: for_stmt.span,
});
}
let base = fc
.resolve_data_field(data_name, field)
.ok_or_else(|| CompileError {
message: format!("unknown data field: {}.{}", data_name, field),
span: for_stmt.span,
})?;
let total_slots = (len as u16).saturating_mul(slots_for_data_type(elem_type));
let zero_const = fc.add_constant(Value::Int(0));
fc.emit(Op::Const(zero_const));
let idx_slot = fc.declare_local("__for_data_idx");
fc.emit(Op::SetLocal(idx_slot));
let end_const = fc.add_constant(Value::Int(len));
fc.emit(Op::Const(end_const));
let end_slot = fc.declare_local("__for_data_end");
fc.emit(Op::SetLocal(end_slot));
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
fc.emit(Op::GetLocal(idx_slot));
fc.emit(Op::GetLocal(end_slot));
fc.emit(Op::CmpGe);
let break_addr = fc.emit(Op::BreakIf(0));
fc.emit(Op::GetLocal(idx_slot));
fc.emit(Op::GetDataIndexed(base, total_slots));
let var_slot = fc.declare_local_typed(&for_stmt.var, Some(elem_type.clone()));
fc.emit(Op::SetLocal(var_slot));
fc.begin_scope();
compile_block(fc, &for_stmt.body)?;
fc.emit(Op::PopN(1));
fc.end_scope();
fc.emit(Op::GetLocal(idx_slot));
let one_const = fc.add_constant(Value::Int(1));
fc.emit(Op::Const(one_const));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(idx_slot));
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
if let Op::BreakIf(a) = &mut fc.chunk.ops[break_addr] {
*a = after_endloop;
}
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
fc.exit_loop();
Ok(())
}
fn compile_for(fc: &mut FuncCompiler, for_stmt: &ForStmt) -> Result<(), CompileError> {
match &for_stmt.iterable {
Iterable::Range(start, end) => {
compile_expr(fc, start)?;
let var_slot = fc.declare_local(&for_stmt.var);
fc.emit(Op::SetLocal(var_slot));
compile_expr(fc, end)?;
let end_slot = fc.declare_local("__for_end");
fc.emit(Op::SetLocal(end_slot));
let loop_addr = fc.emit(Op::Loop(0)); fc.enter_loop();
fc.emit(Op::GetLocal(var_slot));
fc.emit(Op::GetLocal(end_slot));
fc.emit(Op::CmpGe);
let break_addr = fc.emit(Op::BreakIf(0));
fc.begin_scope();
compile_block(fc, &for_stmt.body)?;
fc.emit(Op::PopN(1)); fc.end_scope();
fc.emit(Op::GetLocal(var_slot));
let one_const = fc.add_constant(Value::Int(1));
fc.emit(Op::Const(one_const));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(var_slot));
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
if let Op::BreakIf(a) = &mut fc.chunk.ops[break_addr] {
*a = after_endloop;
}
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
fc.exit_loop(); }
Iterable::Expr(expr) => {
if let Expr::FieldAccess { object, field, .. } = expr.as_ref()
&& let Expr::Ident { name, .. } = object.as_ref()
&& fc.is_data_block(name)
{
let field_type = fc
.type_info
.data_field_types
.get(name)
.and_then(|f| f.get(field))
.cloned();
if let Some(TypeExpr::Array(elem, len, _)) = field_type {
let len = len.as_lit().unwrap_or(0);
return compile_for_in_data_array(fc, for_stmt, name, field, &elem, len);
}
}
let static_length = fc.static_for_in_length(expr);
let element_ty = infer_expr_type(fc, expr).and_then(|t| element_type_of(&t));
compile_expr(fc, expr)?;
let arr_slot = fc.declare_local("__for_arr");
fc.emit(Op::SetLocal(arr_slot));
let end_slot = fc.declare_local("__for_end");
if let Some(n) = static_length {
let n_const = fc.add_constant(Value::Int(n));
fc.emit(Op::Const(n_const));
fc.emit(Op::SetLocal(end_slot));
} else {
fc.emit(Op::GetLocal(arr_slot));
fc.emit(Op::Len);
fc.emit(Op::SetLocal(end_slot));
}
let zero_const = fc.add_constant(Value::Int(0));
fc.emit(Op::Const(zero_const));
let idx_slot = fc.declare_local("__for_idx");
fc.emit(Op::SetLocal(idx_slot));
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
fc.emit(Op::GetLocal(idx_slot));
fc.emit(Op::GetLocal(end_slot));
fc.emit(Op::CmpGe);
let break_addr = fc.emit(Op::BreakIf(0));
fc.emit(Op::GetLocal(arr_slot));
fc.emit(Op::GetLocal(idx_slot));
fc.emit(Op::GetIndex(array_elem_operand(
element_ty.as_ref(),
&fc.type_info,
)));
let var_slot = fc.declare_local_typed(&for_stmt.var, element_ty);
fc.emit(Op::SetLocal(var_slot));
fc.begin_scope();
compile_block(fc, &for_stmt.body)?;
fc.emit(Op::PopN(1));
fc.end_scope();
fc.emit(Op::GetLocal(idx_slot));
let one_const = fc.add_constant(Value::Int(1));
fc.emit(Op::Const(one_const));
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
fc.emit(Op::SetLocal(idx_slot));
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
if let Op::BreakIf(a) = &mut fc.chunk.ops[break_addr] {
*a = after_endloop;
}
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
fc.exit_loop();
}
}
Ok(())
}
fn normalize_fixed_defaults(program: &mut Program, frac_bits: u8) {
use crate::ast::*;
fn fix_type(t: &mut TypeExpr, frac_bits: u8) {
match t {
TypeExpr::Prim(PrimType::Fixed(slot), _) => {
if slot.is_none() {
*slot = Some(frac_bits);
}
}
TypeExpr::Prim(_, _) | TypeExpr::Unit(_) | TypeExpr::Multiword(_, _, _) => {}
TypeExpr::Tuple(parts, _) => {
for p in parts.iter_mut() {
fix_type(p, frac_bits);
}
}
TypeExpr::Array(elem, _, _) => fix_type(elem, frac_bits),
TypeExpr::Option(inner, _) => fix_type(inner, frac_bits),
TypeExpr::Named(_, args, _, _) => {
for a in args.iter_mut() {
fix_type(a, frac_bits);
}
}
TypeExpr::Labelled(inner, _, _) => fix_type(inner, frac_bits),
TypeExpr::NegativeLabelled(inner, _, _) => fix_type(inner, frac_bits),
}
}
fn fix_opt(t: &mut Option<TypeExpr>, frac_bits: u8) {
if let Some(t) = t.as_mut() {
fix_type(t, frac_bits);
}
}
fn fix_block(block: &mut Block, frac_bits: u8) {
for stmt in block.stmts.iter_mut() {
fix_stmt(stmt, frac_bits);
}
if let Some(e) = block.tail_expr.as_mut() {
fix_expr(e, frac_bits);
}
}
fn fix_stmt(stmt: &mut Stmt, frac_bits: u8) {
match stmt {
Stmt::Let(l) => {
fix_opt(&mut l.type_expr, frac_bits);
fix_expr(&mut l.value, frac_bits);
}
Stmt::For(f) => {
match &mut f.iterable {
Iterable::Range(s, e) => {
fix_expr(s, frac_bits);
fix_expr(e, frac_bits);
}
Iterable::Expr(e) => fix_expr(e, frac_bits),
}
fix_block(&mut f.body, frac_bits);
}
Stmt::Break(_) => {}
Stmt::DataFieldAssign { value, .. } => fix_expr(value, frac_bits),
Stmt::DataFieldIndexAssign { indices, value, .. } => {
for idx in indices.iter_mut() {
fix_expr(idx, frac_bits);
}
fix_expr(value, frac_bits);
}
Stmt::Expr(e) => fix_expr(e, frac_bits),
Stmt::Assert { cond, .. } => fix_expr(cond, frac_bits),
}
}
fn fix_expr(expr: &mut Expr, frac_bits: u8) {
match expr {
Expr::Literal { .. }
| Expr::Ident { .. }
| Expr::Placeholder { .. }
| Expr::ClosureRef { .. } => {}
Expr::BinOp { left, right, .. } => {
fix_expr(left, frac_bits);
fix_expr(right, frac_bits);
}
Expr::UnaryOp { operand, .. } => fix_expr(operand, frac_bits),
Expr::Call { args, .. } => {
for a in args.iter_mut() {
fix_expr(a, frac_bits);
}
}
Expr::Pipeline { left, args, .. } => {
fix_expr(left, frac_bits);
for a in args.iter_mut() {
fix_expr(a, frac_bits);
}
}
Expr::Yield { value, .. } => fix_expr(value, frac_bits),
Expr::If {
condition,
then_block,
else_block,
..
} => {
fix_expr(condition, frac_bits);
fix_block(then_block, frac_bits);
if let Some(eb) = else_block {
fix_block(eb, frac_bits);
}
}
Expr::Match {
scrutinee, arms, ..
} => {
fix_expr(scrutinee, frac_bits);
for arm in arms.iter_mut() {
if let Some(g) = arm.guard.as_mut() {
fix_expr(g, frac_bits);
}
fix_expr(&mut arm.expr, frac_bits);
}
}
Expr::Loop { body, .. } => fix_block(body, frac_bits),
Expr::Cast {
expr: inner,
target,
..
} => {
fix_expr(inner, frac_bits);
fix_type(target, frac_bits);
}
Expr::TupleLiteral { elements, .. } => {
for e in elements.iter_mut() {
fix_expr(e, frac_bits);
}
}
Expr::ArrayLiteral { elements, .. } => {
for e in elements.iter_mut() {
fix_expr(e, frac_bits);
}
}
Expr::ArrayIndex { object, index, .. } => {
fix_expr(object, frac_bits);
fix_expr(index, frac_bits);
}
Expr::FieldAccess { object, .. } => fix_expr(object, frac_bits),
Expr::TupleIndex { object, .. } => fix_expr(object, frac_bits),
Expr::MethodCall { receiver, args, .. } => {
fix_expr(receiver, frac_bits);
for a in args.iter_mut() {
fix_expr(a, frac_bits);
}
}
Expr::StructInit { fields, .. } => {
for f in fields.iter_mut() {
fix_expr(&mut f.value, frac_bits);
}
}
Expr::EnumVariant { args, .. } => {
for a in args.iter_mut() {
fix_expr(a, frac_bits);
}
}
Expr::Closure {
params,
return_type,
body,
..
} => {
for p in params.iter_mut() {
fix_opt(&mut p.type_expr, frac_bits);
}
fix_opt(return_type, frac_bits);
fix_block(body, frac_bits);
}
Expr::Checked { op_expr, arms, .. } => {
fix_expr(op_expr, frac_bits);
for arm in arms.iter_mut() {
fix_expr(&mut arm.body, frac_bits);
}
}
Expr::SaturateMax { .. } | Expr::SaturateMin { .. } => {}
Expr::Classify { value, .. } | Expr::Declassify { value, .. } => {
fix_expr(value, frac_bits);
}
}
}
fn fix_function(func: &mut FunctionDef, frac_bits: u8) {
for p in func.params.iter_mut() {
fix_opt(&mut p.type_expr, frac_bits);
}
fix_type(&mut func.return_type, frac_bits);
fix_block(&mut func.body, frac_bits);
}
for type_def in program.types.iter_mut() {
match type_def {
TypeDef::Struct(s) => {
for f in s.fields.iter_mut() {
fix_type(&mut f.type_expr, frac_bits);
}
}
TypeDef::Enum(e) => {
for v in e.variants.iter_mut() {
for t in v.fields.iter_mut() {
fix_type(t, frac_bits);
}
}
}
TypeDef::Newtype(n) => {
fix_type(&mut n.underlying, frac_bits);
}
}
}
for data in program.data_decls.iter_mut() {
for f in data.fields.iter_mut() {
fix_type(&mut f.type_expr, frac_bits);
}
}
for func in program.functions.iter_mut() {
fix_function(func, frac_bits);
}
for impl_block in program.impls.iter_mut() {
for method in impl_block.methods.iter_mut() {
for p in method.params.iter_mut() {
fix_opt(&mut p.type_expr, frac_bits);
}
fix_type(&mut method.return_type, frac_bits);
fix_block(&mut method.body, frac_bits);
}
}
}
fn compile_enum_to_word(
fc: &mut FuncCompiler,
inner: &Expr,
enum_name: &str,
) -> Result<(), CompileError> {
compile_expr(fc, inner)?;
let temp = fc.declare_local("__enum_cast");
fc.emit(Op::SetLocal(temp));
let variants: Vec<(String, i64)> = fc
.type_info
.enum_variant_order
.get(enum_name)
.cloned()
.unwrap_or_default();
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
let e_const = fc.add_string_constant(enum_name);
for (variant_name, discriminant) in &variants {
fc.begin_scope();
let v_const = fc.add_string_constant(variant_name);
let d_const = fc.add_constant(Value::Int(*discriminant));
fc.emit(Op::GetLocal(temp));
fc.emit(Op::IsEnum(e_const, v_const, d_const));
emit_consume_peeked_scrutinee(fc);
let fail_addr = fc.emit_jump(Op::If(0));
let disc_const = fc.add_constant(Value::Int(*discriminant));
fc.emit(Op::Const(disc_const));
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
fc.patch_jump(fail_addr);
fc.emit(Op::EndIf);
fc.end_scope();
}
fc.emit(Op::Trap(
crate::bytecode::TrapKind::EnumVariantUnmapped.code(),
));
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
#[derive(Debug, Clone, Copy)]
enum EvalValue {
Int(i64),
Bool(bool),
}
fn eval_predicate_at_int(body: &Expr, param_name: &str, value: i64) -> Option<bool> {
let lookup = |name: &str| -> Option<EvalValue> {
if name == param_name {
Some(EvalValue::Int(value))
} else {
None
}
};
match eval_expr_with(body, &lookup)? {
EvalValue::Bool(b) => Some(b),
EvalValue::Int(_) => None,
}
}
fn fold_to_int(expr: &Expr, lookup: &dyn Fn(&str) -> Option<EvalValue>) -> Option<i64> {
match eval_expr_with(expr, lookup)? {
EvalValue::Int(n) => Some(n),
EvalValue::Bool(_) => None,
}
}
fn eval_expr_with(expr: &Expr, lookup: &dyn Fn(&str) -> Option<EvalValue>) -> Option<EvalValue> {
match expr {
Expr::Literal { value: lit, .. } => match lit {
crate::ast::Literal::Int(n) => Some(EvalValue::Int(*n)),
crate::ast::Literal::Bool(b) => Some(EvalValue::Bool(*b)),
_ => None,
},
Expr::Ident { name, .. } => lookup(name.as_str()),
Expr::UnaryOp { op, operand, .. } => {
let inner = eval_expr_with(operand, lookup)?;
match (op, inner) {
(crate::ast::UnaryOp::Neg, EvalValue::Int(n)) => {
Some(EvalValue::Int(n.wrapping_neg()))
}
(crate::ast::UnaryOp::Not, EvalValue::Bool(b)) => Some(EvalValue::Bool(!b)),
_ => None,
}
}
Expr::BinOp {
op, left, right, ..
} => {
let l = eval_expr_with(left, lookup)?;
let r = eval_expr_with(right, lookup)?;
match (op, l, r) {
(crate::ast::BinOp::Add, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Int(a.wrapping_add(b)))
}
(crate::ast::BinOp::Sub, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Int(a.wrapping_sub(b)))
}
(crate::ast::BinOp::Mul, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Int(a.wrapping_mul(b)))
}
(crate::ast::BinOp::Div, EvalValue::Int(a), EvalValue::Int(b)) if b != 0 => {
Some(EvalValue::Int(a.wrapping_div(b)))
}
(crate::ast::BinOp::Mod, EvalValue::Int(a), EvalValue::Int(b)) if b != 0 => {
Some(EvalValue::Int(a.wrapping_rem(b)))
}
(crate::ast::BinOp::Eq, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Bool(a == b))
}
(crate::ast::BinOp::Eq, EvalValue::Bool(a), EvalValue::Bool(b)) => {
Some(EvalValue::Bool(a == b))
}
(crate::ast::BinOp::NotEq, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Bool(a != b))
}
(crate::ast::BinOp::NotEq, EvalValue::Bool(a), EvalValue::Bool(b)) => {
Some(EvalValue::Bool(a != b))
}
(crate::ast::BinOp::Lt, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Bool(a < b))
}
(crate::ast::BinOp::LtEq, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Bool(a <= b))
}
(crate::ast::BinOp::Gt, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Bool(a > b))
}
(crate::ast::BinOp::GtEq, EvalValue::Int(a), EvalValue::Int(b)) => {
Some(EvalValue::Bool(a >= b))
}
(
crate::ast::BinOp::And | crate::ast::BinOp::Andalso,
EvalValue::Bool(a),
EvalValue::Bool(b),
) => Some(EvalValue::Bool(a && b)),
(
crate::ast::BinOp::Or | crate::ast::BinOp::Orelse,
EvalValue::Bool(a),
EvalValue::Bool(b),
) => Some(EvalValue::Bool(a || b)),
(crate::ast::BinOp::Xor, EvalValue::Bool(a), EvalValue::Bool(b)) => {
Some(EvalValue::Bool(a != b))
}
_ => None,
}
}
_ => None,
}
}
pub(crate) fn predicate_true_set(body: &Expr, param: &str) -> Option<crate::interval::IntervalSet> {
use crate::interval::IntervalSet;
match body {
Expr::Literal {
value: crate::ast::Literal::Bool(true),
..
} => Some(IntervalSet::full()),
Expr::Literal {
value: crate::ast::Literal::Bool(false),
..
} => Some(IntervalSet::empty()),
Expr::BinOp {
op, left, right, ..
} => {
if let Some((cmp, n)) = comparison_against_param(*op, left, right, param) {
comparison_set(cmp, n)
} else if matches!(op, crate::ast::BinOp::And | crate::ast::BinOp::Andalso) {
let l = predicate_true_set(left, param)?;
let r = predicate_true_set(right, param)?;
Some(l.intersect(&r))
} else if matches!(op, crate::ast::BinOp::Or | crate::ast::BinOp::Orelse) {
let l = predicate_true_set(left, param)?;
let r = predicate_true_set(right, param)?;
Some(l.union(&r))
} else {
None
}
}
Expr::UnaryOp {
op: crate::ast::UnaryOp::Not,
operand,
..
} => {
let inner = predicate_true_set(operand, param)?;
Some(inner.complement())
}
_ => None,
}
}
fn comparison_against_param(
op: crate::ast::BinOp,
lhs: &Expr,
rhs: &Expr,
param: &str,
) -> Option<(crate::ast::BinOp, i64)> {
if !matches!(
op,
crate::ast::BinOp::Lt
| crate::ast::BinOp::LtEq
| crate::ast::BinOp::Gt
| crate::ast::BinOp::GtEq
| crate::ast::BinOp::Eq
| crate::ast::BinOp::NotEq
) {
return None;
}
let param_on_left = matches!(lhs, Expr::Ident { name, .. } if name == param);
let param_on_right = matches!(rhs, Expr::Ident { name, .. } if name == param);
if param_on_left {
let n = fold_to_int(rhs, &|_| None)?;
Some((op, n))
} else if param_on_right {
let n = fold_to_int(lhs, &|_| None)?;
Some((flip_cmp(op), n))
} else {
None
}
}
fn flip_cmp(op: crate::ast::BinOp) -> crate::ast::BinOp {
match op {
crate::ast::BinOp::Lt => crate::ast::BinOp::Gt,
crate::ast::BinOp::LtEq => crate::ast::BinOp::GtEq,
crate::ast::BinOp::Gt => crate::ast::BinOp::Lt,
crate::ast::BinOp::GtEq => crate::ast::BinOp::LtEq,
crate::ast::BinOp::Eq => crate::ast::BinOp::Eq,
crate::ast::BinOp::NotEq => crate::ast::BinOp::NotEq,
other => other,
}
}
fn comparison_set(op: crate::ast::BinOp, n: i64) -> Option<crate::interval::IntervalSet> {
use crate::interval::{Interval, IntervalSet};
match op {
crate::ast::BinOp::Lt => match n.checked_sub(1) {
Some(m) => Some(IntervalSet::from_interval(Interval::at_most(m))),
None => Some(IntervalSet::empty()),
},
crate::ast::BinOp::LtEq => Some(IntervalSet::from_interval(Interval::at_most(n))),
crate::ast::BinOp::Gt => match n.checked_add(1) {
Some(m) => Some(IntervalSet::from_interval(Interval::at_least(m))),
None => Some(IntervalSet::empty()),
},
crate::ast::BinOp::GtEq => Some(IntervalSet::from_interval(Interval::at_least(n))),
crate::ast::BinOp::Eq => Some(IntervalSet::singleton(n)),
crate::ast::BinOp::NotEq => Some(IntervalSet::singleton(n).complement()),
_ => None,
}
}
const WIDEN_AFTER_ITERATIONS: usize = 3;
const SUMMARY_PASS_LIMIT: usize = 16;
fn compute_function_return_ranges(
program: &crate::ast::Program,
type_info: &TypeInfo,
) -> BTreeMap<String, crate::interval::IntervalSet> {
use crate::interval::IntervalSet;
let mut summaries: BTreeMap<String, IntervalSet> = BTreeMap::new();
for func in &program.functions {
summaries.insert(func.name.clone(), IntervalSet::empty());
}
for iteration in 0..SUMMARY_PASS_LIMIT {
let mut changed = false;
for func in &program.functions {
let Some(new_range) = compute_function_return_range(func, type_info, &summaries) else {
continue;
};
let old = summaries
.get(&func.name)
.cloned()
.unwrap_or_else(IntervalSet::empty);
if new_range == old {
continue;
}
let updated = if iteration >= WIDEN_AFTER_ITERATIONS {
old.widen(&new_range)
} else {
old.union(&new_range)
};
if updated != old {
summaries.insert(func.name.clone(), updated);
changed = true;
}
}
if !changed {
break;
}
}
summaries.retain(|_, range| !range.is_empty());
summaries
}
fn compute_function_return_range(
func: &crate::ast::FunctionDef,
type_info: &TypeInfo,
known_summaries: &BTreeMap<String, crate::interval::IntervalSet>,
) -> Option<crate::interval::IntervalSet> {
if !func.body.stmts.is_empty() {
return None;
}
let tail = func.body.tail_expr.as_ref()?;
let mut params: BTreeMap<String, crate::interval::IntervalSet> = BTreeMap::new();
for param in &func.params {
let crate::ast::Pattern::Variable(name, _) = ¶m.pattern else {
return None;
};
let range = param_range_from_type(¶m.type_expr, type_info)?;
params.insert(name.clone(), range);
}
eval_expr_to_range(tail, ¶ms, type_info, known_summaries)
}
fn param_range_from_type(
t: &Option<crate::ast::TypeExpr>,
type_info: &TypeInfo,
) -> Option<crate::interval::IntervalSet> {
if let Some(crate::ast::TypeExpr::Named(type_name, _, _, _)) = t
&& let Some(pred_name) = type_info.newtype_refinements.get(type_name)
&& let Some((pred_param, body)) = type_info.refinement_bodies.get(pred_name)
&& let Some(range) = predicate_true_set(body, pred_param)
{
return Some(range);
}
natural_range_of_type_expr(t)
}
fn eval_expr_to_range(
expr: &Expr,
params: &BTreeMap<String, crate::interval::IntervalSet>,
_type_info: &TypeInfo,
known_summaries: &BTreeMap<String, crate::interval::IntervalSet>,
) -> Option<crate::interval::IntervalSet> {
use crate::interval::IntervalSet;
match expr {
Expr::Literal {
value: crate::ast::Literal::Int(n),
..
} => Some(IntervalSet::singleton(*n)),
Expr::Ident { name, .. } => params.get(name).cloned(),
Expr::UnaryOp {
op: crate::ast::UnaryOp::Neg,
operand,
..
} => {
let inner = eval_expr_to_range(operand, params, _type_info, known_summaries)?;
Some(inner.neg())
}
Expr::BinOp {
op, left, right, ..
} => {
let l = eval_expr_to_range(left, params, _type_info, known_summaries)?;
let r = eval_expr_to_range(right, params, _type_info, known_summaries)?;
match op {
crate::ast::BinOp::Add => Some(l.add(&r)),
crate::ast::BinOp::Sub => Some(l.sub(&r)),
crate::ast::BinOp::Mul => Some(l.mul(&r)),
crate::ast::BinOp::Div => Some(l.div(&r)),
crate::ast::BinOp::Mod => Some(l.rem(&r)),
_ => None,
}
}
Expr::Cast { expr: inner, .. } => {
eval_expr_to_range(inner, params, _type_info, known_summaries)
}
Expr::Call { name, .. } => known_summaries.get(name).cloned(),
Expr::If {
then_block,
else_block,
..
} => {
let then_range = block_tail_range(then_block, params, _type_info, known_summaries)?;
let else_range =
block_tail_range(else_block.as_ref()?, params, _type_info, known_summaries)?;
Some(then_range.union(&else_range))
}
Expr::Match {
scrutinee, arms, ..
} => {
use crate::interval::IntervalSet;
let scrut_range = eval_expr_to_range(scrutinee, params, _type_info, known_summaries)?;
let mut result = IntervalSet::empty();
for arm in arms {
let pattern_range = pattern_to_range(&arm.pattern)?;
let arm_scrut = scrut_range.intersect(&pattern_range);
if arm_scrut.is_empty() {
continue;
}
let mut arm_params = params.clone();
if let Pattern::Variable(name, _) = &arm.pattern {
arm_params.insert(name.clone(), arm_scrut.clone());
}
let body_range =
eval_expr_to_range(&arm.expr, &arm_params, _type_info, known_summaries)?;
result = result.union(&body_range);
}
Some(result)
}
_ => None,
}
}
fn block_tail_range(
block: &crate::ast::Block,
params: &BTreeMap<String, crate::interval::IntervalSet>,
type_info: &TypeInfo,
known_summaries: &BTreeMap<String, crate::interval::IntervalSet>,
) -> Option<crate::interval::IntervalSet> {
if !block.stmts.is_empty() {
return None;
}
let tail = block.tail_expr.as_ref()?;
eval_expr_to_range(tail, params, type_info, known_summaries)
}
fn natural_range_of_type_expr(
t: &Option<crate::ast::TypeExpr>,
) -> Option<crate::interval::IntervalSet> {
use crate::interval::{Interval, IntervalSet};
let Some(crate::ast::TypeExpr::Prim(prim, _)) = t else {
return None;
};
match prim {
crate::ast::PrimType::Byte => Some(IntervalSet::from_interval(Interval::range(0, 255))),
crate::ast::PrimType::Word => Some(IntervalSet::full()),
_ => None,
}
}
fn infer_arg_range(expr: &Expr, fc: &FuncCompiler) -> Option<crate::interval::IntervalSet> {
infer_arg_range_with(expr, fc, &BTreeMap::new())
}
fn pattern_to_range(pattern: &Pattern) -> Option<crate::interval::IntervalSet> {
use crate::interval::IntervalSet;
match pattern {
Pattern::Wildcard(_) | Pattern::Variable(_, _) => Some(IntervalSet::full()),
Pattern::Literal(Literal::Int(n), _) => Some(IntervalSet::singleton(*n)),
_ => None,
}
}
fn infer_arg_range_with(
expr: &Expr,
fc: &FuncCompiler,
shadow: &BTreeMap<String, crate::interval::IntervalSet>,
) -> Option<crate::interval::IntervalSet> {
use crate::interval::IntervalSet;
match expr {
Expr::Literal {
value: crate::ast::Literal::Int(n),
..
} => Some(IntervalSet::singleton(*n)),
Expr::Ident { name, .. } => {
if let Some(r) = shadow.get(name) {
return Some(r.clone());
}
let slot = fc.resolve_local(name.as_str())?;
if let Some(v) = fc.local_const_values.get(&slot) {
Some(IntervalSet::singleton(*v))
} else {
fc.local_ranges.get(&slot).cloned()
}
}
Expr::UnaryOp {
op: crate::ast::UnaryOp::Neg,
operand,
..
} => {
let inner = infer_arg_range_with(operand, fc, shadow)?;
Some(inner.neg())
}
Expr::BinOp {
op, left, right, ..
} => {
let l = infer_arg_range_with(left, fc, shadow)?;
let r = infer_arg_range_with(right, fc, shadow)?;
match op {
crate::ast::BinOp::Add => Some(l.add(&r)),
crate::ast::BinOp::Sub => Some(l.sub(&r)),
crate::ast::BinOp::Mul => Some(l.mul(&r)),
crate::ast::BinOp::Div => Some(l.div(&r)),
crate::ast::BinOp::Mod => Some(l.rem(&r)),
_ => None,
}
}
Expr::Cast { expr: inner, .. } => infer_arg_range_with(inner, fc, shadow),
Expr::Call { name, .. } => fc.type_info.function_return_ranges.get(name).cloned(),
Expr::Match {
scrutinee, arms, ..
} => {
let scrut_range = infer_arg_range_with(scrutinee, fc, shadow)?;
let mut result = IntervalSet::empty();
for arm in arms {
let pattern_range = pattern_to_range(&arm.pattern)?;
let arm_scrut = scrut_range.intersect(&pattern_range);
if arm_scrut.is_empty() {
continue;
}
let mut arm_shadow = shadow.clone();
if let Pattern::Variable(name, _) = &arm.pattern {
arm_shadow.insert(name.clone(), arm_scrut.clone());
}
let body_range = infer_arg_range_with(&arm.expr, fc, &arm_shadow)?;
result = result.union(&body_range);
}
Some(result)
}
_ => None,
}
}
fn compile_expr(fc: &mut FuncCompiler, expr: &Expr) -> Result<(), CompileError> {
match expr {
Expr::Literal { value, .. } => match value {
Literal::Int(v) => {
let idx = fc.add_constant(Value::Int(*v));
fc.emit(Op::Const(idx));
}
Literal::Byte(v) => {
let idx = fc.add_constant(Value::Byte(*v));
fc.emit(Op::Const(idx));
}
Literal::Fixed { raw, .. } => {
let idx = fc.add_constant(Value::Fixed(*raw));
fc.emit(Op::Const(idx));
}
#[cfg(feature = "floats")]
Literal::Float(v) => {
let idx = fc.add_constant(Value::Float(*v));
fc.emit(Op::Const(idx));
}
#[cfg(not(feature = "floats"))]
Literal::Float(_) => {
unreachable!(
"float literals are rejected at lex time when the `floats` feature is off"
);
}
Literal::String(s) => {
let idx = fc.add_constant(Value::StaticStr(s.clone()));
fc.emit(Op::Const(idx));
}
Literal::Bool(true) => {
fc.emit(Op::PushImmediate(1));
}
Literal::Bool(false) => {
fc.emit(Op::PushImmediate(2));
}
Literal::Unit => {
fc.emit(Op::PushImmediate(0));
}
},
Expr::Ident { name, span } => {
if let Some(slot) = fc.resolve_local(name) {
fc.emit(Op::GetLocal(slot));
} else if fc.function_map.contains_key(name) {
return Err(CompileError {
message: alloc::format!(
"first-class function references are not supported in V0.2.0; \
rewrite `{}` as a direct call site or as a trait-bounded \
generic",
name
),
span: *span,
});
} else if fc.is_data_block(name) {
return Err(CompileError {
message: format!(
"data block '{}' cannot be used as a value; access individual fields with {}.field_name",
name, name
),
span: *span,
});
} else {
return Err(CompileError {
message: format!("undefined variable: {}", name),
span: *span,
});
}
}
Expr::BinOp {
op,
left,
right,
span,
} => {
match op {
BinOp::Andalso => {
compile_expr(fc, left)?;
fc.emit(Op::Dup);
let if_addr = fc.emit_jump(Op::If(0));
fc.emit(Op::PopN(1));
compile_expr(fc, right)?;
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
return Ok(());
}
BinOp::Orelse => {
compile_expr(fc, left)?;
fc.emit(Op::Dup);
fc.emit(Op::Not);
let if_addr = fc.emit_jump(Op::If(0));
fc.emit(Op::PopN(1));
compile_expr(fc, right)?;
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
return Ok(());
}
BinOp::And => {
let la = fc.declare_local("__and_l");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(la));
compile_expr(fc, right)?; fc.emit(Op::GetLocal(la)); let if_addr = fc.emit_jump(Op::If(0));
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
fc.emit(Op::PopN(1));
let false_c = fc.add_constant(Value::Bool(false));
fc.emit(Op::Const(false_c));
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
return Ok(());
}
BinOp::Or => {
let la = fc.declare_local("__or_l");
compile_expr(fc, left)?;
fc.emit(Op::SetLocal(la));
compile_expr(fc, right)?; fc.emit(Op::GetLocal(la)); fc.emit(Op::Not); let if_addr = fc.emit_jump(Op::If(0));
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
fc.emit(Op::PopN(1));
let true_c = fc.add_constant(Value::Bool(true));
fc.emit(Op::Const(true_c));
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
return Ok(());
}
BinOp::Xor => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CmpNe);
return Ok(());
}
_ => {}
}
let operand_ty = infer_expr_type(fc, left).or_else(|| infer_expr_type(fc, right));
if let Some((n, f)) = operand_ty.as_ref().and_then(|t| t.as_multiword_lit()) {
return compile_multiword_binop(fc, *op, left, right, n, f);
}
if matches!(operand_ty, Some(TypeExpr::Multiword(..))) {
return Err(CompileError {
message: String::from(
"Multiword operand has an out-of-range or unresolved dimension after monomorphization",
),
span: left.span(),
});
}
if matches!(op, BinOp::Shl | BinOp::AShl | BinOp::ShrA | BinOp::ShrL) {
return compile_scalar_shift(fc, *op, left, right);
}
if matches!(op, BinOp::Band | BinOp::Bor | BinOp::Bxor)
&& matches!(&operand_ty, Some(TypeExpr::Prim(PrimType::Byte, _)))
{
return compile_byte_bitwise(fc, *op, left, right);
}
let left_fixed_n = match &operand_ty {
Some(TypeExpr::Prim(PrimType::Fixed(n), _)) => {
Some(n.unwrap_or(crate::typecheck::DEFAULT_FIXED_FRAC_BITS))
}
_ => None,
};
let left_is_int = matches!(&operand_ty, None | Some(TypeExpr::Prim(PrimType::Word, _)));
compile_expr(fc, left)?;
compile_expr(fc, right)?;
match op {
BinOp::Add => {
if left_is_int {
fc.emit(Op::CheckedAdd);
fc.emit(Op::PopN(2));
} else {
fc.emit(Op::Add);
}
}
BinOp::Sub => {
if left_is_int {
fc.emit(Op::CheckedSub);
fc.emit(Op::PopN(2));
} else {
fc.emit(Op::Sub);
}
}
BinOp::Mul => {
if let Some(n) = left_fixed_n {
fc.emit(Op::FixedMul(n));
} else if left_is_int {
fc.emit(Op::CheckedMul(0));
fc.emit(Op::PopN(2));
} else {
fc.emit(Op::Mul);
}
}
BinOp::Div => {
if let Some(n) = left_fixed_n {
fc.emit(Op::FixedDiv(n));
} else {
fc.emit(Op::Div);
}
fc.record_operator_site(span);
}
BinOp::Mod => {
fc.emit(Op::Mod);
fc.record_operator_site(span);
}
BinOp::Eq | BinOp::NotEq => {
let fieldwise = operand_ty
.as_ref()
.map(|ty| composite_needs_fieldwise_eq(ty, &fc.type_info))
.unwrap_or(false);
if fieldwise {
let ty = operand_ty.clone().expect("fieldwise implies Some");
fc.begin_scope();
let rtmp = fc.declare_local("__eq_r");
let ltmp = fc.declare_local("__eq_l");
fc.emit(Op::SetLocal(rtmp));
fc.emit(Op::SetLocal(ltmp));
emit_composite_fieldwise_eq(fc, &ty, ltmp, rtmp)?;
if matches!(op, BinOp::NotEq) {
fc.emit(Op::Not);
}
fc.end_scope();
} else if matches!(op, BinOp::Eq) {
fc.emit(Op::CmpEq);
} else {
fc.emit(Op::CmpNe);
}
}
BinOp::Lt => {
fc.emit(Op::CmpLt);
}
BinOp::Gt => {
fc.emit(Op::CmpGt);
}
BinOp::LtEq => {
fc.emit(Op::CmpLe);
}
BinOp::GtEq => {
fc.emit(Op::CmpGe);
}
BinOp::And | BinOp::Or | BinOp::Xor | BinOp::Andalso | BinOp::Orelse => {
unreachable!("boolean operators are lowered above with an early return")
}
BinOp::Band => {
fc.emit(Op::BitAnd);
}
BinOp::Bor => {
fc.emit(Op::BitOr);
}
BinOp::Bxor => {
fc.emit(Op::BitXor);
}
BinOp::Shl | BinOp::AShl | BinOp::ShrA | BinOp::ShrL => {
unreachable!(
"shifts are dispatched to compile_scalar_shift / compile_multiword_shift"
)
}
}
}
Expr::UnaryOp { op, operand, .. } => {
if matches!(op, UnaryOp::Bnot)
&& let Some((n, _)) =
infer_expr_type(fc, operand).and_then(|t| t.as_multiword_lit())
{
return compile_multiword_bnot(fc, operand, n);
}
if matches!(op, UnaryOp::Bnot)
&& matches!(infer_expr_type(fc, operand), Some(TypeExpr::Multiword(..)))
{
return Err(CompileError {
message: String::from(
"Multiword operand has an out-of-range or unresolved dimension after monomorphization",
),
span: operand.span(),
});
}
let inferred = infer_expr_type(fc, operand);
let operand_is_int = matches!(inferred, None | Some(TypeExpr::Prim(PrimType::Word, _)));
let operand_is_byte = matches!(inferred, Some(TypeExpr::Prim(PrimType::Byte, _)));
compile_expr(fc, operand)?;
match op {
UnaryOp::Neg => {
if operand_is_int {
fc.emit(Op::CheckedNeg);
fc.emit(Op::PopN(2));
} else {
fc.emit(Op::Neg);
}
}
UnaryOp::Not => {
fc.emit(Op::Not);
}
UnaryOp::Bnot => {
if operand_is_byte {
fc.emit(Op::ByteToWord);
}
let neg1 = fc.add_constant(Value::Int(-1));
fc.emit(Op::Const(neg1));
fc.emit(Op::BitXor);
if operand_is_byte {
fc.emit(Op::WordToByte);
}
}
}
}
Expr::Call {
name, args, span, ..
} => {
compile_call(fc, name, args, span)?;
fc.record_call_site_last_op(span);
}
Expr::MethodCall {
receiver,
method,
args,
span,
} => {
let head = match infer_expr_type(fc, receiver).and_then(|t| type_expr_head(&t)) {
Some(h) => h,
None => {
return Err(CompileError {
message: format!(
"method `{}` receiver type cannot be statically resolved; \
this currently requires monomorphization (B2.4)",
method
),
span: *span,
});
}
};
let suffix = format!("::{}::{}", head, method);
let resolved = fc
.function_map
.iter()
.find(|(k, _)| k.ends_with(&suffix))
.map(|(k, &idx)| (k.clone(), idx));
let (mangled, chunk_idx) = match resolved {
Some(p) => p,
None => {
return Err(CompileError {
message: format!(
"type `{}` has no method `{}` from any trait in scope",
head, method
),
span: *span,
});
}
};
let _ = mangled;
compile_expr(fc, receiver)?;
for arg in args {
compile_expr(fc, arg)?;
}
let arg_count = (args.len() + 1) as u8;
fc.emit(Op::Call(chunk_idx, arg_count));
fc.record_call_site_last_op(span);
}
Expr::Pipeline {
left,
func,
args,
span,
} => {
let mut call_args: Vec<&Expr> = Vec::new();
let mut placeholder_found = false;
for arg in args {
if matches!(arg, Expr::Placeholder { .. }) {
call_args.push(left);
placeholder_found = true;
} else {
call_args.push(arg);
}
}
if !placeholder_found {
let mut new_args = vec![left.as_ref()];
for arg in args {
new_args.push(arg);
}
call_args = new_args;
}
for arg in &call_args {
compile_expr(fc, arg)?;
}
let arg_count = call_args.len() as u8;
if let Some(&idx) = fc.function_map.get(func.as_str()) {
fc.emit(Op::Call(idx, arg_count));
} else if let Some(&idx) = fc.native_map.get(func.as_str()) {
let is_external = fc
.native_externals
.get(func.as_str())
.copied()
.unwrap_or(false);
if is_external {
fc.emit(Op::CallExternalNative(idx, arg_count));
} else {
fc.emit(Op::CallVerifiedNative(idx, arg_count));
}
} else {
return Err(CompileError {
message: format!("undefined function: {}", func),
span: *span,
});
}
fc.record_call_site_last_op(span);
}
Expr::Yield { value, .. } => {
compile_expr(fc, value)?;
fc.emit(Op::Yield);
}
Expr::If {
condition,
then_block,
else_block,
..
} => {
compile_expr(fc, condition)?;
let if_addr = fc.emit_jump(Op::If(0));
compile_block(fc, then_block)?;
if let Some(else_blk) = else_block {
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
compile_block(fc, else_blk)?;
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
} else {
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
fc.emit(Op::PushImmediate(0));
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
}
}
Expr::Match {
scrutinee, arms, ..
} => {
let scrutinee_ty = infer_expr_type(fc, scrutinee);
compile_expr(fc, scrutinee)?;
let temp = fc.declare_local("__match");
fc.emit(Op::SetLocal(temp));
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for arm in arms {
fc.begin_scope();
let mut fail_addrs =
compile_pattern_test(fc, &arm.pattern, temp, scrutinee_ty.as_ref())?;
compile_pattern_bind_typed(fc, &arm.pattern, temp, scrutinee_ty.clone())?;
if let Some(guard) = &arm.guard {
compile_expr(fc, guard)?;
let guard_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(guard_fail);
}
compile_expr(fc, &arm.expr)?;
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
fc.end_scope();
for addr in fail_addrs.into_iter().rev() {
fc.patch_jump(addr);
fc.emit(Op::EndIf);
}
}
fc.emit(Op::Trap(crate::bytecode::TrapKind::NoMatchingArm.code()));
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
}
Expr::Loop { body, .. } => {
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
compile_block(fc, body)?;
fc.emit(Op::PopN(1));
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
fc.emit(Op::PushImmediate(0));
}
Expr::FieldAccess {
object,
field,
span,
} => {
if let Expr::Ident { name, .. } = object.as_ref()
&& fc.is_data_block(name)
{
if fc.is_const_data_block(name) {
let cv =
fc.const_data_field_value(name, field)
.ok_or_else(|| CompileError {
message: format!("unknown const data field: {}.{}", name, field),
span: *span,
})?;
let idx = fc.add_const_value(cv);
fc.emit(Op::Const(idx));
return Ok(());
}
let slot = fc
.resolve_data_field(name, field)
.ok_or_else(|| CompileError {
message: format!("unknown data field: {}.{}", name, field),
span: *span,
})?;
let field_type = fc
.type_info
.data_field_types
.get(name)
.and_then(|fields| fields.get(field));
if let Some(t) = field_type
&& matches!(t, TypeExpr::Array(_, _, _))
{
return Err(CompileError {
message: format!(
"data field `{}.{}` is an array; index it through `{}.{}[i]`",
name, field, name, field
),
span: *span,
});
}
fc.emit(Op::GetData(slot));
return Ok(());
}
let obj_ty = infer_expr_type(fc, object);
compile_expr(fc, object)?;
let field_op = match named_type_name(obj_ty.as_ref()) {
Some(tn) => struct_field_access(fc, tn, field),
None => StructField::Boxed {
name_const: fc.add_string_constant(field),
},
};
fc.emit(Op::GetField(field_op));
}
Expr::TupleIndex { object, index, .. } => {
let elem_types: Vec<Option<TypeExpr>> = match infer_expr_type(fc, object) {
Some(TypeExpr::Tuple(ts, _)) => ts.into_iter().map(Some).collect(),
_ => Vec::new(),
};
compile_expr(fc, object)?;
fc.emit(Op::GetTupleField(tuple_field_access(
fc,
&elem_types,
*index as usize,
)));
}
Expr::ArrayIndex {
object,
index,
span,
} => {
if let Some(chain) = data_indexed_chain(object, index)
&& fc.is_data_block(chain.data_name)
{
emit_data_indexed_read(fc, chain, *span)?;
return Ok(());
}
let elem_ty = infer_expr_type(fc, object).and_then(|t| element_type_of(&t));
compile_expr(fc, object)?;
compile_expr(fc, index)?;
fc.emit(Op::GetIndex(array_elem_operand(
elem_ty.as_ref(),
&fc.type_info,
)));
fc.record_operator_site(span);
}
Expr::StructInit { name, fields, .. } => {
let decl_order = fc.type_info.struct_field_order.get(name).cloned();
let ordered: Vec<_> = match &decl_order {
Some(order) => order
.iter()
.map(|(fname, _)| {
fields
.iter()
.find(|f| &f.name == fname)
.expect("type checker guarantees the field is present")
})
.collect(),
None => fields.iter().collect(),
};
let field_names: Vec<String> = ordered.iter().map(|f| f.name.clone()).collect();
let ty = TypeExpr::Named(name.clone(), Vec::new(), Vec::new(), Span::default());
let byte_size = flat_alloc_bytes(&ty, &fc.type_info);
let count = ordered.len() as u16;
for field in &ordered {
compile_expr(fc, &field.value)?;
}
match byte_size {
Some(byte_size) => {
if type_may_intern_opaque(&ty, &fc.type_info) {
fc.may_intern_opaque = true;
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Struct,
count,
byte_size,
}));
}
None => {
let meta = fc.add_struct_template(name, field_names);
fc.emit(Op::NewComposite(NewCompositeOperand::Boxed {
kind: crate::value_layout::CompositeKind::Struct,
count,
meta,
}));
}
}
}
Expr::EnumVariant {
enum_name,
variant,
args,
..
} => {
let opt_some_payload = if enum_name == "Option" && variant == "Some" {
args.first().and_then(|a| infer_expr_type(fc, a))
} else {
None
};
let (ty, disc_override) = match opt_some_payload {
Some(payload_ty) => (
TypeExpr::Option(Box::new(payload_ty), Span::default()),
Some(1i64),
),
None => (
TypeExpr::Named(
enum_name.to_string(),
Vec::new(),
Vec::new(),
Span::default(),
),
None,
),
};
let byte_size = flat_alloc_bytes(&ty, &fc.type_info);
let disc = disc_override.unwrap_or_else(|| {
fc.type_info
.enum_variant_order
.get(enum_name)
.and_then(|vs| vs.iter().find(|(n, _)| n == variant).map(|(_, d)| *d))
.unwrap_or(0)
});
match byte_size {
Some(byte_size) => {
if type_may_intern_opaque(&ty, &fc.type_info) {
fc.may_intern_opaque = true;
}
let d_const = fc.add_constant(Value::Int(disc));
fc.emit(Op::Const(d_const));
for arg in args {
compile_expr(fc, arg)?;
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Enum,
count: args.len() as u16 + 1,
byte_size,
}));
}
None => {
for arg in args {
compile_expr(fc, arg)?;
}
let meta = fc.add_struct_template(enum_name, alloc::vec![variant.clone()]);
fc.emit(Op::NewComposite(NewCompositeOperand::Boxed {
kind: crate::value_layout::CompositeKind::Enum,
count: args.len() as u16,
meta,
}));
}
}
}
Expr::ArrayLiteral { elements, .. } => {
let byte_size = if elements.is_empty() {
Some(0u16)
} else {
infer_expr_type(fc, &elements[0]).and_then(|elem_ty| {
flat_alloc_bytes(
&TypeExpr::array_lit(
Box::new(elem_ty),
elements.len() as i64,
Span::default(),
),
&fc.type_info,
)
})
};
let count = elements.len() as u16;
let byte_size = byte_size.unwrap_or_else(|| conservative_alloc_bytes(count));
if elements.iter().any(|e| elem_may_intern_opaque(fc, e)) {
fc.may_intern_opaque = true;
}
for elem in elements {
compile_expr(fc, elem)?;
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count,
byte_size,
}));
}
Expr::TupleLiteral { elements, .. } => {
let elem_types: Option<Vec<TypeExpr>> =
elements.iter().map(|e| infer_expr_type(fc, e)).collect();
let byte_size = elem_types.and_then(|types| {
flat_alloc_bytes(&TypeExpr::Tuple(types, Span::default()), &fc.type_info)
});
let count = elements.len() as u16;
let byte_size = byte_size.unwrap_or_else(|| conservative_alloc_bytes(count));
if elements.iter().any(|e| elem_may_intern_opaque(fc, e)) {
fc.may_intern_opaque = true;
}
for elem in elements {
compile_expr(fc, elem)?;
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Tuple,
count,
byte_size,
}));
}
Expr::Cast {
expr: inner,
target,
span,
} => {
if matches!(target, TypeExpr::Multiword(..)) && target.as_multiword_lit().is_none() {
return Err(CompileError {
message: String::from(
"Multiword cast target has an out-of-range or unresolved dimension after monomorphization",
),
span: *span,
});
}
if let Some((n, _)) = target.as_multiword_lit() {
let byte_size = flat_alloc_bytes(
&TypeExpr::multiword_lit(n, 0, Span::default()),
&fc.type_info,
)
.unwrap_or_else(|| conservative_alloc_bytes(n));
if let Expr::TupleLiteral { elements, .. } = inner.as_ref() {
for e in elements {
compile_expr(fc, e)?;
}
} else {
compile_expr(fc, inner)?;
let tmp = fc.declare_local("__mw_src");
fc.emit(Op::SetLocal(tmp));
let elem_types = alloc::vec![
Some(TypeExpr::Prim(PrimType::Word, Span::default()));
n as usize
];
for i in 0..n as usize {
fc.emit(Op::GetLocal(tmp));
let operand = tuple_field_access(fc, &elem_types, i);
fc.emit(Op::GetTupleField(operand));
}
}
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Array,
count: n,
byte_size,
}));
return Ok(());
}
let source = infer_expr_type(fc, inner);
if matches!(target, TypeExpr::Prim(PrimType::Word, _))
&& let Some(TypeExpr::Named(enum_name, _, _, _)) = source.as_ref()
&& fc.type_info.enum_variant_order.contains_key(enum_name)
{
let enum_name = enum_name.clone();
compile_enum_to_word(fc, inner, &enum_name)?;
return Ok(());
}
compile_expr(fc, inner)?;
let source_is_newtype = match source.as_ref() {
Some(TypeExpr::Named(name, _, _, _)) => fc.type_info.newtype_names.contains(name),
_ => false,
};
let target_is_newtype = match target {
TypeExpr::Named(name, _, _, _) => fc.type_info.newtype_names.contains(name),
_ => false,
};
if source_is_newtype || target_is_newtype {
} else {
match (source.as_ref(), target) {
(_, TypeExpr::Prim(PrimType::Float, _)) => {
fc.emit(Op::IntToFloat);
}
(
Some(TypeExpr::Prim(PrimType::Byte, _)),
TypeExpr::Prim(PrimType::Word, _),
) => {
fc.emit(Op::ByteToWord);
}
(
Some(TypeExpr::Prim(PrimType::Fixed(n), _)),
TypeExpr::Prim(PrimType::Word, _),
) => {
fc.emit(Op::FixedToWord(
n.unwrap_or(crate::typecheck::DEFAULT_FIXED_FRAC_BITS),
));
}
(_, TypeExpr::Prim(PrimType::Word, _)) => {
fc.emit(Op::FloatToInt);
}
(_, TypeExpr::Prim(PrimType::Byte, _)) => {
fc.emit(Op::WordToByte);
}
(_, TypeExpr::Prim(PrimType::Fixed(n), _)) => {
fc.emit(Op::WordToFixed(
n.unwrap_or(crate::typecheck::DEFAULT_FIXED_FRAC_BITS),
));
}
_ => {
}
}
}
}
Expr::Placeholder { span } => {
return Err(CompileError {
message: String::from("placeholder _ outside of pipeline"),
span: *span,
});
}
Expr::Closure { span, .. } | Expr::ClosureRef { span, .. } => {
return Err(CompileError {
message: String::from(
"internal: closure expression reached the compiler after V0.2.0 \
Phase 4 retired the closure family. This is a compiler bug.",
),
span: *span,
});
}
Expr::Checked {
op_expr,
arms,
span,
} => {
compile_checked(fc, op_expr, arms, span)?;
}
Expr::SaturateMax { .. } => {
let idx = fc.add_constant(Value::Int(i64::MAX));
fc.emit(Op::Const(idx));
}
Expr::SaturateMin { .. } => {
let idx = fc.add_constant(Value::Int(i64::MIN));
fc.emit(Op::Const(idx));
}
Expr::Classify { value, labels, .. } | Expr::Declassify { value, labels, .. } => {
let label_op = fc.chunk.ops.len();
compile_expr(fc, value)?;
fc.record_ifc_labels(label_op, labels);
}
}
Ok(())
}
fn compile_checked(
fc: &mut FuncCompiler,
op_expr: &Expr,
arms: &[crate::ast::CheckedArm],
span: &Span,
) -> Result<(), CompileError> {
use crate::ast::{CheckedArmKind, Pattern};
if let Expr::ArrayIndex { object, index, .. } = op_expr {
return compile_checked_index(fc, op_expr, object, index, arms, span);
}
if let Expr::Call { name, args, .. } = op_expr
&& fc.type_info.newtype_names.contains(name)
&& args.len() == 1
{
return compile_checked_newtype(fc, op_expr, name, &args[0], arms, span);
}
if let Expr::Cast {
expr: inner,
target,
..
} = op_expr
&& let TypeExpr::Named(enum_name, _, _, _) = target
&& fc.type_info.enum_variant_order.contains_key(enum_name)
{
let enum_name = enum_name.clone();
return compile_checked_discriminant(fc, inner, &enum_name, arms, span);
}
if let Expr::Call { name, args, .. } = op_expr
&& fc.native_map.contains_key(name)
{
return compile_checked_native(fc, op_expr, name, args, arms, span);
}
let operand = match op_expr {
Expr::BinOp { left, .. } => Some(left.as_ref()),
Expr::UnaryOp { operand, .. } => Some(operand.as_ref()),
_ => None,
};
let operand_fixed_n: Option<u8> =
operand
.and_then(|e| infer_expr_type(fc, e))
.and_then(|t| match t {
TypeExpr::Prim(PrimType::Fixed(n), _) => {
Some(n.unwrap_or(crate::typecheck::DEFAULT_FIXED_FRAC_BITS))
}
_ => None,
});
match op_expr {
Expr::BinOp {
op: BinOp::Add,
left,
right,
..
} => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CheckedAdd);
}
Expr::BinOp {
op: BinOp::Sub,
left,
right,
..
} => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CheckedSub);
}
Expr::BinOp {
op: BinOp::Mul,
left,
right,
..
} => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CheckedMul(operand_fixed_n.unwrap_or(0)));
}
Expr::BinOp {
op: BinOp::Div,
left,
right,
..
} => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CheckedDiv(operand_fixed_n.unwrap_or(0)));
}
Expr::BinOp {
op: BinOp::Mod,
left,
right,
..
} => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CheckedMod);
}
Expr::BinOp {
op: BinOp::AShl,
left,
right,
..
} => {
let word_bits = (fc.type_info.word_bytes * 8) as i64;
let k = const_shift_amount(right, word_bits - 1)?;
let mul = fc.add_constant(Value::Int(1i64 << k));
compile_expr(fc, left)?;
fc.emit(Op::Const(mul));
fc.emit(Op::CheckedMul(0));
}
Expr::UnaryOp {
op: UnaryOp::Neg,
operand,
..
} => {
compile_expr(fc, operand)?;
fc.emit(Op::CheckedNeg);
}
_ => {
return Err(CompileError {
message: alloc::string::String::from(
"checked-overflow construct currently supports only the operators `+`, `-`, `*`, `/`, `%`, `<<<`, and unary `-`",
),
span: *span,
});
}
}
let bind_ty = if let Some(n) = operand_fixed_n {
TypeExpr::Prim(PrimType::Fixed(Some(n)), *span)
} else {
match operand.and_then(|e| infer_expr_type(fc, e)) {
Some(TypeExpr::Prim(PrimType::Byte, _)) => TypeExpr::Prim(PrimType::Byte, *span),
_ => TypeExpr::Prim(PrimType::Word, *span),
}
};
let suffix = span.start;
let flag_name = alloc::format!("__checked_flag_{}", suffix);
let low_name = alloc::format!("__checked_low_{}", suffix);
let high_name = alloc::format!("__checked_high_{}", suffix);
let flag_slot = fc.declare_local(&flag_name);
let low_slot = fc.declare_local(&low_name);
let high_slot = fc.declare_local(&high_name);
fc.emit(Op::SetLocal(flag_slot));
fc.emit(Op::SetLocal(high_slot));
fc.emit(Op::SetLocal(low_slot));
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for arm in arms {
fc.begin_scope();
let mut fail_addrs: Vec<usize> = Vec::new();
let (class_flag, single_pattern, h_pattern, l_pattern) = match &arm.kind {
CheckedArmKind::Ok(p) => (0_i64, Some(p), None, None),
CheckedArmKind::Overflow(h, Some(l)) => (1_i64, None, Some(h), Some(l)),
CheckedArmKind::Underflow(h, Some(l)) => (2_i64, None, Some(h), Some(l)),
CheckedArmKind::Overflow(p, None) => (1_i64, Some(p), None, None),
CheckedArmKind::Underflow(p, None) => (2_i64, Some(p), None, None),
CheckedArmKind::ZeroDivisor(p) => (3_i64, Some(p), None, None),
CheckedArmKind::Nan(p) => (4_i64, Some(p), None, None),
CheckedArmKind::InvalidIndex(_)
| CheckedArmKind::InvalidNewtype(_)
| CheckedArmKind::PayloadDiscriminant(_)
| CheckedArmKind::InvalidDiscriminant(_)
| CheckedArmKind::Error(_) => {
return Err(CompileError {
message: alloc::string::String::from(
"internal error: non-arithmetic outcome arm on an arithmetic checked construct",
),
span: *span,
});
}
};
fc.emit(Op::GetLocal(flag_slot));
let class_idx = fc.add_constant(Value::Int(class_flag));
fc.emit(Op::Const(class_idx));
fc.emit(Op::CmpEq);
let class_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(class_fail);
let test_literal =
|fc: &mut FuncCompiler, pat: &Pattern, slot: u16, fail_addrs: &mut Vec<usize>| {
if let Pattern::Literal(crate::ast::Literal::Int(v), _) = pat {
fc.emit(Op::GetLocal(slot));
let idx = fc.add_constant(Value::Int(*v));
fc.emit(Op::Const(idx));
fc.emit(Op::CmpEq);
let fail = fc.emit_jump(Op::If(0));
fail_addrs.push(fail);
}
};
if let Some(p) = single_pattern {
test_literal(fc, p, low_slot, &mut fail_addrs);
}
if let (Some(h), Some(l)) = (h_pattern, l_pattern) {
test_literal(fc, h, high_slot, &mut fail_addrs);
test_literal(fc, l, low_slot, &mut fail_addrs);
}
let bind_var = |fc: &mut FuncCompiler,
pat: &Pattern,
slot: u16,
ty: &TypeExpr|
-> Result<(), CompileError> {
if let Pattern::Variable(name, _) = pat {
let v_slot = fc.declare_local_typed(name, Some(ty.clone()));
fc.emit(Op::GetLocal(slot));
fc.emit(Op::SetLocal(v_slot));
}
Ok(())
};
if let Some(p) = single_pattern {
bind_var(fc, p, low_slot, &bind_ty)?;
}
if let (Some(h), Some(l)) = (h_pattern, l_pattern) {
bind_var(fc, h, high_slot, &bind_ty)?;
bind_var(fc, l, low_slot, &bind_ty)?;
}
if let Some(guard) = arm.guard.as_ref() {
compile_expr(fc, guard)?;
let guard_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(guard_fail);
}
compile_expr(fc, &arm.body)?;
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
fc.end_scope();
for addr in fail_addrs.into_iter().rev() {
fc.patch_jump(addr);
fc.emit(Op::EndIf);
}
}
fc.emit(Op::GetLocal(flag_slot));
let three_idx = fc.add_constant(Value::Int(3));
fc.emit(Op::Const(three_idx));
fc.emit(Op::CmpEq);
let not_zero_divisor = fc.emit_jump(Op::If(0));
fc.emit(Op::Trap(crate::bytecode::TrapKind::ZeroDivisor.code()));
fc.patch_jump(not_zero_divisor);
fc.emit(Op::EndIf);
fc.emit(Op::GetLocal(low_slot));
let default_break = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(default_break);
}
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn compile_checked_index(
fc: &mut FuncCompiler,
op_expr: &Expr,
object: &Expr,
index: &Expr,
arms: &[crate::ast::CheckedArm],
span: &Span,
) -> Result<(), CompileError> {
use crate::ast::{CheckedArmKind, Pattern};
let elem_ty = infer_expr_type(fc, op_expr).unwrap_or(TypeExpr::Prim(PrimType::Word, *span));
let word_ty = TypeExpr::Prim(PrimType::Word, *span);
let suffix = span.start;
let arr_slot = fc.declare_local(&alloc::format!("__idx_arr_{}", suffix));
let idx_slot = fc.declare_local(&alloc::format!("__idx_i_{}", suffix));
let len_slot = fc.declare_local(&alloc::format!("__idx_len_{}", suffix));
let flag_slot = fc.declare_local(&alloc::format!("__idx_flag_{}", suffix));
let elem_slot = fc.declare_local(&alloc::format!("__idx_elem_{}", suffix));
compile_expr(fc, object)?;
fc.emit(Op::SetLocal(arr_slot));
compile_expr(fc, index)?;
fc.emit(Op::SetLocal(idx_slot));
let arr_len = infer_expr_type(fc, object)
.as_ref()
.and_then(array_length_of_type);
if let Some(n) = arr_len {
let n_const = fc.add_constant(Value::Int(n));
fc.emit(Op::Const(n_const));
} else {
fc.emit(Op::GetLocal(arr_slot));
fc.emit(Op::Len);
}
fc.emit(Op::SetLocal(len_slot));
let one_idx = fc.add_constant(Value::Int(1));
fc.emit(Op::Const(one_idx));
fc.emit(Op::SetLocal(flag_slot));
fc.emit(Op::GetLocal(idx_slot));
let zero_idx = fc.add_constant(Value::Int(0));
fc.emit(Op::Const(zero_idx));
fc.emit(Op::CmpGe);
let ge_skip = fc.emit_jump(Op::If(0));
fc.emit(Op::GetLocal(idx_slot));
fc.emit(Op::GetLocal(len_slot));
fc.emit(Op::CmpLt);
let lt_skip = fc.emit_jump(Op::If(0));
fc.emit(Op::GetLocal(arr_slot));
fc.emit(Op::GetLocal(idx_slot));
fc.emit(Op::GetIndex(array_elem_operand(
Some(&elem_ty),
&fc.type_info,
)));
fc.emit(Op::SetLocal(elem_slot));
let zero_flag_idx = fc.add_constant(Value::Int(0));
fc.emit(Op::Const(zero_flag_idx));
fc.emit(Op::SetLocal(flag_slot));
fc.patch_jump(lt_skip);
fc.emit(Op::EndIf);
fc.patch_jump(ge_skip);
fc.emit(Op::EndIf);
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for arm in arms {
fc.begin_scope();
let mut fail_addrs: Vec<usize> = Vec::new();
let (class_flag, pat, bind_slot, bind_ty): (i64, &Pattern, u16, &TypeExpr) = match &arm.kind
{
CheckedArmKind::Ok(p) => (0, p, elem_slot, &elem_ty),
CheckedArmKind::InvalidIndex(p) => (1, p, idx_slot, &word_ty),
_ => {
return Err(CompileError {
message: alloc::string::String::from(
"internal error: non-indexing arm in an indexing checked construct",
),
span: *span,
});
}
};
fc.emit(Op::GetLocal(flag_slot));
let cidx = fc.add_constant(Value::Int(class_flag));
fc.emit(Op::Const(cidx));
fc.emit(Op::CmpEq);
let class_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(class_fail);
if let Pattern::Literal(crate::ast::Literal::Int(v), _) = pat {
fc.emit(Op::GetLocal(bind_slot));
let idxc = fc.add_constant(Value::Int(*v));
fc.emit(Op::Const(idxc));
fc.emit(Op::CmpEq);
let fail = fc.emit_jump(Op::If(0));
fail_addrs.push(fail);
}
if let Pattern::Variable(name, _) = pat {
let v_slot = fc.declare_local_typed(name, Some(bind_ty.clone()));
fc.emit(Op::GetLocal(bind_slot));
fc.emit(Op::SetLocal(v_slot));
}
if let Some(guard) = arm.guard.as_ref() {
compile_expr(fc, guard)?;
let guard_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(guard_fail);
}
compile_expr(fc, &arm.body)?;
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
fc.end_scope();
for addr in fail_addrs.into_iter().rev() {
fc.patch_jump(addr);
fc.emit(Op::EndIf);
}
}
fc.emit(Op::GetLocal(arr_slot));
fc.emit(Op::GetLocal(idx_slot));
fc.emit(Op::GetIndex(array_elem_operand(
Some(&elem_ty),
&fc.type_info,
)));
let default_break = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(default_break);
}
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn compile_checked_newtype(
fc: &mut FuncCompiler,
op_expr: &Expr,
newtype_name: &str,
arg: &Expr,
arms: &[crate::ast::CheckedArm],
span: &Span,
) -> Result<(), CompileError> {
use crate::ast::{CheckedArmKind, Pattern};
let newtype_ty = infer_expr_type(fc, op_expr).unwrap_or_else(|| {
TypeExpr::Named(
alloc::string::String::from(newtype_name),
Vec::new(),
Vec::new(),
*span,
)
});
let underlying_ty = infer_expr_type(fc, arg).unwrap_or(TypeExpr::Prim(PrimType::Word, *span));
let suffix = span.start;
let value_slot = fc.declare_local(&alloc::format!("__nt_val_{}", suffix));
let flag_slot = fc.declare_local(&alloc::format!("__nt_flag_{}", suffix));
compile_expr(fc, arg)?;
fc.emit(Op::SetLocal(value_slot));
if let Some(pred_name) = fc.type_info.newtype_refinements.get(newtype_name).cloned() {
let pred_idx = *fc
.function_map
.get(pred_name.as_str())
.ok_or_else(|| CompileError {
message: alloc::format!(
"refinement predicate `{}` for newtype `{}` is not a declared function",
pred_name,
newtype_name
),
span: *span,
})?;
let one_idx = fc.add_constant(Value::Int(1));
fc.emit(Op::Const(one_idx));
fc.emit(Op::SetLocal(flag_slot));
fc.emit(Op::GetLocal(value_slot));
fc.emit(Op::Call(pred_idx, 1));
let pred_fail = fc.emit_jump(Op::If(0));
let zero_idx = fc.add_constant(Value::Int(0));
fc.emit(Op::Const(zero_idx));
fc.emit(Op::SetLocal(flag_slot));
fc.patch_jump(pred_fail);
fc.emit(Op::EndIf);
} else {
let zero_idx = fc.add_constant(Value::Int(0));
fc.emit(Op::Const(zero_idx));
fc.emit(Op::SetLocal(flag_slot));
}
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for arm in arms {
fc.begin_scope();
let mut fail_addrs: Vec<usize> = Vec::new();
let (class_flag, pat, bind_ty): (i64, &Pattern, &TypeExpr) = match &arm.kind {
CheckedArmKind::Ok(p) => (0, p, &newtype_ty),
CheckedArmKind::InvalidNewtype(p) => (1, p, &underlying_ty),
_ => {
return Err(CompileError {
message: alloc::string::String::from(
"internal error: non-newtype arm in a newtype-construction checked construct",
),
span: *span,
});
}
};
fc.emit(Op::GetLocal(flag_slot));
let cidx = fc.add_constant(Value::Int(class_flag));
fc.emit(Op::Const(cidx));
fc.emit(Op::CmpEq);
let class_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(class_fail);
if let Pattern::Literal(crate::ast::Literal::Int(v), _) = pat {
fc.emit(Op::GetLocal(value_slot));
let idxc = fc.add_constant(Value::Int(*v));
fc.emit(Op::Const(idxc));
fc.emit(Op::CmpEq);
let fail = fc.emit_jump(Op::If(0));
fail_addrs.push(fail);
}
if let Pattern::Variable(vname, _) = pat {
let v_slot = fc.declare_local_typed(vname, Some(bind_ty.clone()));
fc.emit(Op::GetLocal(value_slot));
fc.emit(Op::SetLocal(v_slot));
}
if let Some(guard) = arm.guard.as_ref() {
compile_expr(fc, guard)?;
let guard_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(guard_fail);
}
compile_expr(fc, &arm.body)?;
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
fc.end_scope();
for addr in fail_addrs.into_iter().rev() {
fc.patch_jump(addr);
fc.emit(Op::EndIf);
}
}
fc.emit(Op::GetLocal(flag_slot));
let one_idx = fc.add_constant(Value::Int(1));
fc.emit(Op::Const(one_idx));
fc.emit(Op::CmpEq);
let not_invalid = fc.emit_jump(Op::If(0));
fc.emit(Op::Trap(crate::bytecode::TrapKind::RefinementFailed.code()));
fc.record_operator_site(span);
fc.patch_jump(not_invalid);
fc.emit(Op::EndIf);
fc.emit(Op::GetLocal(value_slot));
let default_break = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(default_break);
}
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn compile_checked_discriminant(
fc: &mut FuncCompiler,
inner: &Expr,
enum_name: &str,
arms: &[crate::ast::CheckedArm],
span: &Span,
) -> Result<(), CompileError> {
use crate::ast::{CheckedArmKind, Pattern};
fn variant_name(p: &Pattern) -> Option<&str> {
match p {
Pattern::Variable(name, _) if name.chars().next().is_some_and(|c| c.is_uppercase()) => {
Some(name)
}
_ => None,
}
}
let mut ok_specific: BTreeMap<&str, &crate::ast::CheckedArm> = BTreeMap::new();
let mut ok_generic: Option<&crate::ast::CheckedArm> = None;
let mut payload_specific: BTreeMap<&str, &crate::ast::CheckedArm> = BTreeMap::new();
let mut payload_catchall: Option<&crate::ast::CheckedArm> = None;
let mut invalid_arm: Option<&crate::ast::CheckedArm> = None;
for arm in arms {
match &arm.kind {
CheckedArmKind::Ok(p) => match variant_name(p) {
Some(v) => {
ok_specific.insert(v, arm);
}
None => {
if ok_generic.is_none() {
ok_generic = Some(arm);
}
}
},
CheckedArmKind::PayloadDiscriminant(p) => match variant_name(p) {
Some(v) => {
payload_specific.insert(v, arm);
}
None => {
if payload_catchall.is_none() {
payload_catchall = Some(arm);
}
}
},
CheckedArmKind::InvalidDiscriminant(_) if invalid_arm.is_none() => {
invalid_arm = Some(arm);
}
_ => {}
}
}
let suffix = span.start;
let temp = fc.declare_local(&alloc::format!("__disc_{}", suffix));
compile_expr(fc, inner)?;
fc.emit(Op::SetLocal(temp));
let variants: Vec<(String, i64)> = fc
.type_info
.enum_variant_order
.get(enum_name)
.cloned()
.unwrap_or_default();
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for (vname, disc) in &variants {
let arity = fc
.type_info
.enums
.get(enum_name)
.and_then(|m| m.get(vname))
.map(|fields| fields.len())
.unwrap_or(0);
fc.begin_scope();
fc.emit(Op::GetLocal(temp));
let disc_const = fc.add_constant(Value::Int(*disc));
fc.emit(Op::Const(disc_const));
fc.emit(Op::CmpEq);
let fail = fc.emit_jump(Op::If(0));
if arity == 0 {
if let Some(arm) = ok_specific.get(vname.as_str()) {
compile_expr(fc, &arm.body)?;
} else if let Some(arm) = ok_generic {
let disc = fc
.type_info
.enum_variant_order
.get(enum_name)
.and_then(|vs| {
vs.iter()
.find(|(n, _)| n.as_str() == vname.as_str())
.map(|(_, d)| *d)
})
.unwrap_or(0);
let ty = TypeExpr::Named(
enum_name.to_string(),
Vec::new(),
Vec::new(),
Span::default(),
);
match flat_alloc_bytes(&ty, &fc.type_info) {
Some(byte_size) => {
let d_const = fc.add_constant(Value::Int(disc));
fc.emit(Op::Const(d_const));
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Enum,
count: 1,
byte_size,
}));
}
None => {
let meta = fc.add_struct_template(enum_name, alloc::vec![vname.clone()]);
fc.emit(Op::NewComposite(NewCompositeOperand::Boxed {
kind: crate::value_layout::CompositeKind::Enum,
count: 0,
meta,
}));
}
}
match &arm.kind {
CheckedArmKind::Ok(Pattern::Variable(bind, _)) => {
let slot = fc.declare_local_typed(
bind,
Some(TypeExpr::Named(
alloc::string::String::from(enum_name),
Vec::new(),
Vec::new(),
*span,
)),
);
fc.emit(Op::SetLocal(slot));
}
_ => {
fc.emit(Op::PopN(1));
}
}
compile_expr(fc, &arm.body)?;
} else {
let disc = fc
.type_info
.enum_variant_order
.get(enum_name)
.and_then(|vs| {
vs.iter()
.find(|(n, _)| n.as_str() == vname.as_str())
.map(|(_, d)| *d)
})
.unwrap_or(0);
let ty = TypeExpr::Named(
enum_name.to_string(),
Vec::new(),
Vec::new(),
Span::default(),
);
match flat_alloc_bytes(&ty, &fc.type_info) {
Some(byte_size) => {
let d_const = fc.add_constant(Value::Int(disc));
fc.emit(Op::Const(d_const));
fc.emit(Op::NewComposite(NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Enum,
count: 1,
byte_size,
}));
}
None => {
let meta = fc.add_struct_template(enum_name, alloc::vec![vname.clone()]);
fc.emit(Op::NewComposite(NewCompositeOperand::Boxed {
kind: crate::value_layout::CompositeKind::Enum,
count: 0,
meta,
}));
}
}
}
} else {
let arm = payload_specific
.get(vname.as_str())
.copied()
.or(payload_catchall);
match arm {
Some(arm) => compile_expr(fc, &arm.body)?,
None => {
return Err(CompileError {
message: alloc::format!(
"internal error: payload-bearing variant `{}` is uncovered in a discriminant-to-enum conversion",
vname
),
span: *span,
});
}
}
}
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
fc.patch_jump(fail);
fc.emit(Op::EndIf);
fc.end_scope();
}
fc.begin_scope();
if let Some(arm) = invalid_arm {
if let CheckedArmKind::InvalidDiscriminant(Pattern::Variable(bind, _)) = &arm.kind {
let slot = fc.declare_local_typed(bind, Some(TypeExpr::Prim(PrimType::Word, *span)));
fc.emit(Op::GetLocal(temp));
fc.emit(Op::SetLocal(slot));
}
compile_expr(fc, &arm.body)?;
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
} else {
fc.emit(Op::Trap(
crate::bytecode::TrapKind::EnumVariantUnmapped.code(),
));
}
fc.end_scope();
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn compile_checked_native(
fc: &mut FuncCompiler,
op_expr: &Expr,
native_name: &str,
args: &[Expr],
arms: &[crate::ast::CheckedArm],
span: &Span,
) -> Result<(), CompileError> {
use crate::ast::{CheckedArmKind, Pattern};
let ok_ty = infer_expr_type(fc, op_expr).unwrap_or(TypeExpr::Prim(PrimType::Word, *span));
let word_ty = TypeExpr::Prim(PrimType::Word, *span);
let has_error_arm = arms
.iter()
.any(|a| matches!(a.kind, CheckedArmKind::Error(_)));
if args.len() >= 0x80 {
return Err(CompileError {
message: alloc::string::String::from(
"a native call in an error-handling construct may take at most 127 arguments",
),
span: *span,
});
}
for arg in args {
compile_expr(fc, arg)?;
}
let mut argc = args.len() as u8;
if has_error_arm {
argc |= 0x80;
}
let idx = *fc.native_map.get(native_name).ok_or_else(|| CompileError {
message: alloc::format!("unregistered native: {}", native_name),
span: *span,
})?;
let is_external = fc
.native_externals
.get(native_name)
.copied()
.unwrap_or(false);
if is_external {
fc.emit(Op::CallExternalNative(idx, argc));
} else {
fc.emit(Op::CallVerifiedNative(idx, argc));
}
let suffix = span.start;
let value_slot = fc.declare_local(&alloc::format!("__nat_val_{}", suffix));
let flag_slot = fc.declare_local(&alloc::format!("__nat_flag_{}", suffix));
if has_error_arm {
fc.emit(Op::SetLocal(flag_slot));
fc.emit(Op::SetLocal(value_slot));
} else {
fc.emit(Op::SetLocal(value_slot));
let zero_idx = fc.add_constant(Value::Int(0));
fc.emit(Op::Const(zero_idx));
fc.emit(Op::SetLocal(flag_slot));
}
let loop_addr = fc.emit(Op::Loop(0));
fc.enter_loop();
for arm in arms {
fc.begin_scope();
let mut fail_addrs: Vec<usize> = Vec::new();
let (class_flag, pat, bind_ty): (i64, &Pattern, &TypeExpr) = match &arm.kind {
CheckedArmKind::Ok(p) => (0, p, &ok_ty),
CheckedArmKind::Error(p) => (1, p, &word_ty),
_ => {
return Err(CompileError {
message: alloc::string::String::from(
"internal error: non-native-call arm in a native-call checked construct",
),
span: *span,
});
}
};
fc.emit(Op::GetLocal(flag_slot));
let cidx = fc.add_constant(Value::Int(class_flag));
fc.emit(Op::Const(cidx));
fc.emit(Op::CmpEq);
let class_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(class_fail);
if let Pattern::Literal(crate::ast::Literal::Int(v), _) = pat {
fc.emit(Op::GetLocal(value_slot));
let idxc = fc.add_constant(Value::Int(*v));
fc.emit(Op::Const(idxc));
fc.emit(Op::CmpEq);
let fail = fc.emit_jump(Op::If(0));
fail_addrs.push(fail);
}
if let Pattern::Variable(vname, _) = pat {
let v_slot = fc.declare_local_typed(vname, Some(bind_ty.clone()));
fc.emit(Op::GetLocal(value_slot));
fc.emit(Op::SetLocal(v_slot));
}
if let Some(guard) = arm.guard.as_ref() {
compile_expr(fc, guard)?;
let guard_fail = fc.emit_jump(Op::If(0));
fail_addrs.push(guard_fail);
}
compile_expr(fc, &arm.body)?;
let break_addr = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(break_addr);
}
fc.end_scope();
for addr in fail_addrs.into_iter().rev() {
fc.patch_jump(addr);
fc.emit(Op::EndIf);
}
}
fc.emit(Op::GetLocal(flag_slot));
let one_idx = fc.add_constant(Value::Int(1));
fc.emit(Op::Const(one_idx));
fc.emit(Op::CmpEq);
let not_error = fc.emit_jump(Op::If(0));
fc.emit(Op::Trap(crate::bytecode::TrapKind::NoMatchingArm.code()));
fc.patch_jump(not_error);
fc.emit(Op::EndIf);
fc.emit(Op::GetLocal(value_slot));
let default_break = fc.emit(Op::Break(0));
if let Some(breaks) = fc.loop_breaks.last_mut() {
breaks.push(default_break);
}
let endloop_addr = fc.emit(Op::EndLoop(0));
let after_loop = (loop_addr + 1) as u16;
if let Op::EndLoop(a) = &mut fc.chunk.ops[endloop_addr] {
*a = after_loop;
}
let after_endloop = fc.chunk.ops.len() as u16;
if let Op::Loop(a) = &mut fc.chunk.ops[loop_addr] {
*a = after_endloop;
}
fc.exit_loop();
Ok(())
}
fn compile_call(
fc: &mut FuncCompiler,
name: &str,
args: &[Expr],
span: &Span,
) -> Result<(), CompileError> {
if fc.type_info.newtype_names.contains(name) {
if args.len() != 1 {
return Err(CompileError {
message: alloc::format!(
"newtype `{}` constructor expects 1 argument, got {}",
name,
args.len()
),
span: *span,
});
}
if let Some(pred_name) = fc.type_info.newtype_refinements.get(name).cloned()
&& let Some((param_name, body)) =
fc.type_info.refinement_bodies.get(&pred_name).cloned()
{
if let Some(n) = fold_to_int(&args[0], &|s| fc.local_const_lookup(s)) {
match eval_predicate_at_int(&body, ¶m_name, n) {
Some(true) => {
let opt_op = fc.chunk.ops.len();
compile_expr(fc, &args[0])?;
fc.record_optimisation(opt_op, "refinement-elision");
return Ok(());
}
Some(false) => {
return Err(CompileError {
message: alloc::format!(
"refinement check `{}` provably fails for newtype `{}` at compile time on argument {}",
pred_name,
name,
n
),
span: *span,
});
}
None => {}
}
}
if let Some(arg_range) = infer_arg_range(&args[0], fc)
&& let Some(true_set) = predicate_true_set(&body, ¶m_name)
{
if !arg_range.is_empty() && arg_range.is_subset_of(&true_set) {
let opt_op = fc.chunk.ops.len();
compile_expr(fc, &args[0])?;
fc.record_optimisation(opt_op, "refinement-elision");
return Ok(());
}
if !arg_range.is_empty() && arg_range.intersect(&true_set).is_empty() {
return Err(CompileError {
message: alloc::format!(
"refinement check `{}` provably fails for newtype `{}` at compile time; argument range is disjoint from the predicate's true set",
pred_name,
name
),
span: *span,
});
}
}
}
compile_expr(fc, &args[0])?;
if let Some(pred_name) = fc.type_info.newtype_refinements.get(name).cloned() {
let pred_idx =
*fc.function_map
.get(pred_name.as_str())
.ok_or_else(|| CompileError {
message: alloc::format!(
"refinement predicate `{}` for newtype `{}` is not a declared function",
pred_name,
name
),
span: *span,
})?;
fc.emit(Op::Dup);
fc.emit(Op::Call(pred_idx, 1));
let if_addr = fc.emit_jump(Op::If(0));
let else_addr = fc.emit_jump(Op::Else(0));
fc.patch_jump(if_addr);
fc.emit(Op::Trap(crate::bytecode::TrapKind::RefinementFailed.code()));
fc.patch_jump(else_addr);
fc.emit(Op::EndIf);
}
return Ok(());
}
if let Some(_slot) = fc.resolve_local(name) {
return Err(CompileError {
message: alloc::format!(
"`{}` is a local variable, not a callable. \
V0.2.0 admits only direct calls to top-level \
functions, methods, and host natives.",
name
),
span: *span,
});
}
for arg in args {
compile_expr(fc, arg)?;
}
let arg_count = args.len() as u8;
if let Some(&idx) = fc.function_map.get(name) {
fc.emit(Op::Call(idx, arg_count));
} else if let Some(&idx) = fc.native_map.get(name) {
let is_external = fc.native_externals.get(name).copied().unwrap_or(false);
if is_external {
fc.emit(Op::CallExternalNative(idx, arg_count));
} else {
fc.emit(Op::CallVerifiedNative(idx, arg_count));
}
} else {
return Err(CompileError {
message: format!("undefined function: {}", name),
span: *span,
});
}
Ok(())
}
fn emit_consume_peeked_scrutinee(fc: &mut FuncCompiler) {
let scratch = fc.declare_local("__pat_test");
fc.emit(Op::SetLocal(scratch));
fc.emit(Op::PopN(1));
fc.emit(Op::GetLocal(scratch));
}
fn compile_pattern_test(
fc: &mut FuncCompiler,
pattern: &Pattern,
value_slot: u16,
ty: Option<&TypeExpr>,
) -> Result<Vec<usize>, CompileError> {
let mut fail_addrs = Vec::new();
match pattern {
Pattern::Variable(_, _) | Pattern::Wildcard(_) => {
}
Pattern::Literal(lit, _) => {
fc.emit(Op::GetLocal(value_slot));
match lit {
Literal::Int(v) => {
let idx = fc.add_constant(Value::Int(*v));
fc.emit(Op::Const(idx));
}
Literal::Byte(v) => {
let idx = fc.add_constant(Value::Byte(*v));
fc.emit(Op::Const(idx));
}
Literal::Fixed { raw, .. } => {
let idx = fc.add_constant(Value::Fixed(*raw));
fc.emit(Op::Const(idx));
}
#[cfg(feature = "floats")]
Literal::Float(v) => {
let idx = fc.add_constant(Value::Float(*v));
fc.emit(Op::Const(idx));
}
#[cfg(not(feature = "floats"))]
Literal::Float(_) => {
unreachable!(
"float literals are rejected at lex time when the `floats` feature is off"
);
}
Literal::String(s) => {
let idx = fc.add_constant(Value::StaticStr(s.clone()));
fc.emit(Op::Const(idx));
}
Literal::Bool(true) => {
fc.emit(Op::PushImmediate(1));
}
Literal::Bool(false) => {
fc.emit(Op::PushImmediate(2));
}
Literal::Unit => {
fc.emit(Op::PushImmediate(0));
}
}
fc.emit(Op::CmpEq);
fail_addrs.push(fc.emit_jump(Op::If(0)));
}
Pattern::Enum(enum_name, variant, sub_pats, _) => {
fc.emit(Op::GetLocal(value_slot));
let e_const = fc.add_string_constant(enum_name);
let v_const = fc.add_string_constant(variant);
let is_option_some = enum_name == "Option" && variant == "Some";
let disc = if is_option_some {
1
} else if enum_name == "Option" && variant == "None" {
0
} else {
fc.type_info
.enum_variant_order
.get(enum_name)
.and_then(|vs| vs.iter().find(|(n, _)| n == variant).map(|(_, d)| *d))
.unwrap_or(0)
};
let d_const = fc.add_constant(Value::Int(disc));
fc.emit(Op::IsEnum(e_const, v_const, d_const));
emit_consume_peeked_scrutinee(fc);
fail_addrs.push(fc.emit_jump(Op::If(0)));
for (i, sub_pat) in sub_pats.iter().enumerate() {
if matches!(sub_pat, Pattern::Variable(_, _) | Pattern::Wildcard(_)) {
continue; }
let temp = fc.declare_local(&format!("__enum_field{}", i));
fc.emit(Op::GetLocal(value_slot));
let efield = if is_option_some {
option_some_field(fc, ty, i)
} else {
enum_field_access(fc, enum_name, variant, i)
};
fc.emit(Op::GetEnumField(efield));
fc.emit(Op::SetLocal(temp));
let sub_ty = if is_option_some {
option_inner(ty)
} else {
fc.type_info
.enums
.get(enum_name)
.and_then(|m| m.get(variant))
.and_then(|payloads| payloads.get(i))
.cloned()
};
let sub_fails = compile_pattern_test(fc, sub_pat, temp, sub_ty.as_ref())?;
fail_addrs.extend(sub_fails);
}
}
Pattern::Struct(type_name, field_pats, _) => {
if named_type_name(ty) != Some(type_name.as_str()) {
fc.emit(Op::GetLocal(value_slot));
let t_const = fc.add_string_constant(type_name);
fc.emit(Op::IsStruct(t_const));
emit_consume_peeked_scrutinee(fc);
fail_addrs.push(fc.emit_jump(Op::If(0)));
}
for field_pat in field_pats {
if let Some(pat) = &field_pat.pattern {
if matches!(pat, Pattern::Variable(_, _) | Pattern::Wildcard(_)) {
continue;
}
let temp = fc.declare_local(&format!("__struct_{}", field_pat.name));
fc.emit(Op::GetLocal(value_slot));
let field_op = struct_field_access(fc, type_name, &field_pat.name);
fc.emit(Op::GetField(field_op));
fc.emit(Op::SetLocal(temp));
let sub_ty = fc
.type_info
.structs
.get(type_name)
.and_then(|m| m.get(&field_pat.name))
.cloned();
let sub_fails = compile_pattern_test(fc, pat, temp, sub_ty.as_ref())?;
fail_addrs.extend(sub_fails);
}
}
}
Pattern::Tuple(pats, _) => {
let elem_types = tuple_elem_types_of(ty, pats.len());
for (i, pat) in pats.iter().enumerate() {
if matches!(pat, Pattern::Variable(_, _) | Pattern::Wildcard(_)) {
continue;
}
let temp = fc.declare_local(&format!("__tuple_{}", i));
fc.emit(Op::GetLocal(value_slot));
fc.emit(Op::GetTupleField(tuple_field_access(fc, &elem_types, i)));
fc.emit(Op::SetLocal(temp));
let sub_ty = elem_types.get(i).and_then(|o| o.as_ref());
let sub_fails = compile_pattern_test(fc, pat, temp, sub_ty)?;
fail_addrs.extend(sub_fails);
}
}
}
Ok(fail_addrs)
}
fn compile_pattern_bind_typed(
fc: &mut FuncCompiler,
pattern: &Pattern,
value_slot: u16,
ty: Option<TypeExpr>,
) -> Result<(), CompileError> {
match pattern {
Pattern::Variable(name, _) => {
fc.emit(Op::GetLocal(value_slot));
let slot = fc.declare_local_typed(name, ty);
fc.emit(Op::SetLocal(slot));
}
Pattern::Wildcard(_) | Pattern::Literal(_, _) => {
}
Pattern::Enum(enum_name, variant, sub_pats, _) => {
let is_option_some = enum_name == "Option" && variant == "Some";
let payload_types: Vec<Option<TypeExpr>> = if is_option_some {
alloc::vec![option_inner(ty.as_ref())]
} else {
fc.type_info
.enums
.get(enum_name)
.and_then(|variants| variants.get(variant))
.map(|tys| tys.iter().cloned().map(Some).collect())
.unwrap_or_else(|| sub_pats.iter().map(|_| None).collect())
};
for (i, sub_pat) in sub_pats.iter().enumerate() {
if matches!(sub_pat, Pattern::Wildcard(_) | Pattern::Literal(_, _)) {
continue;
}
fc.emit(Op::GetLocal(value_slot));
let efield = if is_option_some {
option_some_field(fc, ty.as_ref(), i)
} else {
enum_field_access(fc, enum_name, variant, i)
};
fc.emit(Op::GetEnumField(efield));
let sub_ty = payload_types.get(i).cloned().unwrap_or(None);
if let Pattern::Variable(name, _) = sub_pat {
let slot = fc.declare_local_typed(name, sub_ty);
fc.emit(Op::SetLocal(slot));
} else {
let temp = fc.declare_local(&format!("__bind_tmp{}", i));
fc.emit(Op::SetLocal(temp));
compile_pattern_bind_typed(fc, sub_pat, temp, sub_ty)?;
}
}
}
Pattern::Struct(struct_name, field_pats, _) => {
let field_types: BTreeMap<String, TypeExpr> = fc
.type_info
.structs
.get(struct_name)
.cloned()
.unwrap_or_default();
for field_pat in field_pats {
fc.emit(Op::GetLocal(value_slot));
let field_op = struct_field_access(fc, struct_name, &field_pat.name);
fc.emit(Op::GetField(field_op));
let field_ty = field_types.get(&field_pat.name).cloned();
if let Some(pat) = &field_pat.pattern {
if let Pattern::Variable(vname, _) = pat {
let slot = fc.declare_local_typed(vname, field_ty);
fc.emit(Op::SetLocal(slot));
} else if matches!(pat, Pattern::Wildcard(_)) {
fc.emit(Op::PopN(1));
} else {
let temp = fc.declare_local(&format!("__sf_{}", field_pat.name));
fc.emit(Op::SetLocal(temp));
compile_pattern_bind_typed(fc, pat, temp, field_ty)?;
}
} else {
let slot = fc.declare_local_typed(&field_pat.name, field_ty);
fc.emit(Op::SetLocal(slot));
}
}
}
Pattern::Tuple(pats, _) => {
let elem_types: Vec<Option<TypeExpr>> = match &ty {
Some(TypeExpr::Tuple(ts, _)) if ts.len() == pats.len() => {
ts.iter().cloned().map(Some).collect()
}
_ => pats.iter().map(|_| None).collect(),
};
for (i, pat) in pats.iter().enumerate() {
if matches!(pat, Pattern::Wildcard(_) | Pattern::Literal(_, _)) {
continue;
}
fc.emit(Op::GetLocal(value_slot));
fc.emit(Op::GetTupleField(tuple_field_access(fc, &elem_types, i)));
let sub_ty = elem_types.get(i).cloned().unwrap_or(None);
if let Pattern::Variable(name, _) = pat {
let slot = fc.declare_local_typed(name, sub_ty);
fc.emit(Op::SetLocal(slot));
} else {
let temp = fc.declare_local(&format!("__tup_bind{}", i));
fc.emit(Op::SetLocal(temp));
compile_pattern_bind_typed(fc, pat, temp, sub_ty)?;
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::tokenize;
use crate::parser::parse;
use alloc::string::ToString;
fn compile_str(src: &str) -> Result<Module, CompileError> {
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
compile(&program)
}
fn compile_str_debug(src: &str) -> Module {
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
compile_with_options(
&program,
&crate::target::Target::host(),
&CompileOptions { emit_debug: true },
)
.expect("compile")
.0
}
#[test]
fn debug_emission_records_call_site_at_the_call_op() {
let src = "fn helper() -> Word { 1 }\nfn main() -> Word { helper() }";
let module = compile_str_debug(src);
let main_chunk = module
.chunks
.iter()
.find(|c| c.ops.iter().any(|op| matches!(op, Op::Call(..))))
.expect("a chunk containing a call");
let pool = main_chunk
.debug_pool
.as_ref()
.expect("debug build attaches a debug pool to the calling chunk");
let call_idx = main_chunk
.ops
.iter()
.position(|op| matches!(op, Op::Call(..)))
.unwrap();
let call_records: alloc::vec::Vec<_> = pool
.records
.iter()
.filter(|r| r.kind == crate::debug_meta::DebugRecordKind::CallSite)
.collect();
assert_eq!(call_records.len(), 1, "one CallSite for the single call");
assert_eq!(
call_records[0].op_index as usize, call_idx,
"the CallSite must key on the call instruction's op index"
);
let span_idx = call_records[0].operands[0] as usize;
let (_file, start, len) = pool.span_pool[span_idx];
let snippet = &src[start as usize..(start + len) as usize];
assert!(
snippet.contains("helper"),
"span should cover the call expression, got {:?}",
snippet
);
}
#[test]
fn debug_read_path_resolves_call_to_source() {
let src = "fn helper() -> Word { 1 }\nfn main() -> Word { helper() }";
let module = compile_str_debug(src);
let main_chunk = module
.chunks
.iter()
.find(|c| c.ops.iter().any(|op| matches!(op, Op::Call(..))))
.expect("a chunk containing a call");
let pool = main_chunk.debug_pool.as_ref().expect("debug pool present");
let call_idx = main_chunk
.ops
.iter()
.position(|op| matches!(op, Op::Call(..)))
.unwrap() as u32;
let rec = pool
.records_at(call_idx)
.next()
.expect("a record at the call op");
let loc = pool
.source_location(rec)
.expect("the call-site record resolves to a source location");
let snippet = &src[loc.byte_offset as usize..(loc.byte_offset + loc.byte_length) as usize];
assert!(
snippet.contains("helper"),
"resolved location should cover the call expression, got {:?}",
snippet
);
}
#[test]
fn debug_emission_covers_the_emittable_catalogue() {
let src = "fn helper() -> Word { 1 }\nfn main() -> Word { let x = helper(); x }";
let module = compile_str_debug(src);
let main_chunk = module
.chunks
.iter()
.find(|c| c.ops.iter().any(|op| matches!(op, Op::Call(..))))
.expect("a chunk containing a call");
let pool = main_chunk.debug_pool.as_ref().expect("debug pool present");
use crate::debug_meta::DebugRecordKind;
let present = |k: DebugRecordKind| pool.records.iter().any(|r| r.kind == k);
assert!(present(DebugRecordKind::CallSite), "CallSite emitted");
assert!(present(DebugRecordKind::SourceSpan), "SourceSpan emitted");
assert!(present(DebugRecordKind::LineNumber), "LineNumber emitted");
assert!(
present(DebugRecordKind::VariableName),
"VariableName emitted"
);
let var = pool
.records
.iter()
.find(|r| r.kind == DebugRecordKind::VariableName)
.unwrap();
let name_idx = var.operands[1];
assert_eq!(pool.string(name_idx), Some("x"));
}
#[test]
fn debug_emission_records_refinement_elision_as_optimisation() {
let src = "fn nonneg(x: Word) -> bool { x >= 0 }\n\
newtype Counter = Word where nonneg;\n\
fn main() -> Word { let c = Counter(5); c as Word }";
let module = compile_str_debug(src);
let found = module.chunks.iter().any(|c| {
c.debug_pool.as_ref().is_some_and(|p| {
p.records.iter().any(|r| {
r.kind == crate::debug_meta::DebugRecordKind::OptimisationMarker
&& p.string(r.operands[0]) == Some("refinement-elision")
})
})
});
assert!(
found,
"a constant-folded refinement construction should record a refinement-elision OptimisationMarker"
);
}
#[test]
fn debug_emission_records_generic_instantiation() {
let src = "fn id<T>(x: T) -> T { x }\nfn main() -> Word { id(5) }";
let module = compile_str_debug(src);
let found = module.chunks.iter().any(|c| {
c.debug_pool.as_ref().is_some_and(|p| {
p.records.iter().any(|r| {
r.kind == crate::debug_meta::DebugRecordKind::GenericInstantiation
&& p.string(r.operands[0]) == Some("id")
})
})
});
assert!(
found,
"the monomorphized id<Word> chunk should record a GenericInstantiation with origin `id`"
);
}
#[test]
fn debug_emission_records_ifc_label_annotation() {
let src = "fn secret() -> Word@Secret { classify 5@Secret }\n\
fn main() -> Word { declassify secret()@Secret }";
let module = compile_str_debug(src);
let found = module.chunks.iter().any(|c| {
c.debug_pool.as_ref().is_some_and(|p| {
p.records.iter().any(|r| {
r.kind == crate::debug_meta::DebugRecordKind::IfcLabelAnnotation
&& r.operands.iter().any(|&op| p.string(op) == Some("Secret"))
})
})
});
assert!(
found,
"classify/declassify @Secret should record an IfcLabelAnnotation with label Secret"
);
}
#[test]
fn debug_emission_records_type_annotation_for_locals() {
let src = "fn main() -> Word { let x: Word = 5; x }";
let module = compile_str_debug(src);
let found = module.chunks.iter().any(|c| {
c.debug_pool.as_ref().is_some_and(|p| {
p.records.iter().any(|r| {
r.kind == crate::debug_meta::DebugRecordKind::TypeAnnotation
&& p.type_blob(r.operands[1]) == Some("Word".as_bytes())
})
})
});
assert!(
found,
"a typed local should record a TypeAnnotation with TypeRepr `Word`"
);
}
#[cfg(feature = "verify")]
#[test]
fn debug_emission_records_wcet_marker_for_stream_chunks() {
let src = "loop main(tick: Word) -> Word { let r = yield tick; r }";
let module = compile_str_debug(src);
let stream = module
.chunks
.iter()
.find(|c| matches!(c.block_type, crate::bytecode::BlockType::Stream))
.expect("a Stream chunk");
let pool = stream
.debug_pool
.as_ref()
.expect("Stream chunk carries a debug pool under --debug");
let rec = pool
.records
.iter()
.find(|r| r.kind == crate::debug_meta::DebugRecordKind::WcetMarker)
.expect("a WcetMarker record");
let reconstructed = rec.operands[1] as u32 | ((rec.operands[2] as u32) << 16);
let expected = crate::verify::wcet_stream_iteration(stream).unwrap();
assert_eq!(
reconstructed, expected,
"the WcetMarker cycle count should match the verifier's WCET"
);
}
#[test]
fn assert_emits_trap_and_context_under_debug() {
let module = compile_str_debug("fn main() -> Word { assert false, \"boom\"; 0 }");
let assert_code = crate::bytecode::TrapKind::AssertionFailed.code();
let chunk = module
.chunks
.iter()
.find(|c| {
c.ops
.iter()
.any(|op| matches!(op, Op::Trap(c) if *c == assert_code))
})
.expect("debug build emits an AssertionFailed trap");
let pool = chunk.debug_pool.as_ref().expect("debug pool present");
let rec = pool
.records
.iter()
.find(|r| r.kind == crate::debug_meta::DebugRecordKind::AssertionContext)
.expect("an AssertionContext record");
assert!(pool.source_location(rec).is_some());
assert_eq!(pool.string(rec.operands[1]), Some("boom"));
}
#[test]
fn assert_is_compiled_out_in_release() {
let module = compile_str("fn main() -> Word { assert false; 0 }").expect("compile");
let assert_code = crate::bytecode::TrapKind::AssertionFailed.code();
for c in &module.chunks {
assert!(
!c.ops
.iter()
.any(|op| matches!(op, Op::Trap(code) if *code == assert_code)),
"a release build must compile the assert out (no trap op)"
);
assert!(
c.debug_pool.is_none(),
"release build carries no debug pool"
);
}
}
#[test]
fn assert_non_bool_condition_is_type_error() {
let err = compile_str("fn main() -> Word { assert 5; 0 }").expect_err("type error");
assert!(err.message.contains("bool"), "got: {}", err.message);
}
#[test]
fn assert_call_form_remains_a_function_call() {
let module =
compile_str_debug("fn assert(x: Word) -> Word { x }\nfn main() -> Word { assert(7) }");
let assert_code = crate::bytecode::TrapKind::AssertionFailed.code();
assert!(
!module.chunks.iter().any(|c| c
.ops
.iter()
.any(|op| matches!(op, Op::Trap(c) if *c == assert_code))),
"assert(7) is a call, not an assertion statement"
);
}
#[test]
fn assert_strip_keeps_check_drops_context() {
use crate::debug_meta::DebugRecordKind;
let assert_code = crate::bytecode::TrapKind::AssertionFailed.code();
let has_trap = |m: &Module| {
m.chunks.iter().any(|c| {
c.ops
.iter()
.any(|op| matches!(op, Op::Trap(c) if *c == assert_code))
})
};
let mut debug = compile_str_debug("fn main() -> Word { assert false; 0 }");
assert!(has_trap(&debug), "debug build emits the assert check");
assert!(
debug
.chunks
.iter()
.any(|c| c.debug_pool.as_ref().is_some_and(|p| p
.records
.iter()
.any(|r| r.kind == DebugRecordKind::AssertionContext))),
"debug build carries an AssertionContext record"
);
for c in &mut debug.chunks {
c.debug_pool = None;
}
assert!(has_trap(&debug), "strip keeps the assert check ops");
let release = compile_str("fn main() -> Word { assert false; 0 }").expect("compile");
assert!(!has_trap(&release), "release build has no assert check");
}
#[cfg(feature = "verify")]
#[test]
fn assert_in_nested_loop_body_verifies_and_passes() {
use crate::vm::{Vm, VmState};
let module = compile_str_debug(
"fn main() -> Word { for i in 0..3 { assert i < 10, \"small\"; } 0 }",
);
let arena = crate::Arena::with_capacity(crate::vm::DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("verify accepts assert in a loop body");
match vm.call(&[]).expect("call") {
VmState::Finished(crate::Value::Int(0)) => {}
other => panic!("expected Finished(Int(0)), got {:?}", other),
}
}
#[cfg(feature = "verify")]
#[test]
fn assert_in_loop_main_stream_chunk_verifies() {
use crate::vm::Vm;
let module = compile_str_debug(
"loop main(tick: Word) -> Word { assert tick >= 0, \"t\"; let r = yield tick; r }",
);
let arena = crate::Arena::with_capacity(crate::vm::DEFAULT_ARENA_CAPACITY);
let _vm = Vm::new(module, &arena).expect("verify accepts assert in a Stream chunk");
}
#[cfg(feature = "verify")]
#[test]
fn assert_false_traps_at_runtime_under_debug() {
use crate::vm::Vm;
let module = compile_str_debug("fn main() -> Word { assert false, \"nope\"; 0 }");
let arena = crate::Arena::with_capacity(crate::vm::DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("verify");
match vm.call(&[]) {
Err(crate::vm::VmError::AssertionFailed) => {}
other => panic!("expected AssertionFailed, got {:?}", other),
}
}
#[cfg(feature = "verify")]
#[test]
fn assert_true_passes_at_runtime_under_debug() {
use crate::vm::{Vm, VmState};
let module = compile_str_debug("fn main() -> Word { assert true; 0 }");
let arena = crate::Arena::with_capacity(crate::vm::DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("verify");
match vm.call(&[]).expect("call") {
VmState::Finished(crate::Value::Int(0)) => {}
other => panic!("expected Finished(Int(0)), got {:?}", other),
}
}
#[test]
fn debug_emission_records_breakpoint_candidates() {
let src = "fn main() -> Word { let x = 1; let y = 2; x }";
let module = compile_str_debug(src);
let pool = module
.chunks
.iter()
.find_map(|c| c.debug_pool.as_ref())
.expect("debug pool present");
let candidates: alloc::vec::Vec<_> = pool
.records
.iter()
.filter(|r| r.kind == crate::debug_meta::DebugRecordKind::BreakpointCandidate)
.collect();
assert!(
!candidates.is_empty(),
"a debug build records breakpoint candidates"
);
for c in &candidates {
assert!(
pool.source_location(c).is_some(),
"a breakpoint candidate resolves to a source location"
);
}
let release = compile_str(src).expect("compile");
assert!(release.chunks.iter().all(|c| c.debug_pool.is_none()));
}
#[cfg(feature = "verify")]
fn witness_pairs(pool: &crate::debug_meta::DebugPool) -> alloc::vec::Vec<(&str, &str)> {
use crate::debug_meta::DebugRecordKind;
pool.records
.iter()
.filter(|r| r.kind == DebugRecordKind::VerifierWitness)
.filter_map(|r| {
let pass = pool.string(*r.operands.first()?)?;
let property = pool.string(*r.operands.get(1)?)?;
Some((pass, property))
})
.collect()
}
#[cfg(feature = "verify")]
#[test]
fn debug_emission_records_verifier_witness() {
let module = compile_str_debug("fn main() -> Word { 1 }");
let pool = module
.chunks
.iter()
.find_map(|c| c.debug_pool.as_ref())
.expect("debug pool present");
let pairs = witness_pairs(pool);
let passes: alloc::vec::Vec<&str> = pairs.iter().map(|(p, _)| *p).collect();
assert!(passes.contains(&"block-nesting-and-offsets"));
assert!(passes.contains(&"block-type-constraints"));
assert!(!passes.contains(&"productive-divergence"));
assert!(pairs.contains(&("block-nesting-and-offsets", "all-blocks-closed")));
assert!(pairs.contains(&("block-type-constraints", "func-has-no-yield")));
assert!(pairs.contains(&("block-type-constraints", "func-has-no-stream")));
assert!(pairs.contains(&("block-type-constraints", "func-has-no-reset")));
assert!(pairs.contains(&("resource-bounds", "wcet-per-chunk-bound-proven")));
assert!(pairs.contains(&("resource-bounds", "wcmu-per-chunk-bound-proven")));
let release = compile_str("fn main() -> Word { 1 }").expect("compile");
assert!(release.chunks.iter().all(|c| c.debug_pool.is_none()));
}
#[cfg(feature = "verify")]
#[test]
fn func_resource_witness_does_not_set_module_wcet_header() {
let module = compile_str_debug("fn main() -> Word { 1 + 2 }");
assert_eq!(module.wcet_cycles, 0, "no Stream chunk: header stays auto");
assert_eq!(module.wcmu_bytes, 0, "no Stream chunk: header stays auto");
}
#[cfg(feature = "verify")]
#[test]
fn verifier_witness_records_reentrant_resource_bounds() {
let module = compile_str_debug("yield process(input: Word) -> Word { yield input * 2 }");
let chunk = module
.chunks
.iter()
.find(|c| matches!(c.block_type, crate::bytecode::BlockType::Reentrant))
.expect("a Reentrant chunk");
let pool = chunk.debug_pool.as_ref().expect("debug pool present");
let pairs = witness_pairs(pool);
assert!(pairs.contains(&("resource-bounds", "wcet-per-chunk-bound-proven")));
assert!(pairs.contains(&("resource-bounds", "wcmu-per-chunk-bound-proven")));
}
#[test]
fn debug_emission_records_operator_site_at_division() {
use crate::bytecode::Op;
use crate::debug_meta::DebugRecordKind;
let module = compile_str_debug("fn main(d: Word) -> Word { let x = 1 / d; x }");
let chunk = &module.chunks[0];
let div_op = chunk
.ops
.iter()
.position(|op| matches!(op, Op::Div))
.expect("a Div op") as u32;
let pool = chunk.debug_pool.as_ref().expect("debug pool present");
let kinds: alloc::vec::Vec<_> = pool.records_at(div_op).map(|r| r.kind).collect();
assert!(
kinds.contains(&DebugRecordKind::SourceSpan),
"the Div op has an exact SourceSpan, found {kinds:?}"
);
assert!(
kinds.contains(&DebugRecordKind::BreakpointCandidate),
"the Div op is an operator-level breakpoint candidate"
);
}
#[test]
fn debug_emission_records_function_entry_breakpoint() {
use crate::debug_meta::DebugRecordKind;
let module = compile_str_debug("loop main(tick: Word) -> Word { let r = yield tick; r }");
let chunk = module
.chunks
.iter()
.find(|c| matches!(c.block_type, crate::bytecode::BlockType::Stream))
.expect("a Stream chunk");
let pool = chunk.debug_pool.as_ref().expect("debug pool present");
assert!(
pool.records_at(0)
.any(|r| r.kind == DebugRecordKind::BreakpointCandidate),
"function entry (op 0) is a breakpoint candidate"
);
}
#[cfg(feature = "verify")]
#[test]
fn verifier_witness_marks_stream_productive_divergence() {
let module = compile_str_debug("loop main(tick: Word) -> Word { let r = yield tick; r }");
let stream = module
.chunks
.iter()
.find(|c| matches!(c.block_type, crate::bytecode::BlockType::Stream))
.expect("a Stream chunk");
let pool = stream.debug_pool.as_ref().expect("debug pool present");
let pairs = witness_pairs(pool);
assert!(
pairs.contains(&("productive-divergence", "every-stream-to-reset-path-yields")),
"a Stream chunk's witness records the productive-divergence proof"
);
assert!(pairs.contains(&("block-type-constraints", "stream-has-yield")));
assert!(pairs.contains(&("block-type-constraints", "stream-has-exactly-one-stream")));
}
#[cfg(feature = "verify")]
#[test]
fn verifier_witness_records_resource_bounds() {
let module = compile_str_debug("loop main(tick: Word) -> Word { let r = yield tick; r }");
let stream = module
.chunks
.iter()
.find(|c| matches!(c.block_type, crate::bytecode::BlockType::Stream))
.expect("a Stream chunk");
let pool = stream.debug_pool.as_ref().expect("debug pool present");
let pairs = witness_pairs(pool);
assert!(
pairs.contains(&("resource-bounds", "wcet-per-iteration-bound-proven")),
"the WCET bound proof is recorded, found {pairs:?}"
);
assert!(
pairs.contains(&("resource-bounds", "wcmu-per-iteration-bound-proven")),
"the WCMU bound proof is recorded, found {pairs:?}"
);
let func = compile_str_debug("fn main() -> Word { 1 }");
let fpool = func
.chunks
.iter()
.find_map(|c| c.debug_pool.as_ref())
.expect("debug pool present");
let fpairs = witness_pairs(fpool);
assert!(fpairs.contains(&("resource-bounds", "wcet-per-chunk-bound-proven")));
assert!(fpairs.contains(&("resource-bounds", "wcmu-per-chunk-bound-proven")));
assert!(
!fpairs
.iter()
.any(|(_, prop)| prop.ends_with("per-iteration-bound-proven")),
"a Func chunk does not record the Stream per-iteration variant"
);
}
#[cfg(feature = "verify")]
#[test]
fn verifier_witness_keys_obligations_to_construct_positions() {
use crate::bytecode::Op;
use crate::debug_meta::DebugRecordKind;
let module = compile_str_debug("fn main() -> Word { if true { 1 } else { 2 } }");
let chunk = module
.chunks
.iter()
.find(|c| c.name == "main")
.expect("main chunk");
let if_pos = chunk
.ops
.iter()
.position(|op| matches!(op, Op::If(_)))
.expect("an If op") as u32;
let pool = chunk.debug_pool.as_ref().expect("debug pool present");
let at_if: alloc::vec::Vec<&str> = pool
.records_at(if_pos)
.filter(|r| r.kind == DebugRecordKind::VerifierWitness)
.filter_map(|r| pool.string(*r.operands.get(1)?))
.collect();
assert!(
at_if.contains(&"if-branch-target-in-bounds"),
"the If obligation is keyed to op {if_pos}, found {at_if:?}"
);
assert_ne!(if_pos, 0);
}
#[test]
fn release_build_emits_no_debug_pool() {
let src = "fn helper() -> Word { 1 }\nfn main() -> Word { helper() }";
let module = compile_str(src).expect("compile");
assert!(
module.chunks.iter().all(|c| c.debug_pool.is_none()),
"a default (release) build must carry no debug metadata"
);
}
#[test]
fn debug_build_strips_to_release_bytes() {
let src = "fn helper() -> Word { 1 }\nfn main() -> Word { helper() }";
let release = compile_str(src).expect("compile");
let mut debug = compile_str_debug(src);
assert!(
debug.chunks.iter().any(|c| c.debug_pool.is_some()),
"debug build should have emitted at least one pool"
);
for c in &mut debug.chunks {
c.debug_pool = None;
}
assert_eq!(
release.to_bytes().expect("encode release"),
debug.to_bytes().expect("encode stripped"),
"stripped debug build must be byte-identical to the release build"
);
}
#[test]
fn compile_simple_fn() {
let module = compile_str("fn add(a: Word, b: Word) -> Word { a + b }").unwrap();
assert_eq!(module.chunks.len(), 1);
assert_eq!(module.chunks[0].name, "add");
assert_eq!(module.chunks[0].param_count, 2);
}
#[test]
fn compile_literal_fn() {
let module = compile_str("fn fortytwo() -> Word { 42 }").unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(module.chunks[0].ops.contains(&Op::Return));
}
#[test]
fn compile_if_else() {
let module =
compile_str("fn max(a: Word, b: Word) -> Word { if a > b { a } else { b } }").unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::If(_)))
);
}
#[test]
fn compile_let_binding() {
let module = compile_str("fn double(x: Word) -> Word { let y = x * 2; y }").unwrap();
assert_eq!(module.chunks.len(), 1);
}
#[test]
fn compile_for_range() {
let module = compile_str(
"fn sum_to(n: Word) -> Word { let total = 0; for i in 0..n { let x = total + i; } total }"
).unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::Loop(_)))
);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::EndLoop(_)))
);
}
#[test]
fn compile_function_call() {
let module = compile_str(
"fn double(x: Word) -> Word { x * 2 }\nfn quad(x: Word) -> Word { double(double(x)) }",
)
.unwrap();
assert_eq!(module.chunks.len(), 2);
let quad = &module.chunks[1];
assert!(quad.ops.iter().any(|op| matches!(op, Op::Call(_, 1))));
}
#[test]
fn compile_multiheaded() {
let module = compile_str(
"fn classify(0) -> Text { \"zero\" }\nfn classify(x: Word) -> Text { \"other\" }",
)
.unwrap();
assert_eq!(module.chunks.len(), 1);
}
#[test]
fn compile_enum_variant() {
let module = compile_str(
"enum Color { Red, Green, Blue }\nfn make() -> Color { let x = Color::Red(); x }",
)
.unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::NewComposite(o) if o.kind() == crate::value_layout::CompositeKind::Enum))
);
}
#[test]
fn compile_enum_to_word_cast_implicit() {
let module = compile_str(
"enum Color { Red, Green, Blue }\n\
fn pick() -> Word { let c = Color::Green(); c as Word }",
)
.unwrap();
assert_eq!(module.chunks.len(), 1);
let ops = &module.chunks[0].ops;
let isenum_count = ops
.iter()
.filter(|op| matches!(op, Op::IsEnum(_, _, _)))
.count();
assert!(
isenum_count >= 3,
"expected at least one IsEnum per variant, got {}",
isenum_count
);
assert!(ops.iter().any(|op| matches!(op, Op::Loop(_))));
assert!(ops.iter().any(|op| matches!(op, Op::EndLoop(_))));
}
#[test]
fn compile_enum_to_word_cast_explicit_discriminants() {
let module = compile_str(
"enum Code { A = 10, B = 20, C = 30 }\n\
fn pick() -> Word { let c = Code::B(); c as Word }",
)
.unwrap();
let consts = &module.chunks[0].constants;
let int_consts: Vec<i64> = consts
.iter()
.filter_map(|c| match c {
ConstValue::Int(n) => Some(*n),
_ => None,
})
.collect();
for expected in &[10, 20, 30] {
assert!(
int_consts.contains(expected),
"expected discriminant {} in constant pool, got {:?}",
expected,
int_consts
);
}
}
#[test]
fn compile_enum_to_word_cast_negative_discriminant() {
let module = compile_str(
"enum Sign { Neg = -1, Zero = 0, Pos = 1 }\n\
fn pick() -> Word { let s = Sign::Neg(); s as Word }",
)
.unwrap();
let consts = &module.chunks[0].constants;
let int_consts: Vec<i64> = consts
.iter()
.filter_map(|c| match c {
ConstValue::Int(n) => Some(*n),
_ => None,
})
.collect();
assert!(int_consts.contains(&-1));
assert!(int_consts.contains(&1));
}
#[test]
fn compile_struct_init() {
let module = compile_str(
"struct Point { x: Word, y: Word }\nfn make() -> Point { let p = Point { x: 1, y: 2 }; p }",
)
.unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::NewComposite(o) if o.kind() == crate::value_layout::CompositeKind::Struct))
);
}
#[test]
fn compile_yield_function() {
let module = compile_str("yield process(input: Word) -> Word { yield input * 2 }").unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::Yield))
);
assert_eq!(module.chunks[0].block_type, BlockType::Reentrant);
}
#[test]
fn compile_loop_function() {
let module =
compile_str("loop main(input: Word) -> Word { let input = yield input + 1; input }")
.unwrap();
assert_eq!(module.chunks.len(), 1);
assert_eq!(module.chunks[0].block_type, BlockType::Stream);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::Stream))
);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::Reset))
);
}
#[test]
fn compile_entry_point() {
let module = compile_str("fn main(x: Word) -> Word { x }").unwrap();
assert!(module.entry_point.is_some());
}
#[test]
fn compile_pipeline() {
let module = compile_str(
"fn double(x: Word) -> Word { x * 2 }\nfn apply(x: Word) -> Word { x |> double() }",
)
.unwrap();
assert_eq!(module.chunks.len(), 2);
}
#[test]
fn error_undefined_variable() {
let result = compile_str("fn bad() -> Word { unknown }");
assert!(result.is_err());
}
#[test]
fn error_undefined_function() {
let result = compile_str("fn bad() -> Word { missing(1) }");
assert!(result.is_err());
}
#[test]
fn error_break_outside_loop() {
let result = compile_str("fn bad() -> () { break; }");
assert!(result.is_err());
}
#[test]
fn compile_for_in_array() {
let module =
compile_str("fn main() -> Word { let s = 0; for x in [1, 2, 3] { let s = s + x; } s }")
.unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::Loop(_)))
);
assert!(!module.chunks[0].ops.iter().any(|op| matches!(op, Op::Len)));
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::GetIndex(_)))
);
}
#[test]
fn compile_array_index_bakes_flat_access() {
let module = compile_str("fn main() -> Word { let a = [10, 20, 30]; a[1] }").unwrap();
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::GetIndex(crate::bytecode::ArrayElem::Flat { .. })))
);
}
#[test]
fn compile_checked_index_folds_length_no_op_len() {
let module = compile_str(
"fn main() -> Word { let a = [10, 20, 30]; a[1] { ok(v) => v, invalid_index(i) => i } }",
)
.unwrap();
let ops = &module.chunks[0].ops;
assert!(!ops.iter().any(|op| matches!(op, Op::Len)));
assert!(
ops.iter()
.any(|op| matches!(op, Op::GetIndex(crate::bytecode::ArrayElem::Flat { .. })))
);
}
#[test]
fn compile_struct_access_bakes_flat_field() {
let module = compile_str(
"struct P { a: Word, b: Word }\nfn main() -> Word { let p = P { a: 1, b: 2 }; p.a + p.b }",
)
.unwrap();
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::GetField(crate::bytecode::StructField::Flat { .. })))
);
}
#[test]
fn compile_struct_pattern_folds_is_struct() {
let module = compile_str(
"struct P { a: Word, b: Word }\nfn main() -> Word { let p = P { a: 1, b: 2 }; match p { P { a, b } => a + b, _ => 0 } }",
)
.unwrap();
let ops = &module.chunks[0].ops;
assert!(!ops.iter().any(|op| matches!(op, Op::IsStruct(_))));
assert!(
ops.iter()
.any(|op| matches!(op, Op::GetField(crate::bytecode::StructField::Flat { .. })))
);
}
#[test]
fn compile_tuple_literal() {
let module =
compile_str("fn main() -> (Word, Word, Word) { let t = (1, 2, 3); t }").unwrap();
assert_eq!(module.chunks.len(), 1);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::NewComposite(o) if o.kind() == crate::value_layout::CompositeKind::Tuple && o.count() == 3))
);
}
#[test]
fn compile_block_structured_control() {
let module = compile_str("fn main() -> Word { if true { 1 } else { 2 } }").unwrap();
for op in &module.chunks[0].ops {
assert!(
!matches!(
op,
Op::Loop(_) | Op::EndLoop(_) | Op::Break(_) | Op::BreakIf(_)
),
"unexpected loop instruction in simple if/else"
);
}
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::If(_)))
);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::Else(_)))
);
assert!(
module.chunks[0]
.ops
.iter()
.any(|op| matches!(op, Op::EndIf))
);
}
#[test]
fn data_block_admits_primitives() {
let src = "data ctx { score: Word, level: Word, ratio: Float, alive: bool }\n\
fn main() -> Word { ctx.score }";
let module = compile_str(src).unwrap();
let layout = module.data_layout.expect("expected data layout");
assert_eq!(layout.slots.len(), 4);
}
#[test]
fn data_block_admits_unit() {
let src = "data ctx { tick: () }\n\
fn main() -> () { ctx.tick }";
let module = compile_str(src).unwrap();
assert!(module.data_layout.is_some());
}
#[test]
fn data_block_admits_tuple_of_admissible() {
let src = "data ctx { pos: (Float, Float) }\n\
fn main() -> (Float, Float) { ctx.pos }";
assert!(compile_str(src).is_ok());
}
#[test]
fn data_block_admits_array_of_admissible() {
let src = "data ctx { samples: [Float; 4] }\n\
fn main() -> () { () }";
assert!(compile_str(src).is_ok());
}
#[test]
fn data_block_admits_option_of_admissible() {
let src = "data ctx { last: Option<Word> }\n\
fn main() -> () { () }";
assert!(compile_str(src).is_ok());
}
#[test]
fn data_block_admits_struct_of_admissible() {
let src = "struct Point { x: Float, y: Float }\n\
data ctx { origin: Point }\n\
fn main() -> () { () }";
assert!(compile_str(src).is_ok());
}
#[test]
fn data_block_admits_enum_of_admissible() {
let src = "enum Status { Idle, Active(Word), Error(Word, Word) }\n\
data ctx { state: Status }\n\
fn main() -> () { () }";
assert!(compile_str(src).is_ok());
}
#[test]
fn data_block_rejects_string() {
let src = "data ctx { name: Text }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("Text"));
}
#[test]
fn data_block_rejects_string_in_tuple() {
let src = "data ctx { pair: (Word, Text) }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("Text"));
}
#[test]
fn data_block_rejects_string_in_array() {
let src = "data ctx { names: [Text; 4] }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("Text"));
}
#[test]
fn data_block_rejects_string_in_option() {
let src = "data ctx { last: Option<Text> }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("Text"));
}
#[test]
fn data_block_rejects_string_in_struct() {
let src = "struct Tag { label: Text }\n\
data ctx { t: Tag }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("Text"));
}
#[test]
fn data_block_rejects_string_in_enum() {
let src = "enum Tag { Named(Text), Unnamed }\n\
data ctx { t: Tag }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("Text"));
}
#[test]
fn data_block_rejects_unknown_named_type() {
let src = "data ctx { handle: Mystery }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("Mystery") || err.message.contains("opaque"));
}
#[test]
fn multiple_data_blocks_same_visibility_rejected() {
let src = "data ctx_a { x: Word }\n\
data ctx_b { y: Word }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("R28") || err.message.contains("one"));
}
#[test]
fn two_private_data_blocks_rejected() {
let src = "private data ctx_a { x: Word }\n\
private data ctx_b { y: Word }\n\
fn main() -> () { () }";
let err = compile_str(src).unwrap_err();
assert!(err.message.contains("R28") || err.message.contains("private"));
}
#[test]
fn no_data_block_compiles() {
let module = compile_str("fn main() -> Word { 42 }").unwrap();
assert!(module.data_layout.is_none());
}
#[test]
fn untyped_param_is_inferred_from_return_type() {
let module = compile_str("fn main(x) -> Word { x }").expect("compile");
assert_eq!(module.chunks.len(), 1);
assert_eq!(module.chunks[0].param_count, 1);
assert_eq!(
module.chunks[0].param_types,
alloc::vec![crate::bytecode::TypeTag::Word],
);
}
#[test]
fn multiheaded_fn_main_dispatches() {
let src = "fn main(0) -> Word { 100 }\n\
fn main(x: Word) -> Word { x }";
let module = compile_str(src).expect("compile");
assert_eq!(module.chunks.len(), 1);
assert_eq!(module.chunks[0].block_type, BlockType::Func);
assert!(module.entry_point.is_some());
}
#[test]
fn multiheaded_yield_main_dispatches() {
let src = "yield main(0) -> Word { yield 100 }\n\
yield main(x: Word) -> Word { yield x }";
let module = compile_str(src).expect("compile");
assert_eq!(module.chunks.len(), 1);
assert_eq!(module.chunks[0].block_type, BlockType::Reentrant);
assert!(module.entry_point.is_some());
}
#[test]
fn multiheaded_loop_main_dispatches() {
let src = "loop main(0) -> Word { yield 100 }\n\
loop main(x: Word) -> Word { let z = yield x; z }";
let module = compile_str(src).expect("compile");
assert_eq!(module.chunks.len(), 1);
assert_eq!(module.chunks[0].block_type, BlockType::Stream);
assert!(module.entry_point.is_some());
let stream_count = module.chunks[0]
.ops
.iter()
.filter(|op| matches!(op, Op::Stream))
.count();
let reset_count = module.chunks[0]
.ops
.iter()
.filter(|op| matches!(op, Op::Reset))
.count();
assert_eq!(stream_count, 1);
assert_eq!(reset_count, 1);
}
#[test]
fn duplicate_fn_main_is_rejected() {
let err = compile_str(
"fn main() -> Word { 1 }\n\
fn main() -> Word { 2 }",
)
.expect_err("expected duplicate-head rejection");
assert!(
err.message.contains("dead code"),
"unexpected error: {}",
err.message
);
}
#[test]
fn duplicate_yield_main_is_rejected() {
let err = compile_str(
"yield main(x: Word) -> Word { yield x }\n\
yield main(x: Word) -> Word { yield x + 1 }",
)
.expect_err("expected duplicate-head rejection");
assert!(
err.message.contains("dead code"),
"unexpected error: {}",
err.message
);
}
#[test]
fn duplicate_loop_main_is_rejected() {
let err = compile_str(
"loop main(x: Word) -> Word { let z = yield x; z }\n\
loop main(x: Word) -> Word { let z = yield x; z }",
)
.expect_err("expected duplicate-head rejection");
assert!(
err.message.contains("dead code"),
"unexpected error: {}",
err.message
);
}
#[test]
fn duplicate_non_entry_function_is_rejected() {
let err = compile_str(
"fn helper(x: Word) -> Word { x }\n\
fn helper(x: Word) -> Word { x + 1 }\n\
fn main() -> Word { helper(0) }",
)
.expect_err("expected duplicate-head rejection");
assert!(
err.message.contains("dead code"),
"unexpected error: {}",
err.message
);
}
#[test]
#[cfg(feature = "verify")]
fn closure_compile_error_carries_source_span() {
let src = "fn main() -> Word {\n\
let fact = |n: Word| if n <= 1 { 1 } else { n * fact(n - 1) };\n\
fact(5)\n\
}";
let err = compile_str(src).expect_err("expected rejection");
assert!(
err.message.contains("closures are not supported"),
"unexpected error: {}",
err.message
);
assert_ne!(
err.span,
crate::token::Span::default(),
"expected source span on closure rejection",
);
}
#[test]
fn chunk_size_thresholds_are_consistent() {
assert_eq!(CHUNK_SIZE_HARD_LIMIT, u16::MAX as usize);
assert_eq!(
CHUNK_SIZE_SOFT_WARN_THRESHOLD,
(CHUNK_SIZE_HARD_LIMIT * 80) / 100
);
#[allow(clippy::assertions_on_constants)]
{
assert!(CHUNK_SIZE_SOFT_WARN_THRESHOLD < CHUNK_SIZE_HARD_LIMIT);
}
}
#[test]
fn small_chunk_produces_no_warnings() {
let tokens = tokenize("fn main() -> Word { 1 + 2 }").expect("lex");
let program = parse(&tokens).expect("parse");
let (_module, warnings) =
compile_with_warnings(&program, &crate::target::Target::host()).unwrap();
assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
}
fn make_chunk_with_ops(name: &str, ops: alloc::vec::Vec<crate::bytecode::Op>) -> Chunk {
Chunk {
name: name.into(),
ops,
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: crate::bytecode::BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
}
}
#[test]
fn soft_warning_fires_on_long_chunk() {
let op_count = CHUNK_SIZE_SOFT_WARN_THRESHOLD + 1;
let ops: alloc::vec::Vec<crate::bytecode::Op> =
(0..op_count).map(|_| crate::bytecode::Op::Return).collect();
let chunk = make_chunk_with_ops("long_chunk", ops);
let mut warnings: alloc::vec::Vec<CompileWarning> = alloc::vec::Vec::new();
check_chunk_size_against_limits(&chunk, crate::token::Span::default(), &mut warnings)
.expect("admissible at this threshold");
assert_eq!(warnings.len(), 1, "expected one soft warning");
let warning = &warnings[0];
assert_eq!(warning.chunk_name, "long_chunk");
assert!(
warning.message.contains("soft-warning threshold"),
"unexpected warning message: {}",
warning.message,
);
assert!(
warning.message.contains(&op_count.to_string()),
"warning message should name the op count, got: {}",
warning.message,
);
}
#[test]
fn hard_cap_rejects_oversize_chunk() {
let op_count = CHUNK_SIZE_HARD_LIMIT + 1;
let ops: alloc::vec::Vec<crate::bytecode::Op> =
(0..op_count).map(|_| crate::bytecode::Op::Return).collect();
let chunk = make_chunk_with_ops("oversize_chunk", ops);
let mut warnings: alloc::vec::Vec<CompileWarning> = alloc::vec::Vec::new();
let err =
check_chunk_size_against_limits(&chunk, crate::token::Span::default(), &mut warnings)
.unwrap_err();
assert!(
err.message.contains("oversize_chunk"),
"diagnostic should name the chunk: {}",
err.message,
);
assert!(err.message.contains(&CHUNK_SIZE_HARD_LIMIT.to_string()));
assert!(
warnings.is_empty(),
"hard cap rejection should not also emit a warning",
);
}
#[test]
fn boundary_chunk_size_no_warning() {
let op_count = CHUNK_SIZE_SOFT_WARN_THRESHOLD;
let ops: alloc::vec::Vec<crate::bytecode::Op> =
(0..op_count).map(|_| crate::bytecode::Op::Return).collect();
let chunk = make_chunk_with_ops("boundary", ops);
let mut warnings: alloc::vec::Vec<CompileWarning> = alloc::vec::Vec::new();
check_chunk_size_against_limits(&chunk, crate::token::Span::default(), &mut warnings)
.unwrap();
assert!(warnings.is_empty());
}
}