extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crate::ast::*;
use crate::bytecode::*;
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>>,
enums: BTreeMap<String, BTreeMap<String, Vec<TypeExpr>>>,
enum_variant_order: BTreeMap<String, Vec<(String, i64)>>,
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>,
}
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,
local_const_values: BTreeMap<u16, i64>,
local_ranges: BTreeMap<u16, crate::interval::IntervalSet>,
}
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,
) -> 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(),
},
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,
local_const_values: BTreeMap::new(),
local_ranges: BTreeMap::new(),
}
}
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, cv: ConstValue) -> u16 {
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;
}
self.locals.push(Local {
name: String::from(name),
slot,
depth: self.scope_depth,
ty,
});
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;
self.chunk
}
}
fn array_length_of_type(t: &TypeExpr) -> Option<i64> {
match t {
TypeExpr::Array(_, n, _) => Some(*n),
_ => None,
}
}
fn element_type_of(t: &TypeExpr) -> Option<TypeExpr> {
match t {
TypeExpr::Array(elem, _, _) => Some((**elem).clone()),
_ => None,
}
}
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> {
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 = crate::monomorphize::monomorphize(owned);
crate::typecheck::check_with_target(&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_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);
}
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, &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 data_layout = if data_layout_slots.is_empty() {
None
} else {
Some(DataLayout {
slots: data_layout_slots,
})
};
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::default();
for type_def in &program.types {
match type_def {
TypeDef::Struct(s) => {
let mut fields = BTreeMap::new();
for f in &s.fields {
fields.insert(f.name.clone(), f.type_expr.clone());
}
type_info.structs.insert(s.name.clone(), fields);
}
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);
}
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 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 mut chunks: Vec<Chunk> = Vec::new();
for (name, defs) in &groups {
let chunk = compile_function_group(
name,
defs,
&function_map,
&native_map,
&native_externals,
&data_fields,
&const_fields,
&type_info,
)?;
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);
}
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,
});
}
}
}
#[cfg_attr(not(feature = "verify"), allow(unused_mut))]
let mut module = Module {
schema_hash: crate::bytecode::compute_schema_hash(data_layout.as_ref()),
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,
flags: 0,
shared_data_bytes: shared_count.saturating_mul(crate::bytecode::VALUE_SLOT_SIZE_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 wcet_overflow = false;
let mut wcmu_overflow = false;
for chunk in &module.chunks {
if matches!(chunk.block_type, crate::bytecode::BlockType::Stream) {
match crate::verify::wcet_stream_iteration(chunk) {
Ok(c) => {
max_wcet = max_wcet.max(c);
}
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);
}
}
Err(_) => {
wcmu_overflow = true;
}
}
}
}
module.wcet_cycles = if wcet_overflow { u32::MAX } else { max_wcet };
module.wcmu_bytes = if wcmu_overflow { u32::MAX } else { max_wcmu };
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::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;
}
}
}
}
if let Some(tail) = &block.tail_expr {
return expr_uses(tail, name);
}
false
}
block_uses(body, name)
}
fn const_value_any(init: &ConstInitializer) -> crate::bytecode::ConstValue {
use crate::bytecode::ConstValue;
match init {
ConstInitializer::Scalar(Literal::Int(n)) => ConstValue::Int(*n),
#[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(),
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 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(),
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 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, _) => (*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);
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));
for index_expr in chain.indices {
compile_expr(fc, index_expr)?;
fc.emit(Op::GetIndex);
}
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));
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));
Ok(())
}
fn validate_data_field_type(
type_expr: &TypeExpr,
types: &[TypeDef],
visiting: &mut BTreeSet<String>,
) -> Result<(), CompileError> {
match type_expr {
TypeExpr::Prim(prim, span) => match prim {
PrimType::Byte
| PrimType::Word
| PrimType::Fixed(_)
| PrimType::Float
| PrimType::Bool => Ok(()),
PrimType::Text => Err(CompileError {
message: String::from(
"data field type Text is not admissible: variable-length \
types cannot be inlined into the data segment",
),
span: *span,
}),
},
TypeExpr::Unit(_) => Ok(()),
TypeExpr::Tuple(elems, _) => {
for elem in elems {
validate_data_field_type(elem, types, visiting)?;
}
Ok(())
}
TypeExpr::Array(elem, _len, _) => validate_data_field_type(elem, types, visiting),
TypeExpr::Option(inner, _) => validate_data_field_type(inner, types, visiting),
TypeExpr::Labelled(inner, _, _) => validate_data_field_type(inner, types, visiting),
TypeExpr::NegativeLabelled(_, _, span) => Err(CompileError {
message: String::from(
"negative information-flow labels (`!Label`) are not admitted on data field types; they are admissible only on function parameter and return types",
),
span: *span,
}),
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, 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, visiting)?;
}
}
visiting.remove(name);
Ok(())
}
Some(TypeDef::Newtype(n)) => {
visiting.insert(name.clone());
validate_data_field_type(&n.underlying, types, 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,
) -> Result<Chunk, 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 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(),
);
fc.chunk.param_count = param_count;
fc.chunk.param_types = first.params.iter().map(type_tag_for_param).collect();
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])?;
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);
}
let msg = fc.add_string_constant(&format!("no matching head for {}", name));
fc.emit(Op::Trap(msg));
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);
}
}
Ok(fc.finish())
}
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 {
compile_expr(fc, tail)?;
} 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 compile_stmt(fc: &mut FuncCompiler, stmt: &Stmt) -> Result<(), CompileError> {
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));
}
}
Ok(())
}
fn infer_expr_type(fc: &FuncCompiler, expr: &Expr) -> Option<TypeExpr> {
match expr {
Expr::StructInit { name, span, .. } => {
Some(TypeExpr::Named(name.clone(), Vec::new(), *span))
}
Expr::EnumVariant {
enum_name, span, ..
} => Some(TypeExpr::Named(enum_name.clone(), 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(
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::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 => {
infer_expr_type(fc, left).or_else(|| infer_expr_type(fc, right))
}
BinOp::Eq
| BinOp::NotEq
| BinOp::Lt
| BinOp::Gt
| BinOp::LtEq
| BinOp::GtEq
| BinOp::And
| BinOp::Or => Some(TypeExpr::Prim(PrimType::Bool, *span)),
}
}
Expr::UnaryOp { operand, .. } => infer_expr_type(fc, operand),
_ => 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::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 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(i as u8));
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
&& 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 {
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);
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::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),
}
}
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);
fc.emit(Op::GetLocal(temp));
fc.emit(Op::IsEnum(e_const, v_const));
let fail_addr = fc.emit_jump(Op::If(0));
fc.emit(Op::PopN(1)); 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();
}
let msg = fc.add_string_constant("enum-to-Word cast: variant not in declaration");
fc.emit(Op::Trap(msg));
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, EvalValue::Bool(a), EvalValue::Bool(b)) => {
Some(EvalValue::Bool(a && b))
}
(crate::ast::BinOp::Or, EvalValue::Bool(a), EvalValue::Bool(b)) => {
Some(EvalValue::Bool(a || b))
}
_ => None,
}
}
_ => None,
}
}
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) {
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) {
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 = match else_block {
Some(b) => block_tail_range(b, params, _type_info, known_summaries)?,
None => return None,
};
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));
}
#[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, ..
} => {
match op {
BinOp::And => {
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::Or => {
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(());
}
_ => {}
}
let operand_ty = infer_expr_type(fc, left).or_else(|| infer_expr_type(fc, 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);
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);
}
}
BinOp::Mod => {
fc.emit(Op::Mod);
}
BinOp::Eq => {
fc.emit(Op::CmpEq);
}
BinOp::NotEq => {
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 => unreachable!(),
}
}
Expr::UnaryOp { op, operand, .. } => {
let operand_is_int = matches!(
infer_expr_type(fc, operand),
None | Some(TypeExpr::Prim(PrimType::Word, _))
);
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);
}
}
}
Expr::Call { name, args, span } => {
compile_call(fc, name, args, 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));
}
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,
});
}
}
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, ..
} => {
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)?;
compile_pattern_bind(fc, &arm.pattern, temp)?;
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);
}
}
let msg = fc.add_string_constant("no matching arm in match expression");
fc.emit(Op::Trap(msg));
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(());
}
compile_expr(fc, object)?;
let name_const = fc.add_string_constant(field);
fc.emit(Op::GetField(name_const));
}
Expr::TupleIndex { object, index, .. } => {
compile_expr(fc, object)?;
fc.emit(Op::GetTupleField(*index as u8));
}
Expr::ArrayIndex {
object,
index,
span,
} => {
if let Some(chain) = data_indexed_chain(object, index) {
emit_data_indexed_read(fc, chain, *span)?;
return Ok(());
}
compile_expr(fc, object)?;
compile_expr(fc, index)?;
fc.emit(Op::GetIndex);
}
Expr::StructInit { name, fields, .. } => {
let field_names: Vec<String> = fields.iter().map(|f| f.name.clone()).collect();
let template_idx = fc.add_struct_template(name, field_names);
for field in fields {
compile_expr(fc, &field.value)?;
}
fc.emit(Op::NewStruct(template_idx));
}
Expr::EnumVariant {
enum_name,
variant,
args,
..
} => {
for arg in args {
compile_expr(fc, arg)?;
}
let enum_const = fc.add_string_constant(enum_name);
let var_const = fc.add_string_constant(variant);
fc.emit(Op::NewEnum(enum_const, var_const, args.len() as u8));
}
Expr::ArrayLiteral { elements, .. } => {
for elem in elements {
compile_expr(fc, elem)?;
}
fc.emit(Op::NewArray(elements.len() as u16));
}
Expr::TupleLiteral { elements, .. } => {
for elem in elements {
compile_expr(fc, elem)?;
}
fc.emit(Op::NewTuple(elements.len() as u8));
}
Expr::Cast {
expr: inner,
target,
..
} => {
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, .. } | Expr::Declassify { value, .. } => {
compile_expr(fc, value)?;
}
}
Ok(())
}
fn compile_checked(
fc: &mut FuncCompiler,
op_expr: &Expr,
arms: &[crate::ast::CheckedArm],
span: &Span,
) -> Result<(), CompileError> {
use crate::ast::{CheckedArmKind, Pattern};
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);
}
Expr::BinOp {
op: BinOp::Div,
left,
right,
..
} => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CheckedDiv);
}
Expr::BinOp {
op: BinOp::Mod,
left,
right,
..
} => {
compile_expr(fc, left)?;
compile_expr(fc, right)?;
fc.emit(Op::CheckedMod);
}
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 `+`, `-`, `*`, `/`, `%`, and unary `-` on Word operands",
),
span: *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, l) => (1_i64, None, Some(h), Some(l)),
CheckedArmKind::Underflow(h, l) => (2_i64, None, Some(h), Some(l)),
};
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 word_ty = TypeExpr::Prim(PrimType::Word, *span);
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, &word_ty)?;
}
if let (Some(h), Some(l)) = (h_pattern, l_pattern) {
bind_var(fc, h, high_slot, &word_ty)?;
bind_var(fc, l, low_slot, &word_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);
}
}
let msg = fc.add_string_constant("no matching arm in checked-overflow construct");
fc.emit(Op::Trap(msg));
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) => {
compile_expr(fc, &args[0])?;
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) {
compile_expr(fc, &args[0])?;
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);
let msg = fc.add_string_constant(&alloc::format!(
"refinement check `{}` failed for newtype `{}`",
pred_name,
name
));
fc.emit(Op::Trap(msg));
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 compile_pattern_test(
fc: &mut FuncCompiler,
pattern: &Pattern,
value_slot: u16,
) -> 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));
}
#[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, _) => {
if enum_name == "Option" && variant == "None" {
fc.emit(Op::GetLocal(value_slot));
fc.emit(Op::PushImmediate(3));
fc.emit(Op::CmpEq);
fail_addrs.push(fc.emit_jump(Op::If(0)));
return Ok(fail_addrs);
}
fc.emit(Op::GetLocal(value_slot));
let e_const = fc.add_string_constant(enum_name);
let v_const = fc.add_string_constant(variant);
fc.emit(Op::IsEnum(e_const, v_const));
fail_addrs.push(fc.emit_jump(Op::If(0)));
fc.emit(Op::PopN(1));
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));
fc.emit(Op::GetEnumField(i as u8));
fc.emit(Op::SetLocal(temp));
let sub_fails = compile_pattern_test(fc, sub_pat, temp)?;
fail_addrs.extend(sub_fails);
}
}
Pattern::Struct(type_name, field_pats, _) => {
fc.emit(Op::GetLocal(value_slot));
let t_const = fc.add_string_constant(type_name);
fc.emit(Op::IsStruct(t_const));
fail_addrs.push(fc.emit_jump(Op::If(0)));
fc.emit(Op::PopN(1));
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 name_const = fc.add_string_constant(&field_pat.name);
fc.emit(Op::GetField(name_const));
fc.emit(Op::SetLocal(temp));
let sub_fails = compile_pattern_test(fc, pat, temp)?;
fail_addrs.extend(sub_fails);
}
}
}
Pattern::Tuple(pats, _) => {
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(i as u8));
fc.emit(Op::SetLocal(temp));
let sub_fails = compile_pattern_test(fc, pat, temp)?;
fail_addrs.extend(sub_fails);
}
}
}
Ok(fail_addrs)
}
fn compile_pattern_bind(
fc: &mut FuncCompiler,
pattern: &Pattern,
value_slot: u16,
) -> Result<(), CompileError> {
compile_pattern_bind_typed(fc, pattern, value_slot, None)
}
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 payload_types: Vec<Option<TypeExpr>> = 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));
fc.emit(Op::GetEnumField(i as u8));
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 {
let name_const = fc.add_string_constant(&field_pat.name);
fc.emit(Op::GetLocal(value_slot));
fc.emit(Op::GetField(name_const));
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(i as u8));
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)
}
#[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::NewEnum(_, _, 0)))
);
}
#[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::NewStruct(_)))
);
}
#[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_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::NewTuple(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(),
}
}
#[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());
}
}