use super::BorrowMode;
use crate::bytecode::{BuiltinFunction, Constant, Instruction, OpCode, Operand};
use crate::type_tracking::{NumericType, StorageHint, TypeTracker, VariableTypeInfo};
use shape_ast::ast::{Spanned, TypeAnnotation};
use shape_ast::error::{Result, ShapeError};
use std::collections::{BTreeSet, HashMap};
use std::sync::OnceLock;
use super::{BuiltinNameResolution, BytecodeCompiler, DropKind, ParamPassMode, ResolutionScope};
pub(super) fn ownership_moves_enabled() -> bool {
#[cfg(test)]
{
if let Some(v) = TEST_OWNERSHIP_MOVES_OVERRIDE.with(|cell| cell.get()) {
return v;
}
}
static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| match std::env::var("SHAPE_V2_OWNERSHIP_MOVES") {
Ok(v) => !matches!(
v.trim(),
"0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
),
Err(_) => true,
})
}
#[cfg(test)]
thread_local! {
pub(super) static TEST_OWNERSHIP_MOVES_OVERRIDE: std::cell::Cell<Option<bool>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn with_ownership_moves_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
struct Guard(Option<bool>);
impl Drop for Guard {
fn drop(&mut self) {
TEST_OWNERSHIP_MOVES_OVERRIDE.with(|cell| cell.set(self.0));
}
}
let prev = TEST_OWNERSHIP_MOVES_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
let _guard = Guard(prev);
f()
}
pub(super) fn promote_to_shared_enabled() -> bool {
#[cfg(test)]
{
if let Some(v) = TEST_PROMOTE_TO_SHARED_OVERRIDE.with(|cell| cell.get()) {
return v;
}
}
static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| match std::env::var("SHAPE_V2_PROMOTE_TO_SHARED") {
Ok(v) => !matches!(
v.trim(),
"0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
),
Err(_) => true,
})
}
#[cfg(test)]
thread_local! {
pub(super) static TEST_PROMOTE_TO_SHARED_OVERRIDE: std::cell::Cell<Option<bool>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn with_promote_to_shared_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
struct Guard(Option<bool>);
impl Drop for Guard {
fn drop(&mut self) {
TEST_PROMOTE_TO_SHARED_OVERRIDE.with(|cell| cell.set(self.0));
}
}
let prev = TEST_PROMOTE_TO_SHARED_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
let _guard = Guard(prev);
f()
}
pub(super) fn box_by_default_enabled() -> bool {
#[cfg(test)]
{
if let Some(v) = TEST_BOX_BY_DEFAULT_OVERRIDE.with(|cell| cell.get()) {
return v;
}
}
static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| match std::env::var("SHAPE_V2_BOX_BY_DEFAULT") {
Ok(v) => !matches!(
v.trim(),
"0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
),
Err(_) => true,
})
}
#[cfg(test)]
thread_local! {
pub(super) static TEST_BOX_BY_DEFAULT_OVERRIDE: std::cell::Cell<Option<bool>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn with_box_by_default_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
struct Guard(Option<bool>);
impl Drop for Guard {
fn drop(&mut self) {
TEST_BOX_BY_DEFAULT_OVERRIDE.with(|cell| cell.set(self.0));
}
}
let prev = TEST_BOX_BY_DEFAULT_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
let _guard = Guard(prev);
f()
}
pub(super) fn typed_bitwise_enabled() -> bool {
#[cfg(test)]
{
if let Some(v) = TEST_TYPED_BITWISE_OVERRIDE.with(|cell| cell.get()) {
return v;
}
}
static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| match std::env::var("SHAPE_V2_TYPED_BITWISE") {
Ok(v) => !matches!(
v.trim(),
"0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
),
Err(_) => true,
})
}
#[cfg(test)]
thread_local! {
pub(super) static TEST_TYPED_BITWISE_OVERRIDE: std::cell::Cell<Option<bool>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn with_typed_bitwise_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
struct Guard(Option<bool>);
impl Drop for Guard {
fn drop(&mut self) {
TEST_TYPED_BITWISE_OVERRIDE.with(|cell| cell.set(self.0));
}
}
let prev = TEST_TYPED_BITWISE_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
let _guard = Guard(prev);
f()
}
pub(super) fn typed_string_coerce_concat_enabled() -> bool {
#[cfg(test)]
{
if let Some(v) = TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE.with(|cell| cell.get()) {
return v;
}
}
static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| match std::env::var("SHAPE_V2_STRING_COERCE_CONCAT") {
Ok(v) => !matches!(
v.trim(),
"0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
),
Err(_) => true,
})
}
#[cfg(test)]
thread_local! {
pub(super) static TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE: std::cell::Cell<Option<bool>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn with_typed_string_coerce_concat_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
struct Guard(Option<bool>);
impl Drop for Guard {
fn drop(&mut self) {
TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE.with(|cell| cell.set(self.0));
}
}
let prev = TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
let _guard = Guard(prev);
f()
}
pub(crate) fn infer_top_level_concrete_types_from_mir(
mir: &crate::mir::MirFunction,
) -> Vec<shape_value::v2::ConcreteType> {
infer_top_level_concrete_types_from_mir_with_returns(mir, None)
}
pub(crate) fn infer_top_level_concrete_types_from_mir_with_returns(
mir: &crate::mir::MirFunction,
callee_returns: Option<&dyn Fn(&str) -> Option<shape_value::v2::ConcreteType>>,
) -> Vec<shape_value::v2::ConcreteType> {
infer_top_level_concrete_types_from_mir_with_resolvers(
mir,
callee_returns,
None,
None,
None,
)
}
pub(crate) fn infer_top_level_concrete_types_from_mir_with_resolvers(
mir: &crate::mir::MirFunction,
callee_returns: Option<&dyn Fn(&str) -> Option<shape_value::v2::ConcreteType>>,
method_returns: Option<
&dyn Fn(&str, &str) -> Option<shape_value::v2::ConcreteType>,
>,
monomorph_method_returns: Option<
&dyn Fn(shape_ast::ast::span::Span) -> Option<shape_value::v2::ConcreteType>,
>,
value_call_returns: Option<
&dyn Fn(shape_ast::ast::span::Span) -> Option<shape_value::v2::ConcreteType>,
>,
) -> Vec<shape_value::v2::ConcreteType> {
use crate::mir::types::{MirConstant, Operand, StatementKind};
let n = mir.num_locals as usize;
let mut concrete_types: Vec<shape_value::v2::ConcreteType> =
vec![shape_value::v2::ConcreteType::Void; n];
let mut slot_scalar_kind: Vec<Option<shape_value::v2::ConcreteType>> =
vec![None; n];
for block in mir.iter_blocks() {
for stmt in &block.statements {
if let StatementKind::Assign(
crate::mir::types::Place::Local(dst),
crate::mir::types::Rvalue::Use(Operand::Constant(c)),
) = &stmt.kind
{
let idx = dst.0 as usize;
if idx < n {
slot_scalar_kind[idx] = match c {
MirConstant::Int(_) => Some(shape_value::v2::ConcreteType::I64),
MirConstant::Float(_) => Some(shape_value::v2::ConcreteType::F64),
MirConstant::Bool(_) => Some(shape_value::v2::ConcreteType::Bool),
_ => None,
};
}
}
}
}
let mut struct_names: Vec<Option<String>> = vec![None; n];
for (slot, name) in &mir.local_struct_type_names {
let idx = slot.0 as usize;
if idx < n {
struct_names[idx] = Some(name.clone());
}
}
for (slot, elem) in &mir.local_typed_array_element_types {
let idx = slot.0 as usize;
if idx < n
&& matches!(
concrete_types[idx],
shape_value::v2::ConcreteType::Void
)
{
concrete_types[idx] =
shape_value::v2::ConcreteType::Array(Box::new(elem.clone()));
}
}
for (slot, decl_ct) in &mir.local_declared_scalar_types {
let idx = slot.0 as usize;
if idx >= n {
continue;
}
if matches!(
concrete_types[idx],
shape_value::v2::ConcreteType::Void | shape_value::v2::ConcreteType::I64
) {
concrete_types[idx] = decl_ct.clone();
}
slot_scalar_kind[idx] = Some(decl_ct.clone());
}
let mut copy_source: Vec<Option<u16>> = vec![None; n];
for block in mir.iter_blocks() {
for stmt in &block.statements {
if let StatementKind::Assign(
crate::mir::types::Place::Local(dst),
crate::mir::types::Rvalue::Use(
Operand::Move(crate::mir::types::Place::Local(src))
| Operand::Copy(crate::mir::types::Place::Local(src))
| Operand::MoveExplicit(crate::mir::types::Place::Local(src)),
),
) = &stmt.kind
{
let di = dst.0 as usize;
if di < n {
copy_source[di] = Some(src.0);
}
}
}
}
for block in mir.iter_blocks() {
for stmt in &block.statements {
match &stmt.kind {
StatementKind::ObjectStore { container_slot, .. } => {
let idx = container_slot.0 as usize;
if idx < n {
concrete_types[idx] =
shape_value::v2::ConcreteType::placeholder_struct(
shape_value::v2::concrete_type::StructLayoutId(0),
);
}
}
StatementKind::EnumStore {
container_slot,
operands,
variant_name,
} => {
let idx = container_slot.0 as usize;
if idx < n {
let variant_tag = variant_name
.as_deref()
.and_then(crate::mir::types::VariantTag::from_name);
let inner_concrete = if operands.len() == 1 {
operand_concrete_type(&operands[0], &slot_scalar_kind)
} else {
None
};
match (variant_tag, inner_concrete) {
(
Some(crate::mir::types::VariantTag::Ok),
Some(inner),
) => {
concrete_types[idx] = shape_value::v2::ConcreteType::Result(
Box::new(inner),
Box::new(shape_value::v2::ConcreteType::Void),
);
}
(
Some(crate::mir::types::VariantTag::Err),
Some(inner),
) => {
concrete_types[idx] = shape_value::v2::ConcreteType::Result(
Box::new(shape_value::v2::ConcreteType::Void),
Box::new(inner),
);
}
(
Some(crate::mir::types::VariantTag::Some_),
Some(inner),
) => {
concrete_types[idx] = shape_value::v2::ConcreteType::Option(
Box::new(inner),
);
}
(Some(crate::mir::types::VariantTag::None_), _) => {
concrete_types[idx] = shape_value::v2::ConcreteType::Option(
Box::new(shape_value::v2::ConcreteType::Void),
);
}
_ => {
concrete_types[idx] =
shape_value::v2::ConcreteType::placeholder_enum(
shape_value::v2::concrete_type::EnumLayoutId(0),
);
}
}
}
}
StatementKind::ArrayStore {
container_slot,
operands,
} => {
let idx = container_slot.0 as usize;
if idx < n {
if let Some(elem) = infer_array_elem_from_operands(
operands,
&slot_scalar_kind,
&concrete_types,
©_source,
) {
concrete_types[idx] =
shape_value::v2::ConcreteType::Array(Box::new(elem));
}
}
}
_ => {}
}
}
}
if let Some(resolver) = callee_returns {
use crate::mir::types::TerminatorKind;
for block in mir.iter_blocks() {
if let TerminatorKind::Call {
func, destination, ..
} = &block.terminator.kind
{
if let Operand::Constant(MirConstant::Function(name)) = func {
if let crate::mir::types::Place::Local(dst) = destination {
let idx = dst.0 as usize;
if idx < n
&& matches!(
concrete_types[idx],
shape_value::v2::ConcreteType::Void
)
{
if let Some(ct) = resolver(name.as_str()) {
if !matches!(
ct,
shape_value::v2::ConcreteType::Void
) {
concrete_types[idx] = ct;
}
}
}
}
}
}
}
}
if let Some(monomorph_resolver) = monomorph_method_returns {
use crate::mir::types::TerminatorKind;
for block in mir.iter_blocks() {
if let TerminatorKind::Call {
func, destination, ..
} = &block.terminator.kind
{
if let Operand::Constant(MirConstant::Method(_)) = func {
if let crate::mir::types::Place::Local(dst) = destination {
let idx = dst.0 as usize;
if idx < n
&& matches!(
concrete_types[idx],
shape_value::v2::ConcreteType::Void
)
{
let span = block.terminator.span;
if let Some(ct) = monomorph_resolver(span) {
if !matches!(
ct,
shape_value::v2::ConcreteType::Void
) {
concrete_types[idx] = ct;
}
}
}
}
}
}
}
}
if let Some(value_call_resolver) = value_call_returns {
use crate::mir::types::TerminatorKind;
for block in mir.iter_blocks() {
if let TerminatorKind::Call {
func, destination, ..
} = &block.terminator.kind
{
let is_local_callee = matches!(
func,
Operand::Copy(crate::mir::types::Place::Local(_))
| Operand::Move(crate::mir::types::Place::Local(_))
| Operand::MoveExplicit(crate::mir::types::Place::Local(_))
);
if !is_local_callee {
continue;
}
if let crate::mir::types::Place::Local(dst) = destination {
let idx = dst.0 as usize;
if idx < n
&& matches!(
concrete_types[idx],
shape_value::v2::ConcreteType::Void
)
{
let span = block.terminator.span;
if let Some(ct) = value_call_resolver(span) {
if !matches!(
ct,
shape_value::v2::ConcreteType::Void
) {
concrete_types[idx] = ct;
}
}
}
}
}
}
}
use crate::mir::types::{Place, Rvalue};
let mut changed = true;
let mut iter_budget = n.max(1) * 4; while changed && iter_budget > 0 {
changed = false;
iter_budget -= 1;
for block in mir.iter_blocks() {
for stmt in &block.statements {
if let StatementKind::Assign(
Place::Local(dst),
Rvalue::Use(
Operand::Move(Place::Local(src))
| Operand::Copy(Place::Local(src))
| Operand::MoveExplicit(Place::Local(src)),
),
) = &stmt.kind
{
let (di, si) = (dst.0 as usize, src.0 as usize);
if di < n && si < n {
let src_ct = concrete_types[si].clone();
if !matches!(src_ct, shape_value::v2::ConcreteType::Void)
&& concrete_types[di] != src_ct
{
concrete_types[di] = src_ct;
changed = true;
}
if let Some(src_name) = struct_names[si].clone() {
if struct_names[di].as_deref() != Some(src_name.as_str()) {
struct_names[di] = Some(src_name);
changed = true;
}
}
}
}
}
}
}
if let Some(method_resolver) = method_returns {
use crate::mir::types::TerminatorKind;
for block in mir.iter_blocks() {
if let TerminatorKind::Call {
func,
args,
destination,
..
} = &block.terminator.kind
{
if let Operand::Constant(MirConstant::Method(method_name)) = func {
if let crate::mir::types::Place::Local(dst) = destination {
let idx = dst.0 as usize;
if idx < n
&& matches!(
concrete_types[idx],
shape_value::v2::ConcreteType::Void
)
{
let receiver_slot = match args.first() {
Some(Operand::Move(crate::mir::types::Place::Local(s)))
| Some(Operand::Copy(crate::mir::types::Place::Local(s)))
| Some(Operand::MoveExplicit(crate::mir::types::Place::Local(s))) => s.0 as usize,
_ => continue,
};
if receiver_slot >= n {
continue;
}
if let Some(type_name) = struct_names[receiver_slot].clone() {
if let Some(ct) =
method_resolver(&type_name, method_name.as_str())
{
if !matches!(
ct,
shape_value::v2::ConcreteType::Void
) {
concrete_types[idx] = ct;
continue;
}
}
}
let receiver_ct = &concrete_types[receiver_slot];
let inferred = match (
method_name.as_str(),
receiver_ct,
) {
(
"sum" | "mean" | "min" | "max",
shape_value::v2::ConcreteType::Array(elem),
) => Some((**elem).clone()),
(
"get",
shape_value::v2::ConcreteType::Array(elem),
) => Some((**elem).clone()),
_ => None,
};
if let Some(ct) = inferred {
if !matches!(ct, shape_value::v2::ConcreteType::Void) {
concrete_types[idx] = ct;
}
}
}
}
}
}
}
}
concrete_types
}
fn operand_concrete_type(
operand: &crate::mir::types::Operand,
slot_scalar_kind: &[Option<shape_value::v2::ConcreteType>],
) -> Option<shape_value::v2::ConcreteType> {
use crate::mir::types::{MirConstant, Operand, Place};
match operand {
Operand::Constant(MirConstant::Int(_)) => Some(shape_value::v2::ConcreteType::I64),
Operand::Constant(MirConstant::Float(_)) => Some(shape_value::v2::ConcreteType::F64),
Operand::Constant(MirConstant::Bool(_)) => Some(shape_value::v2::ConcreteType::Bool),
Operand::Constant(MirConstant::Str(_))
| Operand::Constant(MirConstant::StringId(_)) => {
Some(shape_value::v2::ConcreteType::String)
}
Operand::Move(Place::Local(s))
| Operand::Copy(Place::Local(s))
| Operand::MoveExplicit(Place::Local(s)) => {
let idx = s.0 as usize;
slot_scalar_kind.get(idx).and_then(|k| k.clone())
}
_ => None,
}
}
fn resolve_copy_chain(
slot: u16,
copy_source: &[Option<u16>],
concrete_types: &[shape_value::v2::ConcreteType],
) -> Option<shape_value::v2::ConcreteType> {
let mut cur = slot as usize;
let mut budget = copy_source.len();
loop {
if cur >= concrete_types.len() {
return None;
}
let ct = &concrete_types[cur];
if !matches!(ct, shape_value::v2::ConcreteType::Void) {
return Some(ct.clone());
}
let next = copy_source.get(cur).copied().flatten();
match next {
Some(n) if (n as usize) != cur && budget > 0 => {
cur = n as usize;
budget -= 1;
}
_ => return None,
}
}
}
fn infer_array_elem_from_operands(
operands: &[crate::mir::types::Operand],
slot_scalar_kind: &[Option<shape_value::v2::ConcreteType>],
concrete_types: &[shape_value::v2::ConcreteType],
copy_source: &[Option<u16>],
) -> Option<shape_value::v2::ConcreteType> {
use crate::mir::types::{MirConstant, Operand, Place};
if operands.is_empty() {
return None;
}
let mut elem: Option<shape_value::v2::ConcreteType> = None;
for op in operands {
let here = match op {
Operand::Constant(MirConstant::Int(_)) => {
Some(shape_value::v2::ConcreteType::I64)
}
Operand::Constant(MirConstant::Float(_)) => {
Some(shape_value::v2::ConcreteType::F64)
}
Operand::Constant(MirConstant::Bool(_)) => {
Some(shape_value::v2::ConcreteType::Bool)
}
Operand::Move(Place::Local(s))
| Operand::Copy(Place::Local(s))
| Operand::MoveExplicit(Place::Local(s)) => {
let idx = s.0 as usize;
let scalar_kind = slot_scalar_kind.get(idx).and_then(|k| k.clone());
if scalar_kind.is_some() {
scalar_kind
} else if let Some(shape_value::v2::ConcreteType::Array(inner)) =
resolve_copy_chain(s.0, copy_source, concrete_types).as_ref()
{
Some((**inner).clone())
} else {
None
}
}
_ => return None,
};
match (&elem, here) {
(None, Some(k)) => elem = Some(k),
(Some(a), Some(ref b)) if a == b => {}
_ => return None, }
}
elem
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::compiler) enum BinOperandKind {
Numeric(NumericType),
String,
Bool,
Unknown,
}
impl BinOperandKind {
pub(in crate::compiler) fn from_numeric(nt: Option<NumericType>) -> Self {
match nt {
Some(n) => BinOperandKind::Numeric(n),
None => BinOperandKind::Unknown,
}
}
}
fn typed_numeric_opcode(op: shape_ast::ast::BinaryOp, nt: NumericType) -> Option<OpCode> {
use shape_ast::ast::BinaryOp;
let matched = match (op, nt) {
(BinaryOp::Add, NumericType::Int) => OpCode::AddInt,
(BinaryOp::Sub, NumericType::Int) => OpCode::SubInt,
(BinaryOp::Mul, NumericType::Int) => OpCode::MulInt,
(BinaryOp::Div, NumericType::Int) => OpCode::DivInt,
(BinaryOp::Mod, NumericType::Int) => OpCode::ModInt,
(BinaryOp::Pow, NumericType::Int) => OpCode::PowInt,
(BinaryOp::Add, NumericType::Number) => OpCode::AddNumber,
(BinaryOp::Sub, NumericType::Number) => OpCode::SubNumber,
(BinaryOp::Mul, NumericType::Number) => OpCode::MulNumber,
(BinaryOp::Div, NumericType::Number) => OpCode::DivNumber,
(BinaryOp::Mod, NumericType::Number) => OpCode::ModNumber,
(BinaryOp::Pow, NumericType::Number) => OpCode::PowNumber,
(BinaryOp::Add, NumericType::Decimal) => OpCode::AddDecimal,
(BinaryOp::Sub, NumericType::Decimal) => OpCode::SubDecimal,
(BinaryOp::Mul, NumericType::Decimal) => OpCode::MulDecimal,
(BinaryOp::Div, NumericType::Decimal) => OpCode::DivDecimal,
(BinaryOp::Greater, NumericType::Int) => OpCode::GtInt,
(BinaryOp::Less, NumericType::Int) => OpCode::LtInt,
(BinaryOp::GreaterEq, NumericType::Int) => OpCode::GteInt,
(BinaryOp::LessEq, NumericType::Int) => OpCode::LteInt,
(BinaryOp::Equal, NumericType::Int) => OpCode::EqInt,
(BinaryOp::NotEqual, NumericType::Int) => OpCode::NeqInt,
(BinaryOp::Greater, NumericType::Number) => OpCode::GtNumber,
(BinaryOp::Less, NumericType::Number) => OpCode::LtNumber,
(BinaryOp::GreaterEq, NumericType::Number) => OpCode::GteNumber,
(BinaryOp::LessEq, NumericType::Number) => OpCode::LteNumber,
(BinaryOp::Equal, NumericType::Number) => OpCode::EqNumber,
(BinaryOp::NotEqual, NumericType::Number) => OpCode::NeqNumber,
(BinaryOp::Equal, NumericType::Decimal) => OpCode::EqDecimal,
_ => return None,
};
Some(matched)
}
fn is_arith_or_cmp_op(op: shape_ast::ast::BinaryOp) -> bool {
use shape_ast::ast::BinaryOp;
matches!(
op,
BinaryOp::Add
| BinaryOp::Sub
| BinaryOp::Mul
| BinaryOp::Div
| BinaryOp::Mod
| BinaryOp::Pow
| BinaryOp::Greater
| BinaryOp::Less
| BinaryOp::GreaterEq
| BinaryOp::LessEq
| BinaryOp::Equal
| BinaryOp::NotEqual
)
}
pub(in crate::compiler) fn emit_binary_op(
compiler: &mut BytecodeCompiler,
op: shape_ast::ast::BinaryOp,
lhs: BinOperandKind,
rhs: BinOperandKind,
) -> Result<bool> {
use shape_ast::ast::BinaryOp;
if !is_arith_or_cmp_op(op) {
return Ok(false);
}
if let (BinOperandKind::Numeric(lnt), BinOperandKind::Numeric(rnt)) = (lhs, rhs) {
if lnt == rnt {
if let Some(typed) = typed_numeric_opcode(op, lnt) {
compiler.emit(Instruction::simple(typed));
compiler.last_expr_schema = None;
compiler.last_expr_type_info = None;
compiler.last_expr_numeric_type = match op {
BinaryOp::Add
| BinaryOp::Sub
| BinaryOp::Mul
| BinaryOp::Div
| BinaryOp::Mod
| BinaryOp::Pow => Some(lnt),
_ => None,
};
return Ok(true);
}
}
}
if matches!(
(lhs, rhs, op),
(
BinOperandKind::String,
BinOperandKind::String,
BinaryOp::Add
)
) {
compiler.emit(Instruction::simple(OpCode::StringConcatTyped));
compiler.last_expr_schema = None;
compiler.last_expr_type_info = None;
compiler.last_expr_numeric_type = None;
return Ok(true);
}
Ok(false)
}
pub(crate) fn strip_error_prefix(e: &ShapeError) -> String {
let msg = e.to_string();
const PREFIXES: &[&str] = &[
"Runtime error: ",
"Type error: ",
"Semantic error: ",
"Parse error: ",
"VM error: ",
"Lexical error: ",
];
let mut s = msg.as_str();
for _ in 0..3 {
let mut stripped = false;
for prefix in PREFIXES {
if let Some(rest) = s.strip_prefix(prefix) {
s = rest;
stripped = true;
break;
}
}
const COMPTIME_PREFIXES: &[&str] = &[
"Comptime block evaluation failed: ",
"Comptime handler execution failed: ",
"Comptime block directive processing failed: ",
];
for prefix in COMPTIME_PREFIXES {
if let Some(rest) = s.strip_prefix(prefix) {
s = rest;
stripped = true;
break;
}
}
if !stripped {
break;
}
}
s.to_string()
}
impl BytecodeCompiler {
pub(crate) fn resolve_type_tag(
numeric_type: Option<crate::type_tracking::NumericType>,
type_info: &Option<crate::type_tracking::VariableTypeInfo>,
) -> u8 {
use crate::type_tracking::NumericType;
if let Some(nt) = numeric_type {
return match nt {
NumericType::Number => 0, NumericType::Int => 1, NumericType::IntWidth(_) => 1, NumericType::Decimal => 22, };
}
if let Some(info) = type_info {
if let Some(ref name) = info.type_name {
return match name.as_str() {
"number" | "Number" => 0, "int" | "Int" => 1, "bool" | "Bool" => 9, "string" | "String" => 10, "DateTime" => 25, _ => {
if name.starts_with("Array") || name.starts_with("Vec") {
12 } else if name.starts_with("HashMap") || name.starts_with("Map") {
13 } else if name.starts_with("Set") {
14 } else {
0xFF
}
}
};
}
use crate::type_tracking::VariableKind;
match &info.kind {
VariableKind::Table { .. } => return 11, _ => {}
}
}
0xFF }
fn scalar_type_name_from_numeric(numeric_type: NumericType) -> &'static str {
match numeric_type {
NumericType::Int | NumericType::IntWidth(_) => "int",
NumericType::Number => "number",
NumericType::Decimal => "decimal",
}
}
fn array_type_name_from_numeric(numeric_type: NumericType) -> &'static str {
match numeric_type {
NumericType::Int | NumericType::IntWidth(_) => "Vec<int>",
NumericType::Number => "Vec<number>",
NumericType::Decimal => "Vec<decimal>",
}
}
fn is_array_type_name(type_name: Option<&str>) -> bool {
matches!(type_name, Some(name) if name.starts_with("Vec<") && name.ends_with('>'))
}
fn is_temporal_type_name(type_name: Option<&str>) -> bool {
matches!(
type_name,
Some("DateTime") | Some("Duration") | Some("TimeSpan")
)
}
pub(super) fn tracker_type_name_is_primitive(name: &str) -> bool {
matches!(
name,
"int" | "i8" | "i16" | "i32" | "i64"
| "u8" | "u16" | "u32" | "u64"
| "number" | "f32" | "f64"
| "bool" | "string" | "decimal" | "bigint"
| "DateTime" | "Duration" | "TimeSpan"
)
}
pub(super) fn tracked_type_name_from_annotation(type_ann: &TypeAnnotation) -> Option<String> {
match type_ann {
TypeAnnotation::Basic(name) => Some(name.clone()),
TypeAnnotation::Reference(name) => Some(name.to_string()),
TypeAnnotation::Array(inner) => Some(format!("Vec<{}>", inner.to_type_string())),
TypeAnnotation::Generic { name, args } if name == "Vec" && args.len() == 1 => {
Some(format!("Vec<{}>", args[0].to_type_string()))
}
TypeAnnotation::Generic { name, args } if name == "Mat" && args.len() == 1 => {
Some(format!("Mat<{}>", args[0].to_type_string()))
}
TypeAnnotation::Generic { name, .. } if name == "Option" || name == "Result" => {
Some(name.to_lowercase())
}
_ => None,
}
}
pub(super) fn resolve_type_name(&self, name: &str) -> String {
if name.contains("::") || self.is_type_known_direct(name) {
return name.to_string();
}
for scope in self.module_scope_stack.iter().rev() {
let qualified = format!("{}::{}", scope, name);
if self.is_type_known_direct(&qualified) {
return qualified;
}
}
if let Some(imported) = self.imported_names.get(name) {
if !imported.module_path.is_empty() {
let qualified = format!("{}::{}", imported.module_path, imported.original_name);
if self.is_type_known_direct(&qualified) {
return qualified;
}
}
if self.is_type_known_direct(&imported.original_name) {
return imported.original_name.clone();
}
}
for ns in &self.module_namespace_bindings {
let qualified = format!("{}::{}", ns, name);
if self.is_type_known_direct(&qualified) {
return qualified;
}
if let Some(canonical) = self.graph_namespace_map.get(ns) {
let cq = format!("{}::{}", canonical, name);
if self.is_type_known_direct(&cq) {
return cq;
}
}
}
name.to_string()
}
fn is_type_known_direct(&self, name: &str) -> bool {
self.struct_types.contains_key(name)
|| self.type_aliases.contains_key(name)
|| self.type_inference.env.lookup_type_alias(name).is_some()
|| self.type_inference.env.get_enum(name).is_some()
|| self.type_inference.env.lookup_trait(name).is_some()
|| self.type_tracker.schema_registry().get(name).is_some()
}
pub(super) fn resolve_trait_name(&self, name: &str) -> (String, String) {
let basename = name.rsplit("::").next().unwrap_or(name).to_string();
if self.trait_defs.contains_key(name) {
return (name.to_string(), basename);
}
for scope in self.module_scope_stack.iter().rev() {
let q = format!("{}::{}", scope, name);
if self.trait_defs.contains_key(&q) {
return (q, basename);
}
}
if let Some(imported) = self.imported_names.get(name) {
if !imported.module_path.is_empty() {
let q = format!("{}::{}", imported.module_path, imported.original_name);
if self.trait_defs.contains_key(&q) {
return (q, basename);
}
}
}
for ns in &self.module_namespace_bindings {
let q = format!("{}::{}", ns, name);
if self.trait_defs.contains_key(&q) {
return (q, basename);
}
if let Some(canonical) = self.graph_namespace_map.get(ns) {
let cq = format!("{}::{}", canonical, name);
if self.trait_defs.contains_key(&cq) {
return (cq, basename);
}
}
}
if self.type_inference.env.lookup_trait(name).is_some() {
return (name.to_string(), basename);
}
if self.type_inference.env.lookup_trait(&basename).is_some() {
return (basename.clone(), basename);
}
(name.to_string(), basename)
}
pub(super) fn mark_slot_as_numeric_array(
&mut self,
slot: u16,
is_local: bool,
numeric_type: NumericType,
) {
let info =
VariableTypeInfo::named(Self::array_type_name_from_numeric(numeric_type).to_string());
if is_local {
self.type_tracker.set_local_type(slot, info);
} else {
self.type_tracker.set_binding_type(slot, info);
}
}
pub(super) fn mark_slot_as_numeric_scalar(
&mut self,
slot: u16,
is_local: bool,
numeric_type: NumericType,
) {
let info =
VariableTypeInfo::named(Self::scalar_type_name_from_numeric(numeric_type).to_string());
if is_local {
self.type_tracker.set_local_type(slot, info);
} else {
self.type_tracker.set_binding_type(slot, info);
}
}
pub(super) fn seed_numeric_hint_from_expr(
&mut self,
expr: &shape_ast::ast::Expr,
numeric_type: NumericType,
) {
match expr {
shape_ast::ast::Expr::Identifier(name, _) => {
if let Some(local_idx) = self.resolve_local(name) {
self.mark_slot_as_numeric_scalar(local_idx, true, numeric_type);
return;
}
let scoped_name = self
.resolve_scoped_module_binding_name(name)
.unwrap_or_else(|| name.to_string());
if let Some(binding_idx) = self.module_bindings.get(&scoped_name).copied() {
self.mark_slot_as_numeric_scalar(binding_idx, false, numeric_type);
}
}
shape_ast::ast::Expr::IndexAccess {
object,
end_index: None,
..
} => {
if let shape_ast::ast::Expr::Identifier(name, _) = object.as_ref() {
if let Some(local_idx) = self.resolve_local(name) {
self.mark_slot_as_numeric_array(local_idx, true, numeric_type);
return;
}
let scoped_name = self
.resolve_scoped_module_binding_name(name)
.unwrap_or_else(|| name.to_string());
if let Some(binding_idx) = self.module_bindings.get(&scoped_name).copied() {
self.mark_slot_as_numeric_array(binding_idx, false, numeric_type);
}
}
}
_ => {}
}
}
fn recover_or_bail_with_null_placeholder(&mut self, err: ShapeError) -> Result<()> {
if self.should_recover_compile_diagnostics() {
self.errors.push(err);
self.emit(Instruction::simple(OpCode::PushNull));
Ok(())
} else {
Err(err)
}
}
pub(super) fn compile_expr_as_value_or_placeholder(
&mut self,
expr: &shape_ast::ast::Expr,
) -> Result<()> {
match self.compile_expr(expr) {
Ok(()) => Ok(()),
Err(err) => self.recover_or_bail_with_null_placeholder(err),
}
}
pub(super) fn emit(&mut self, instruction: Instruction) -> usize {
let idx = self.program.emit(instruction);
if self.current_line > 0 {
self.program.debug_info.line_numbers.push((
idx,
self.current_file_id,
self.current_line,
));
}
idx
}
pub(super) fn emit_bool(&mut self, value: bool) {
let const_idx = self.program.add_constant(Constant::Bool(value));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(const_idx)),
));
}
pub(super) fn emit_unit(&mut self) {
let const_idx = self.program.add_constant(Constant::Unit);
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(const_idx)),
));
}
pub(super) fn emit_jump(&mut self, mut opcode: OpCode, dummy: i32) -> usize {
if opcode == OpCode::JumpIfFalse && self.last_instruction_produces_bool() {
opcode = OpCode::JumpIfFalseTrusted;
}
self.emit(Instruction::new(opcode, Some(Operand::Offset(dummy))))
}
fn last_instruction_produces_bool(&self) -> bool {
self.program
.instructions
.last()
.map(|instr| {
matches!(
instr.opcode,
OpCode::GtInt
| OpCode::GtNumber
| OpCode::GtDecimal
| OpCode::LtInt
| OpCode::LtNumber
| OpCode::LtDecimal
| OpCode::GteInt
| OpCode::GteNumber
| OpCode::GteDecimal
| OpCode::LteInt
| OpCode::LteNumber
| OpCode::LteDecimal
| OpCode::EqInt
| OpCode::EqNumber
| OpCode::NeqInt
| OpCode::NeqNumber
| OpCode::EqString
| OpCode::EqDecimal
| OpCode::IsNull
| OpCode::Not
)
})
.unwrap_or(false)
}
pub(super) fn last_emitted_native_kind(&self) -> Option<StorageHint> {
let instrs = &self.program.instructions;
let mut idx = instrs.len();
while idx > 0 {
let prev = &instrs[idx - 1];
match prev.opcode {
OpCode::DropCall
| OpCode::DropCallAsync
| OpCode::DropLocal
| OpCode::DropSharedLocal => {
idx -= 1;
if idx > 0
&& matches!(
instrs[idx - 1].opcode,
OpCode::LoadLocal
| OpCode::LoadLocalTrusted
| OpCode::LoadModuleBinding
)
{
idx -= 1;
}
}
OpCode::ReturnOwned => {
idx -= 1;
}
_ => break,
}
}
if idx == 0 {
return None;
}
let instr = &instrs[idx - 1];
match instr.opcode {
OpCode::AddInt
| OpCode::SubInt
| OpCode::MulInt
| OpCode::DivInt
| OpCode::ModInt
| OpCode::PowInt
| OpCode::NegInt
| OpCode::BitAndInt
| OpCode::BitOrInt
| OpCode::BitXorInt
| OpCode::BitShlInt
| OpCode::BitShrInt
| OpCode::BitNotInt
| OpCode::BitAnd
| OpCode::BitOr
| OpCode::BitXor
| OpCode::BitShl
| OpCode::BitShr
| OpCode::BitNot
| OpCode::LoadLocalI64
| OpCode::LoadLocalU64
| OpCode::LoadLocalI32
| OpCode::LoadLocalU32
| OpCode::LoadLocalI16
| OpCode::LoadLocalU16
| OpCode::LoadLocalI8
| OpCode::LoadLocalU8
| OpCode::LoadModuleBindingI64
| OpCode::LoadModuleBindingU64
| OpCode::LoadModuleBindingI32
| OpCode::LoadModuleBindingU32
| OpCode::LoadModuleBindingI16
| OpCode::LoadModuleBindingU16
| OpCode::LoadModuleBindingI8
| OpCode::LoadModuleBindingU8
| OpCode::ArrayLenTyped
| OpCode::MapLenTyped
| OpCode::StringLenTyped
| OpCode::AddI32
| OpCode::SubI32
| OpCode::MulI32
| OpCode::DivI32
| OpCode::ModI32
| OpCode::AddTyped
| OpCode::SubTyped
| OpCode::MulTyped
| OpCode::DivTyped
| OpCode::ModTyped
| OpCode::CmpTyped
| OpCode::CastWidth => Some(StorageHint::Int64),
OpCode::AddNumber
| OpCode::SubNumber
| OpCode::MulNumber
| OpCode::DivNumber
| OpCode::ModNumber
| OpCode::PowNumber
| OpCode::NegNumber
| OpCode::LoadLocalF64
| OpCode::LoadModuleBindingF64 => Some(StorageHint::Float64),
OpCode::EqInt
| OpCode::NeqInt
| OpCode::LtInt
| OpCode::LteInt
| OpCode::GtInt
| OpCode::GteInt
| OpCode::EqNumber
| OpCode::NeqNumber
| OpCode::LtNumber
| OpCode::LteNumber
| OpCode::GtNumber
| OpCode::GteNumber
| OpCode::EqString
| OpCode::EqDecimal
| OpCode::LtDecimal
| OpCode::LteDecimal
| OpCode::GtDecimal
| OpCode::GteDecimal
| OpCode::IsNull
| OpCode::Not
| OpCode::LoadLocalBool
| OpCode::LoadModuleBindingBool
| OpCode::EqI32
| OpCode::NeqI32
| OpCode::LtI32
| OpCode::LteI32
| OpCode::GtI32
| OpCode::GteI32 => Some(StorageHint::Bool),
OpCode::PushConst => self.push_const_native_kind(instr),
OpCode::LoadLocalTrusted => self.load_local_trusted_native_kind(instr),
OpCode::LoadModuleBinding => self.load_module_binding_native_kind(instr),
OpCode::GetFieldTyped => self.get_field_typed_native_kind(instr),
OpCode::Call => self.call_native_kind(instr),
OpCode::LoadSharedModuleBinding => self.load_shared_module_binding_native_kind(instr),
OpCode::DerefLoad => self.deref_load_native_kind(),
OpCode::GetProp => self.get_prop_native_kind_at(idx - 1),
_ => None,
}
}
fn call_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
let Some(Operand::Function(fid)) = &instr.operand else {
return None;
};
let callee_idx = fid.0 as usize;
let func = self.program.functions.get(callee_idx)?;
let return_ann = self
.function_defs
.get(&func.name)
.and_then(|def| def.return_type.as_ref())
.or_else(|| {
self.foreign_function_defs
.get(&func.name)
.and_then(|def| def.return_type.as_ref())
});
if let Some(ann) = return_ann {
if let Some(name) = ann.as_type_name_str() {
if let Some(kind) = primitive_type_name_to_storage_hint(name).or_else(|| {
self.type_aliases
.get(name)
.and_then(|aliased| primitive_type_name_to_storage_hint(aliased.as_str()))
}) {
if matches!(
kind,
StorageHint::Int64
| StorageHint::UInt64
| StorageHint::Int32
| StorageHint::UInt32
| StorageHint::Int16
| StorageHint::UInt16
| StorageHint::Int8
| StorageHint::UInt8
| StorageHint::Float64
| StorageHint::Bool
) {
return Some(kind);
}
}
}
}
let entry = func.entry_point;
let end = entry.checked_add(func.body_length)?;
if end > self.program.instructions.len() {
return None;
}
let mut nested_ranges: Vec<(usize, usize)> = Vec::new();
for (other_idx, other) in self.program.functions.iter().enumerate() {
if other_idx == callee_idx {
continue;
}
let o_entry = other.entry_point;
let Some(o_end) = o_entry.checked_add(other.body_length) else {
continue;
};
if o_entry >= entry && o_end <= end && o_entry > entry {
nested_ranges.push((o_entry, o_end));
}
}
nested_ranges.sort_unstable_by_key(|&(s, _)| s);
let mut found: Option<StorageHint> = None;
let mut pos = entry;
let mut nested_cursor = 0;
while pos < end {
while nested_cursor < nested_ranges.len()
&& nested_ranges[nested_cursor].1 <= pos
{
nested_cursor += 1;
}
if nested_cursor < nested_ranges.len() {
let (n_start, n_end) = nested_ranges[nested_cursor];
if pos >= n_start && pos < n_end {
pos = n_end;
continue;
}
}
let instr = &self.program.instructions[pos];
let kind = match instr.opcode {
OpCode::ReturnValueI64
| OpCode::ReturnValueU64
| OpCode::ReturnValueI32
| OpCode::ReturnValueU32
| OpCode::ReturnValueI16
| OpCode::ReturnValueU16
| OpCode::ReturnValueI8
| OpCode::ReturnValueU8 => Some(StorageHint::Int64),
OpCode::ReturnValueF64 => Some(StorageHint::Float64),
OpCode::ReturnValueBool => Some(StorageHint::Bool),
OpCode::ReturnValue | OpCode::ReturnValuePtr => return None,
_ => {
pos += 1;
continue;
}
};
match (found, kind) {
(None, k) => found = k,
(Some(a), Some(b)) if a == b => {}
_ => return None,
}
pos += 1;
}
found
}
fn push_const_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
let Some(Operand::Const(idx)) = &instr.operand else {
return None;
};
let constant = self.program.constants.get(*idx as usize)?;
match constant {
Constant::Number(_) => Some(StorageHint::Float64),
Constant::Int(i) => {
let _ = i;
Some(StorageHint::Int64)
}
Constant::UInt(u) => {
let _ = u;
Some(StorageHint::Int64)
}
Constant::Bool(_) => Some(StorageHint::Bool),
_ => None,
}
}
fn load_module_binding_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
let Some(Operand::ModuleBinding(idx)) = &instr.operand else {
return None;
};
match self.type_tracker.get_module_binding_storage_hint(*idx) {
Some(kind @ (StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)) => {
Some(kind)
}
_ => None,
}
}
fn load_local_trusted_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
let Some(Operand::Local(idx)) = &instr.operand else {
return None;
};
let hint = self.type_tracker.get_local_storage_hint(*idx);
if matches!(
hint,
Some(StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)
) {
return hint;
}
if let Some(nt) = self.last_expr_numeric_type {
return Some(match nt {
crate::type_tracking::NumericType::Number => StorageHint::Float64,
crate::type_tracking::NumericType::Int => StorageHint::Int64,
crate::type_tracking::NumericType::IntWidth(_) => StorageHint::Int64,
crate::type_tracking::NumericType::Decimal => return None,
});
}
if let Some(info) = &self.last_expr_type_info {
return match info.storage_hint {
Some(kind @ (StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)) => {
Some(kind)
}
_ => None,
};
}
None
}
fn get_field_typed_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
use crate::executor::typed_object_ops::{
FIELD_TAG_BOOL, FIELD_TAG_F64, FIELD_TAG_I64, FIELD_TAG_TIMESTAMP,
};
let Some(Operand::TypedField {
field_type_tag, ..
}) = &instr.operand
else {
return None;
};
match *field_type_tag {
FIELD_TAG_I64 | FIELD_TAG_TIMESTAMP => Some(StorageHint::Int64),
FIELD_TAG_F64 => Some(StorageHint::Float64),
FIELD_TAG_BOOL => Some(StorageHint::Bool),
_ => None,
}
}
fn load_shared_module_binding_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
let Some(Operand::ModuleBinding(idx)) = &instr.operand else {
return None;
};
match self.type_tracker.get_module_binding_storage_hint(*idx) {
Some(kind @ (StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)) => {
Some(kind)
}
_ => None,
}
}
fn deref_load_native_kind(&self) -> Option<StorageHint> {
use crate::executor::typed_object_ops::{
FIELD_TAG_BOOL, FIELD_TAG_F64, FIELD_TAG_I64, FIELD_TAG_TIMESTAMP,
};
let n = self.program.instructions.len();
if n < 4 {
return None;
}
let make_field_ref = &self.program.instructions[n - 3];
if make_field_ref.opcode != OpCode::MakeFieldRef {
return None;
}
let Some(Operand::TypedField {
field_type_tag, ..
}) = &make_field_ref.operand
else {
return None;
};
match *field_type_tag {
FIELD_TAG_I64 | FIELD_TAG_TIMESTAMP => Some(StorageHint::Int64),
FIELD_TAG_F64 => Some(StorageHint::Float64),
FIELD_TAG_BOOL => Some(StorageHint::Bool),
_ => None,
}
}
fn get_prop_native_kind(&self) -> Option<StorageHint> {
let idx = self.program.instructions.len().checked_sub(1)?;
self.get_prop_native_kinds.get(&idx).copied()
}
fn get_prop_native_kind_at(&self, idx: usize) -> Option<StorageHint> {
self.get_prop_native_kinds.get(&idx).copied()
}
pub(super) fn record_get_prop_native_kind(
&mut self,
field_type: Option<&shape_runtime::type_schema::FieldType>,
) {
use shape_runtime::type_schema::FieldType;
let kind = match field_type {
Some(FieldType::I64) | Some(FieldType::Timestamp) => StorageHint::Int64,
Some(FieldType::F64) => StorageHint::Float64,
Some(FieldType::Bool) => StorageHint::Bool,
_ => return,
};
if let Some(idx) = self.program.instructions.len().checked_sub(1) {
self.get_prop_native_kinds.insert(idx, kind);
}
}
pub(super) fn patch_jump(&mut self, jump_idx: usize) {
let offset = self.program.current_offset() as i32 - jump_idx as i32 - 1;
self.program.instructions[jump_idx] = Instruction::new(
self.program.instructions[jump_idx].opcode,
Some(Operand::Offset(offset)),
);
}
pub(super) fn compile_call_args(
&mut self,
args: &[shape_ast::ast::Expr],
expected_param_modes: Option<&[ParamPassMode]>,
) -> Result<Vec<(u16, u16)>> {
self.call_arg_module_binding_ref_writebacks.push(Vec::new());
let mut first_error: Option<ShapeError> = None;
for (idx, arg) in args.iter().enumerate() {
let pass_mode = expected_param_modes
.and_then(|modes| modes.get(idx).copied())
.unwrap_or(ParamPassMode::ByValue);
let arg_result = match pass_mode {
ParamPassMode::ByRefExclusive | ParamPassMode::ByRefShared => {
let borrow_mode = if pass_mode.is_exclusive() {
BorrowMode::Exclusive
} else {
BorrowMode::Shared
};
if let shape_ast::ast::Expr::Reference { expr, span, .. } = arg {
self.compile_reference_expr(expr, *span, borrow_mode)
.map(|_| ())
} else {
self.compile_implicit_reference_arg(arg, borrow_mode)
}
}
ParamPassMode::ByValue => {
if let shape_ast::ast::Expr::Reference { span, .. } = arg {
let message = if expected_param_modes.is_some() {
"[B0004] unexpected `&` argument: target parameter is not a reference parameter".to_string()
} else {
"[B0004] cannot pass `&` to a callable value without a declared reference contract; \
call a named function with known parameter modes or add an explicit callable type"
.to_string()
};
Err(ShapeError::SemanticError {
message,
location: Some(self.span_to_source_location(*span)),
})
} else {
self.plan_flexible_binding_escape_from_expr(arg);
self.compile_expr(arg)
}
}
};
if let Err(err) = arg_result {
if self.should_recover_compile_diagnostics() {
self.errors.push(err);
self.emit(Instruction::simple(OpCode::PushNull));
continue;
}
first_error = Some(err);
break;
}
}
let writebacks = self
.call_arg_module_binding_ref_writebacks
.pop()
.unwrap_or_default();
if let Some(err) = first_error {
Err(err)
} else {
Ok(writebacks)
}
}
pub(super) fn compile_implicit_reference_arg(
&mut self,
arg: &shape_ast::ast::Expr,
mode: BorrowMode,
) -> Result<()> {
use shape_ast::ast::Expr;
match arg {
Expr::Identifier(name, span) => self
.compile_reference_identifier(name, *span, mode)
.map(|_| ()),
Expr::PropertyAccess {
object,
property,
optional: false,
span,
} => self
.compile_reference_property_access(object, property, *span, mode)
.map(|_| ()),
Expr::IndexAccess {
object,
index,
end_index: None,
span,
} => self
.compile_reference_index_access(object, index, *span, mode)
.map(|_| ()),
_ => {
self.compile_expr_preserving_refs(arg)?;
if let Some(returned_mode) = self.last_expr_reference_mode() {
if mode == BorrowMode::Exclusive && returned_mode != BorrowMode::Exclusive {
return Err(ShapeError::SemanticError {
message:
"cannot pass a shared reference result to an exclusive parameter"
.to_string(),
location: Some(self.span_to_source_location(arg.span())),
});
}
return Ok(());
}
if mode == BorrowMode::Exclusive {
return Err(ShapeError::SemanticError {
message:
"[B0004] mutable reference arguments must be simple variables or existing exclusive references"
.to_string(),
location: Some(self.span_to_source_location(arg.span())),
});
}
let temp = self.declare_temp_local("__arg_ref_")?;
self.emit(Instruction::new(
OpCode::StoreLocal,
Some(Operand::Local(temp)),
));
self.emit(Instruction::new(
OpCode::MakeRef,
Some(Operand::Local(temp)),
));
Ok(())
}
}
}
pub(super) fn compile_reference_identifier(
&mut self,
name: &str,
span: shape_ast::ast::Span,
mode: BorrowMode,
) -> Result<u32> {
if let Some(local_idx) = self.resolve_local(name) {
if mode == BorrowMode::Exclusive && self.const_locals.contains(&local_idx) {
return Err(ShapeError::SemanticError {
message: format!(
"Cannot pass const variable '{}' by exclusive reference",
name
),
location: Some(self.span_to_source_location(span)),
});
}
if self.ref_locals.contains(&local_idx) {
self.emit(Instruction::new(
OpCode::LoadLocal,
Some(Operand::Local(local_idx)),
));
return Ok(u32::MAX);
}
if self.reference_value_locals.contains(&local_idx) {
if mode == BorrowMode::Exclusive
&& !self.exclusive_reference_value_locals.contains(&local_idx)
{
return Err(ShapeError::SemanticError {
message: format!(
"Cannot pass shared reference variable '{}' as an exclusive reference",
name
),
location: Some(self.span_to_source_location(span)),
});
}
self.emit(Instruction::new(
OpCode::LoadLocal,
Some(Operand::Local(local_idx)),
));
return Ok(u32::MAX);
}
self.emit(Instruction::new(
OpCode::MakeRef,
Some(Operand::Local(local_idx)),
));
Ok(u32::MAX)
} else if let Some(scoped_name) = self.resolve_scoped_module_binding_name(name) {
let Some(&binding_idx) = self.module_bindings.get(&scoped_name) else {
return Err(ShapeError::SemanticError {
message: format!(
"[B0004] reference argument must be a local or module_binding variable, got '{}'",
name
),
location: Some(self.span_to_source_location(span)),
});
};
if mode == BorrowMode::Exclusive && self.const_module_bindings.contains(&binding_idx) {
return Err(ShapeError::SemanticError {
message: format!(
"Cannot pass const variable '{}' by exclusive reference",
name
),
location: Some(self.span_to_source_location(span)),
});
}
if self.reference_value_module_bindings.contains(&binding_idx) {
if mode == BorrowMode::Exclusive
&& !self
.exclusive_reference_value_module_bindings
.contains(&binding_idx)
{
return Err(ShapeError::SemanticError {
message: format!(
"Cannot pass shared reference variable '{}' as an exclusive reference",
name
),
location: Some(self.span_to_source_location(span)),
});
}
self.emit(Instruction::new(
OpCode::LoadModuleBinding,
Some(Operand::ModuleBinding(binding_idx)),
));
return Ok(u32::MAX);
}
self.emit(Instruction::new(
OpCode::MakeRef,
Some(Operand::ModuleBinding(binding_idx)),
));
Ok(u32::MAX)
} else if let Some(func_idx) = self.find_function(name) {
let temp = self.declare_temp_local("__fn_ref_")?;
let const_idx = self
.program
.add_constant(Constant::Function(func_idx as u16));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(const_idx)),
));
self.emit(Instruction::new(
OpCode::StoreLocal,
Some(Operand::Local(temp)),
));
self.emit(Instruction::new(
OpCode::MakeRef,
Some(Operand::Local(temp)),
));
Ok(u32::MAX)
} else {
Err(ShapeError::SemanticError {
message: format!(
"[B0004] reference argument must be a local or module_binding variable, got '{}'",
name
),
location: Some(self.span_to_source_location(span)),
})
}
}
pub(super) fn push_scope(&mut self) {
self.locals.push(HashMap::new());
self.type_tracker.push_scope();
}
pub(super) fn pop_scope(&mut self) {
self.locals.pop();
self.type_tracker.pop_scope();
}
pub(super) fn declare_local(&mut self, name: &str) -> Result<u16> {
let idx = self.next_local;
self.next_local += 1;
if let Some(scope) = self.locals.last_mut() {
scope.insert(name.to_string(), idx);
}
Ok(idx)
}
pub(super) fn resolve_local(&self, name: &str) -> Option<u16> {
for scope in self.locals.iter().rev() {
if let Some(&idx) = scope.get(name) {
return Some(idx);
}
}
None
}
pub(super) fn local_name_for_slot(&self, slot: u16) -> Option<&str> {
for scope in self.locals.iter().rev() {
for (name, &idx) in scope.iter() {
if idx == slot {
return Some(name.as_str());
}
}
}
None
}
pub(super) fn slot_is_boxed(&self, slot: u16) -> bool {
self.local_name_for_slot(slot)
.map(|name| self.boxed_locals.contains(name))
.unwrap_or(false)
}
pub(super) fn slot_is_shared(&self, slot: u16) -> bool {
self.local_name_for_slot(slot)
.map(|name| self.shared_locals.contains(name))
.unwrap_or(false)
}
pub(super) fn declare_temp_local(&mut self, prefix: &str) -> Result<u16> {
let name = format!("{}{}", prefix, self.next_local);
self.declare_local(&name)
}
pub(super) fn set_local_type_info(&mut self, slot: u16, type_name: &str) {
let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
VariableTypeInfo::known(schema.id, type_name.to_string())
} else {
VariableTypeInfo::named(type_name.to_string())
};
self.type_tracker.set_local_type(slot, info);
}
pub(super) fn set_module_binding_type_info(&mut self, slot: u16, type_name: &str) {
let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
VariableTypeInfo::known(schema.id, type_name.to_string())
} else {
VariableTypeInfo::named(type_name.to_string())
};
self.type_tracker.set_binding_type(slot, info);
}
pub(super) fn capture_function_local_storage_hints(&mut self, func_idx: usize) {
let Some(func) = self.program.functions.get(func_idx) else {
return;
};
let proven_hints: Option<Vec<StorageHint>> = (0..func.locals_count)
.map(|slot| self.type_tracker.get_local_storage_hint(slot))
.collect();
let instr_len = self.program.instructions.len();
let code_end = if func.body_length > 0 {
(func.entry_point + func.body_length).min(instr_len)
} else {
instr_len
};
let has_trusted = if func.entry_point <= code_end && code_end <= instr_len {
self.program.instructions[func.entry_point..code_end]
.iter()
.any(|i| i.opcode.is_trusted())
} else {
false
};
let hints: Vec<StorageHint> = match proven_hints {
Some(h) => {
if !h.is_empty() || has_trusted {
self.program.functions[func_idx].frame_descriptor = Some(
crate::type_tracking::FrameDescriptor::from_slots(h.clone()),
);
}
h
}
None => Vec::new(),
};
if self.program.function_local_storage_hints.len() <= func_idx {
self.program
.function_local_storage_hints
.resize(func_idx + 1, Vec::new());
}
self.program.function_local_storage_hints[func_idx] = hints;
}
pub(super) fn infer_top_level_return_kind_from_item(
&self,
item: &shape_ast::ast::Item,
) -> Option<StorageHint> {
use shape_ast::ast::{Expr, Item, Statement};
let expr: &Expr = match item {
Item::Expression(expr, _) => expr,
Item::Statement(Statement::Expression(expr, _), _) => expr,
_ => return None,
};
if let Expr::MethodCall { .. } = expr {
return self.last_emitted_native_kind();
}
if let Expr::Match(match_expr, _) = expr {
return Self::match_arms_uniform_literal_kind(match_expr);
}
let owned_qualified;
let call_name: &str = match expr {
Expr::FunctionCall { name, .. } => name.as_str(),
Expr::QualifiedFunctionCall {
namespace,
function,
..
} => {
owned_qualified = format!("{}::{}", namespace, function);
owned_qualified.as_str()
}
_ => return None,
};
let return_ann: Option<&TypeAnnotation> = self
.function_defs
.get(call_name)
.and_then(|def| def.return_type.as_ref())
.or_else(|| {
self.foreign_function_defs
.get(call_name)
.and_then(|def| def.return_type.as_ref())
});
let ann = return_ann?;
let name = ann.as_type_name_str()?;
let inferred = primitive_type_name_to_storage_hint(name)
.or_else(|| {
self.type_aliases
.get(name)
.and_then(|aliased| primitive_type_name_to_storage_hint(aliased.as_str()))
})
.or_else(|| {
if let shape_ast::ast::Expr::QualifiedFunctionCall { namespace, .. } = expr {
let q = format!("{}::{}", namespace, name);
self.type_aliases
.get(&q)
.and_then(|aliased| primitive_type_name_to_storage_hint(aliased.as_str()))
} else {
None
}
})?;
let native_kind = self.last_emitted_native_kind()?;
if matches!(
inferred,
StorageHint::Int8
| StorageHint::UInt8
| StorageHint::Int16
| StorageHint::UInt16
| StorageHint::Int32
| StorageHint::UInt32
| StorageHint::Int64
| StorageHint::UInt64
) && native_kind == StorageHint::Int64
{
return Some(inferred);
}
if native_kind == inferred {
Some(inferred)
} else {
None
}
}
fn match_arms_uniform_literal_kind(
match_expr: &shape_ast::ast::expr_helpers::MatchExpr,
) -> Option<StorageHint> {
use shape_ast::ast::{Expr, literals::Literal};
let mut uniform: Option<StorageHint> = None;
for arm in &match_expr.arms {
let kind = match &*arm.body {
Expr::Literal(Literal::Int(i), _) => {
let _ = i;
StorageHint::Int64
}
Expr::Literal(Literal::TypedInt(_, w), _) => {
use shape_ast::IntWidth;
match w {
IntWidth::I8 => StorageHint::Int8,
IntWidth::U8 => StorageHint::UInt8,
IntWidth::I16 => StorageHint::Int16,
IntWidth::U16 => StorageHint::UInt16,
IntWidth::I32 => StorageHint::Int32,
IntWidth::U32 => StorageHint::UInt32,
IntWidth::U64 => StorageHint::UInt64,
}
}
Expr::Literal(Literal::Bool(_), _) => StorageHint::Bool,
Expr::Literal(Literal::Number(_), _) => StorageHint::Float64,
_ => return None,
};
match uniform {
None => uniform = Some(kind),
Some(prev) if prev == kind => {}
_ => return None,
}
}
uniform
}
pub(super) fn infer_top_level_return_kind(&self) -> Option<StorageHint> {
let inferred: StorageHint = self
.last_expr_numeric_type
.and_then(|nt| match nt {
crate::type_tracking::NumericType::Number => Some(StorageHint::Float64),
crate::type_tracking::NumericType::Int => Some(StorageHint::Int64),
crate::type_tracking::NumericType::IntWidth(w) => {
use shape_ast::IntWidth;
Some(match w {
IntWidth::I8 => StorageHint::Int8,
IntWidth::U8 => StorageHint::UInt8,
IntWidth::I16 => StorageHint::Int16,
IntWidth::U16 => StorageHint::UInt16,
IntWidth::I32 => StorageHint::Int32,
IntWidth::U32 => StorageHint::UInt32,
IntWidth::U64 => StorageHint::UInt64,
})
}
crate::type_tracking::NumericType::Decimal => None,
})
.or_else(|| {
self.last_expr_type_info
.as_ref()
.and_then(|info| info.storage_hint)
})?;
let native_kind = self.last_emitted_native_kind()?;
if matches!(
inferred,
StorageHint::Int8
| StorageHint::UInt8
| StorageHint::Int16
| StorageHint::UInt16
| StorageHint::Int32
| StorageHint::UInt32
| StorageHint::Int64
| StorageHint::UInt64
) && native_kind == StorageHint::Int64
{
return Some(inferred);
}
if native_kind == inferred {
Some(inferred)
} else {
None
}
}
pub(super) fn populate_program_storage_hints(&mut self) {
let top_hints_proven: Option<Vec<StorageHint>> = (0..self.next_local)
.map(|slot| self.type_tracker.get_local_storage_hint(slot))
.collect();
let top_hints: Vec<StorageHint> = top_hints_proven.clone().unwrap_or_default();
self.program.top_level_local_storage_hints = top_hints.clone();
let return_kind: Option<StorageHint> = self
.top_level_program_return_kind
.or_else(|| self.infer_top_level_return_kind());
let has_trusted = self
.program
.instructions
.iter()
.any(|i| i.opcode.is_trusted());
let has_any_known = top_hints_proven.is_some() && !top_hints.is_empty();
let has_typed_return = return_kind.is_some();
if has_any_known || has_trusted || has_typed_return {
let slots = top_hints_proven.unwrap_or_default();
let mut frame = crate::type_tracking::FrameDescriptor::from_slots(slots);
frame.return_kind = return_kind;
self.program.top_level_frame = Some(frame);
}
let module_binding_hints: Vec<StorageHint> = (0..self.module_bindings.len() as u16)
.map(|idx| self.type_tracker.get_module_binding_storage_hint(idx))
.collect::<Option<Vec<_>>>()
.unwrap_or_default();
self.program.module_binding_storage_hints = module_binding_hints;
if self.program.function_local_storage_hints.len() < self.program.functions.len() {
self.program
.function_local_storage_hints
.resize(self.program.functions.len(), Vec::new());
} else if self.program.function_local_storage_hints.len() > self.program.functions.len() {
self.program
.function_local_storage_hints
.truncate(self.program.functions.len());
}
}
pub(super) fn propagate_assignment_type_to_slot(
&mut self,
slot: u16,
is_local: bool,
allow_number_hint: bool,
) {
if let Some(ref info) = self.last_expr_type_info {
if info.is_indexed()
|| info.is_datatable()
|| info.schema_id.is_some()
|| Self::is_array_type_name(info.type_name.as_deref())
|| Self::is_temporal_type_name(info.type_name.as_deref())
|| matches!(
info.type_name.as_deref(),
Some("string" | "bool" | "char")
)
{
if is_local {
self.type_tracker.set_local_type(slot, info.clone());
} else {
self.type_tracker.set_binding_type(slot, info.clone());
}
return;
}
}
if let Some(schema_id) = self.last_expr_schema {
let schema_name = self
.type_tracker
.schema_registry()
.get_by_id(schema_id)
.map(|s| s.name.clone())
.unwrap_or_else(|| format!("__anon_{}", schema_id));
let info = VariableTypeInfo::known(schema_id, schema_name);
if is_local {
self.type_tracker.set_local_type(slot, info);
} else {
self.type_tracker.set_binding_type(slot, info);
}
return;
}
if let Some(numeric_type) = self.last_expr_numeric_type {
let (type_name, hint) = match numeric_type {
crate::type_tracking::NumericType::Int => ("int", StorageHint::Int64),
crate::type_tracking::NumericType::IntWidth(w) => {
use shape_ast::IntWidth;
let hint = match w {
IntWidth::I8 => StorageHint::Int8,
IntWidth::U8 => StorageHint::UInt8,
IntWidth::I16 => StorageHint::Int16,
IntWidth::U16 => StorageHint::UInt16,
IntWidth::I32 => StorageHint::Int32,
IntWidth::U32 => StorageHint::UInt32,
IntWidth::U64 => StorageHint::UInt64,
};
(w.type_name(), hint)
}
crate::type_tracking::NumericType::Number => {
if !allow_number_hint {
if is_local {
self.type_tracker
.set_local_type(slot, VariableTypeInfo::unknown());
} else {
self.type_tracker
.set_binding_type(slot, VariableTypeInfo::unknown());
}
return;
}
("number", StorageHint::Float64)
}
crate::type_tracking::NumericType::Decimal => {
if is_local {
self.type_tracker
.set_local_type(slot, VariableTypeInfo::unknown());
} else {
self.type_tracker
.set_binding_type(slot, VariableTypeInfo::unknown());
}
return;
}
};
let info = VariableTypeInfo::with_storage(type_name.to_string(), hint);
if is_local {
self.type_tracker.set_local_type(slot, info);
} else {
self.type_tracker.set_binding_type(slot, info);
}
return;
}
if is_local {
self.type_tracker
.set_local_type(slot, VariableTypeInfo::unknown());
} else {
self.type_tracker
.set_binding_type(slot, VariableTypeInfo::unknown());
}
}
pub(super) fn propagate_assignment_type_to_identifier(&mut self, name: &str) {
if let Some(local_idx) = self.resolve_local(name) {
if self.local_binding_is_reference_value(local_idx) {
return;
}
self.propagate_assignment_type_to_slot(local_idx, true, true);
return;
}
let scoped_name = self
.resolve_scoped_module_binding_name(name)
.unwrap_or_else(|| name.to_string());
let binding_idx = self.get_or_create_module_binding(&scoped_name);
self.propagate_assignment_type_to_slot(binding_idx, false, true);
}
pub(crate) fn resolve_canonical_module_path(&self, local_name: &str) -> Option<String> {
self.graph_namespace_map
.get(local_name)
.or_else(|| self.module_scope_sources.get(local_name))
.cloned()
}
pub fn type_tracker(&self) -> &TypeTracker {
&self.type_tracker
}
pub fn type_tracker_mut(&mut self) -> &mut TypeTracker {
&mut self.type_tracker
}
pub(super) fn resolve_column_index(&self, field: &str) -> Result<u32> {
self.program
.data_schema
.as_ref()
.ok_or_else(|| ShapeError::RuntimeError {
message: format!(
"No data schema provided. Cannot resolve field '{}'. \
Hint: Use stdlib/finance to load market data with OHLCV schema.",
field
),
location: None,
})?
.get_index(field)
.ok_or_else(|| ShapeError::RuntimeError {
message: format!(
"Unknown column '{}' in data schema. Available columns: {:?}",
field,
self.program
.data_schema
.as_ref()
.map(|s| &s.column_names)
.unwrap_or(&vec![])
),
location: None,
})
}
pub(super) fn is_data_column(&self, field: &str) -> bool {
self.program
.data_schema
.as_ref()
.map(|s| s.get_index(field).is_some())
.unwrap_or(false)
}
pub(super) fn collect_outer_scope_vars(&self) -> Vec<String> {
let mut names = BTreeSet::new();
for scope in &self.locals {
for name in scope.keys() {
names.insert(name.clone());
}
}
for name in self.module_bindings.keys() {
names.insert(name.clone());
}
names.into_iter().collect()
}
pub(super) fn get_or_create_module_binding(&mut self, name: &str) -> u16 {
if let Some(&idx) = self.module_bindings.get(name) {
idx
} else {
let idx = self.next_global;
self.next_global += 1;
self.module_bindings.insert(name.to_string(), idx);
idx
}
}
pub(super) fn resolve_scoped_module_binding_name(&self, name: &str) -> Option<String> {
if self.module_bindings.contains_key(name) {
return Some(name.to_string());
}
for module_path in self.module_scope_stack.iter().rev() {
let candidate = format!("{}::{}", module_path, name);
if self.module_bindings.contains_key(&candidate) {
return Some(candidate);
}
}
None
}
pub(crate) fn shared_module_binding_contains(&self, name: &str) -> bool {
if self.shared_module_bindings.contains(name) {
return true;
}
if let Some(scoped) = self.resolve_scoped_module_binding_name(name) {
return self.shared_module_bindings.contains(&scoped);
}
false
}
pub(super) fn resolve_scoped_function_name(&self, name: &str) -> Option<String> {
if self.program.functions.iter().any(|f| f.name == name) {
return Some(name.to_string());
}
for module_path in self.module_scope_stack.iter().rev() {
let candidate = format!("{}::{}", module_path, name);
if self.program.functions.iter().any(|f| f.name == candidate) {
return Some(candidate);
}
}
None
}
pub(super) fn find_function(&self, name: &str) -> Option<usize> {
if let Some(actual_name) = self.function_aliases.get(name) {
if let Some(idx) = self
.program
.functions
.iter()
.position(|f| f.name == *actual_name)
{
return Some(idx);
}
}
if let Some(resolved) = self.resolve_scoped_function_name(name) {
if let Some(idx) = self
.program
.functions
.iter()
.position(|f| f.name == resolved)
{
return Some(idx);
}
}
if let Some(imported) = self.imported_names.get(name) {
let original = &imported.original_name;
if let Some(idx) = self
.program
.functions
.iter()
.position(|f| f.name == *original)
{
return Some(idx);
}
if let Some(resolved) = self.resolve_scoped_function_name(original) {
if let Some(idx) = self
.program
.functions
.iter()
.position(|f| f.name == resolved)
{
return Some(idx);
}
}
if !imported.module_path.is_empty() {
let qualified = format!("{}::{}", imported.module_path, original);
if let Some(idx) = self
.program
.functions
.iter()
.position(|f| f.name == qualified)
{
return Some(idx);
}
}
}
None
}
pub(super) fn resolve_receiver_extend_type(
&self,
receiver: &shape_ast::ast::Expr,
receiver_type_info: &Option<crate::type_tracking::VariableTypeInfo>,
_receiver_schema: Option<u32>,
) -> Option<String> {
if let Some(numeric) = self.last_expr_numeric_type {
return Some(
match numeric {
crate::type_tracking::NumericType::Int
| crate::type_tracking::NumericType::IntWidth(_) => "Int",
crate::type_tracking::NumericType::Number => "Number",
crate::type_tracking::NumericType::Decimal => "Decimal",
}
.to_string(),
);
}
if let Some(info) = receiver_type_info {
if let Some(type_name) = &info.type_name {
let base = type_name.split('<').next().unwrap_or(type_name);
return Some(base.to_string());
}
}
match receiver {
shape_ast::ast::Expr::Literal(lit, _) => match lit {
shape_ast::ast::Literal::String(_)
| shape_ast::ast::Literal::FormattedString { .. } => Some("String".to_string()),
shape_ast::ast::Literal::Bool(_) => Some("Bool".to_string()),
_ => None,
},
shape_ast::ast::Expr::Array(..) => Some("Vec".to_string()),
_ => None,
}
}
pub(super) fn emit_store_identifier(&mut self, name: &str) -> Result<()> {
if let Some(&upvalue_idx) = self.mutable_closure_captures.get(name) {
if let Some(&shared_idx) = self.shared_closure_captures.get(name) {
debug_assert_eq!(upvalue_idx, shared_idx);
let opcode = match self.shared_capture_inner_kinds.get(name).copied() {
Some(kind) => shared_typed_store_opcode(kind),
None => OpCode::StoreSharedCapture,
};
self.emit(Instruction::new(opcode, Some(Operand::Local(shared_idx))));
return Ok(());
}
if let Some(&owned_idx) = self.owned_mutable_closure_captures.get(name) {
debug_assert_eq!(upvalue_idx, owned_idx);
let opcode = match self.owned_mutable_capture_inner_kinds.get(name).copied() {
Some(kind) => owned_mutable_typed_store_opcode(kind),
None => OpCode::StoreOwnedMutableCapture,
};
self.emit(Instruction::new(opcode, Some(Operand::Local(owned_idx))));
return Ok(());
}
self.emit(Instruction::new(
OpCode::StoreClosure,
Some(Operand::Local(upvalue_idx)),
));
return Ok(());
}
if self.shared_locals.contains(name)
&& let Some(local_idx) = self.resolve_local(name)
{
self.emit(Instruction::new(
OpCode::StoreSharedLocal,
Some(Operand::Local(local_idx)),
));
return Ok(());
}
if let Some(local_idx) = self.resolve_local(name) {
if self.local_binding_is_reference_value(local_idx) {
if !self.local_reference_binding_is_exclusive(local_idx) {
return Err(ShapeError::SemanticError {
message: format!(
"cannot assign through shared reference variable '{}'",
name
),
location: None,
});
}
self.emit(Instruction::new(
OpCode::DerefStore,
Some(Operand::Local(local_idx)),
));
} else {
self.emit(Instruction::new(
OpCode::StoreLocal,
Some(Operand::Local(local_idx)),
));
if let Some(type_name) = self
.type_tracker
.get_local_type(local_idx)
.and_then(|info| info.type_name.as_deref())
{
if let Some(w) = shape_ast::IntWidth::from_name(type_name) {
if let Some(last) = self.program.instructions.last_mut() {
if last.opcode == OpCode::StoreLocal {
last.opcode = OpCode::StoreLocalTyped;
last.operand = Some(Operand::TypedLocal(
local_idx,
crate::bytecode::NumericWidth::from_int_width(w),
));
}
}
}
}
}
} else {
let scoped_name = self
.resolve_scoped_module_binding_name(name)
.unwrap_or_else(|| name.to_string());
let binding_idx = self.get_or_create_module_binding(&scoped_name);
self.emit(Instruction::new(
OpCode::StoreModuleBinding,
Some(Operand::ModuleBinding(binding_idx)),
));
if let Some(type_name) = self
.type_tracker
.get_binding_type(binding_idx)
.and_then(|info| info.type_name.as_deref())
{
if let Some(w) = shape_ast::IntWidth::from_name(type_name) {
if let Some(last) = self.program.instructions.last_mut() {
if last.opcode == OpCode::StoreModuleBinding {
last.opcode = OpCode::StoreModuleBindingTyped;
last.operand = Some(Operand::TypedModuleBinding(
binding_idx,
crate::bytecode::NumericWidth::from_int_width(w),
));
}
}
}
}
}
Ok(())
}
pub(super) fn classify_builtin_function(&self, name: &str) -> Option<BuiltinNameResolution> {
let builtin = match name {
"Some" => BuiltinFunction::SomeCtor,
"Ok" => BuiltinFunction::OkCtor,
"Err" => BuiltinFunction::ErrCtor,
"HashMap" => BuiltinFunction::HashMapCtor,
"Set" => BuiltinFunction::SetCtor,
"Deque" => BuiltinFunction::DequeCtor,
"PriorityQueue" => BuiltinFunction::PriorityQueueCtor,
"Mutex" => BuiltinFunction::MutexCtor,
"Atomic" => BuiltinFunction::AtomicCtor,
"Lazy" => BuiltinFunction::LazyCtor,
"Channel" => BuiltinFunction::ChannelCtor,
"__json_object_get" => BuiltinFunction::JsonObjectGet,
"__json_array_at" => BuiltinFunction::JsonArrayAt,
"__json_object_keys" => BuiltinFunction::JsonObjectKeys,
"__json_array_len" => BuiltinFunction::JsonArrayLen,
"__json_object_len" => BuiltinFunction::JsonObjectLen,
"__intrinsic_vec_abs" => BuiltinFunction::IntrinsicVecAbs,
"__intrinsic_vec_sqrt" => BuiltinFunction::IntrinsicVecSqrt,
"__intrinsic_vec_ln" => BuiltinFunction::IntrinsicVecLn,
"__intrinsic_vec_exp" => BuiltinFunction::IntrinsicVecExp,
"__intrinsic_vec_add" => BuiltinFunction::IntrinsicVecAdd,
"__intrinsic_vec_sub" => BuiltinFunction::IntrinsicVecSub,
"__intrinsic_vec_mul" => BuiltinFunction::IntrinsicVecMul,
"__intrinsic_vec_div" => BuiltinFunction::IntrinsicVecDiv,
"__intrinsic_vec_max" => BuiltinFunction::IntrinsicVecMax,
"__intrinsic_vec_min" => BuiltinFunction::IntrinsicVecMin,
"__intrinsic_vec_select" => BuiltinFunction::IntrinsicVecSelect,
"__intrinsic_matmul_vec" => BuiltinFunction::IntrinsicMatMulVec,
"__intrinsic_matmul_mat" => BuiltinFunction::IntrinsicMatMulMat,
"__intrinsic_vec_add_i64" => BuiltinFunction::IntrinsicVecAddI64,
"__intrinsic_mat_add" => BuiltinFunction::IntrinsicMatAdd,
"__intrinsic_mat_sub" => BuiltinFunction::IntrinsicMatSub,
"abs" => BuiltinFunction::Abs,
"min" => BuiltinFunction::Min,
"max" => BuiltinFunction::Max,
"sqrt" => BuiltinFunction::Sqrt,
"ln" => BuiltinFunction::Ln,
"pow" => BuiltinFunction::Pow,
"exp" => BuiltinFunction::Exp,
"log" => BuiltinFunction::Log,
"floor" => BuiltinFunction::Floor,
"ceil" => BuiltinFunction::Ceil,
"round" => BuiltinFunction::Round,
"sin" => BuiltinFunction::Sin,
"cos" => BuiltinFunction::Cos,
"tan" => BuiltinFunction::Tan,
"asin" => BuiltinFunction::Asin,
"acos" => BuiltinFunction::Acos,
"atan" => BuiltinFunction::Atan,
"stddev" => BuiltinFunction::StdDev,
"__intrinsic_map" => BuiltinFunction::Map,
"__intrinsic_filter" => BuiltinFunction::Filter,
"__intrinsic_reduce" => BuiltinFunction::Reduce,
"print" => BuiltinFunction::Print,
"format" => BuiltinFunction::Format,
"__intrinsic_snapshot" | "snapshot" => BuiltinFunction::Snapshot,
"exit" => BuiltinFunction::Exit,
"range" => BuiltinFunction::Range,
"is_number" | "isNumber" => BuiltinFunction::IsNumber,
"is_string" | "isString" => BuiltinFunction::IsString,
"is_bool" | "isBool" => BuiltinFunction::IsBool,
"is_array" | "isArray" => BuiltinFunction::IsArray,
"is_object" | "isObject" => BuiltinFunction::IsObject,
"is_data_row" | "isDataRow" => BuiltinFunction::IsDataRow,
"to_string" | "toString" => BuiltinFunction::ToString,
"to_number" | "toNumber" => BuiltinFunction::ToNumber,
"to_bool" | "toBool" => BuiltinFunction::ToBool,
"__native_ptr_size" => BuiltinFunction::NativePtrSize,
"__native_ptr_new_cell" => BuiltinFunction::NativePtrNewCell,
"__native_ptr_free_cell" => BuiltinFunction::NativePtrFreeCell,
"__native_ptr_read_ptr" => BuiltinFunction::NativePtrReadPtr,
"__native_ptr_write_ptr" => BuiltinFunction::NativePtrWritePtr,
"__native_table_from_arrow_c" => BuiltinFunction::NativeTableFromArrowC,
"__native_table_from_arrow_c_typed" => BuiltinFunction::NativeTableFromArrowCTyped,
"__native_table_bind_type" => BuiltinFunction::NativeTableBindType,
"fold" => BuiltinFunction::ControlFold,
"__intrinsic_minimize" => BuiltinFunction::IntrinsicMinimize,
"__intrinsic_bspline2_3d_batch" => BuiltinFunction::IntrinsicBspline2_3dBatch,
"__intrinsic_mean" => BuiltinFunction::IntrinsicMean,
"__intrinsic_min" => BuiltinFunction::IntrinsicMin,
"__intrinsic_max" => BuiltinFunction::IntrinsicMax,
"__intrinsic_std" => BuiltinFunction::IntrinsicStd,
"__intrinsic_variance" => BuiltinFunction::IntrinsicVariance,
"__intrinsic_random" => BuiltinFunction::IntrinsicRandom,
"__intrinsic_random_int" => BuiltinFunction::IntrinsicRandomInt,
"__intrinsic_random_seed" => BuiltinFunction::IntrinsicRandomSeed,
"__intrinsic_random_normal" => BuiltinFunction::IntrinsicRandomNormal,
"__intrinsic_random_array" => BuiltinFunction::IntrinsicRandomArray,
"__intrinsic_dist_uniform" => BuiltinFunction::IntrinsicDistUniform,
"__intrinsic_dist_lognormal" => BuiltinFunction::IntrinsicDistLognormal,
"__intrinsic_dist_exponential" => BuiltinFunction::IntrinsicDistExponential,
"__intrinsic_dist_poisson" => BuiltinFunction::IntrinsicDistPoisson,
"__intrinsic_dist_sample_n" => BuiltinFunction::IntrinsicDistSampleN,
"__intrinsic_brownian_motion" => BuiltinFunction::IntrinsicBrownianMotion,
"__intrinsic_gbm" => BuiltinFunction::IntrinsicGbm,
"__intrinsic_ou_process" => BuiltinFunction::IntrinsicOuProcess,
"__intrinsic_random_walk" => BuiltinFunction::IntrinsicRandomWalk,
"__intrinsic_rolling_sum" => BuiltinFunction::IntrinsicRollingSum,
"__intrinsic_rolling_mean" => BuiltinFunction::IntrinsicRollingMean,
"__intrinsic_rolling_std" => BuiltinFunction::IntrinsicRollingStd,
"__intrinsic_rolling_min" => BuiltinFunction::IntrinsicRollingMin,
"__intrinsic_rolling_max" => BuiltinFunction::IntrinsicRollingMax,
"__intrinsic_ema" => BuiltinFunction::IntrinsicEma,
"__intrinsic_linear_recurrence" => BuiltinFunction::IntrinsicLinearRecurrence,
"__intrinsic_shift" => BuiltinFunction::IntrinsicShift,
"__intrinsic_diff" => BuiltinFunction::IntrinsicDiff,
"__intrinsic_pct_change" => BuiltinFunction::IntrinsicPctChange,
"__intrinsic_fillna" => BuiltinFunction::IntrinsicFillna,
"__intrinsic_cumsum" => BuiltinFunction::IntrinsicCumsum,
"__intrinsic_cumprod" => BuiltinFunction::IntrinsicCumprod,
"__intrinsic_clip" => BuiltinFunction::IntrinsicClip,
"__intrinsic_sin" => BuiltinFunction::Sin,
"__intrinsic_cos" => BuiltinFunction::Cos,
"__intrinsic_tan" => BuiltinFunction::Tan,
"__intrinsic_asin" => BuiltinFunction::Asin,
"__intrinsic_acos" => BuiltinFunction::Acos,
"__intrinsic_atan" => BuiltinFunction::Atan,
"__intrinsic_atan2" => BuiltinFunction::IntrinsicAtan2,
"__intrinsic_sinh" => BuiltinFunction::IntrinsicSinh,
"__intrinsic_cosh" => BuiltinFunction::IntrinsicCosh,
"__intrinsic_tanh" => BuiltinFunction::IntrinsicTanh,
"__intrinsic_correlation" => BuiltinFunction::IntrinsicCorrelation,
"__intrinsic_covariance" => BuiltinFunction::IntrinsicCovariance,
"__intrinsic_percentile" => BuiltinFunction::IntrinsicPercentile,
"__intrinsic_median" => BuiltinFunction::IntrinsicMedian,
"__intrinsic_char_code" => BuiltinFunction::IntrinsicCharCode,
"__intrinsic_from_char_code" => BuiltinFunction::IntrinsicFromCharCode,
"__intrinsic_series" => BuiltinFunction::IntrinsicSeries,
"reflect" => BuiltinFunction::Reflect,
"sign" => BuiltinFunction::Sign,
"gcd" => BuiltinFunction::Gcd,
"lcm" => BuiltinFunction::Lcm,
"hypot" => BuiltinFunction::Hypot,
"clamp" => BuiltinFunction::Clamp,
"isNaN" | "is_nan" => BuiltinFunction::IsNaN,
"isFinite" | "is_finite" => BuiltinFunction::IsFinite,
"mat" => BuiltinFunction::MatFromFlat,
_ => return None,
};
let scope = match name {
"Some" | "Ok" | "Err" => ResolutionScope::TypeAssociated,
"print" => ResolutionScope::Prelude,
_ if Self::is_internal_intrinsic_name(name) => ResolutionScope::InternalIntrinsic,
_ => ResolutionScope::ModuleBinding,
};
Some(match scope {
ResolutionScope::InternalIntrinsic => {
BuiltinNameResolution::InternalOnly { builtin, scope }
}
_ => BuiltinNameResolution::Surface { builtin, scope },
})
}
pub(super) fn is_internal_intrinsic_name(name: &str) -> bool {
name.starts_with("__native_")
|| name.starts_with("__intrinsic_")
|| name.starts_with("__json_")
}
pub(super) const fn variable_scope_summary() -> &'static str {
"Variable names resolve from local scope and module scope."
}
pub(super) const fn function_scope_summary() -> &'static str {
"Function names resolve from module scope, explicit imports, type-associated scope, and the implicit prelude."
}
pub(super) fn undefined_variable_message(&self, name: &str) -> String {
format!(
"Undefined variable: {}. {}",
name,
Self::variable_scope_summary()
)
}
pub(super) fn undefined_function_message(&self, name: &str) -> String {
format!(
"Undefined function: {}. {}",
name,
Self::function_scope_summary()
)
}
pub(super) fn internal_intrinsic_error_message(
&self,
name: &str,
resolution: BuiltinNameResolution,
) -> String {
format!(
"'{}' resolves to {} and is not available from ordinary user code. Internal intrinsics are reserved for std::* implementations and compiler-generated code.",
name,
resolution.scope().label()
)
}
pub(super) fn builtin_requires_arg_count(&self, builtin: BuiltinFunction) -> bool {
matches!(
builtin,
BuiltinFunction::Abs
| BuiltinFunction::Min
| BuiltinFunction::Max
| BuiltinFunction::Sqrt
| BuiltinFunction::Ln
| BuiltinFunction::Pow
| BuiltinFunction::Exp
| BuiltinFunction::Log
| BuiltinFunction::Floor
| BuiltinFunction::Ceil
| BuiltinFunction::Round
| BuiltinFunction::Sin
| BuiltinFunction::Cos
| BuiltinFunction::Tan
| BuiltinFunction::Asin
| BuiltinFunction::Acos
| BuiltinFunction::Atan
| BuiltinFunction::StdDev
| BuiltinFunction::Range
| BuiltinFunction::Slice
| BuiltinFunction::Push
| BuiltinFunction::Pop
| BuiltinFunction::First
| BuiltinFunction::Last
| BuiltinFunction::Zip
| BuiltinFunction::Map
| BuiltinFunction::Filter
| BuiltinFunction::Reduce
| BuiltinFunction::ForEach
| BuiltinFunction::Find
| BuiltinFunction::FindIndex
| BuiltinFunction::Some
| BuiltinFunction::Every
| BuiltinFunction::SomeCtor
| BuiltinFunction::OkCtor
| BuiltinFunction::ErrCtor
| BuiltinFunction::HashMapCtor
| BuiltinFunction::SetCtor
| BuiltinFunction::DequeCtor
| BuiltinFunction::PriorityQueueCtor
| BuiltinFunction::MutexCtor
| BuiltinFunction::AtomicCtor
| BuiltinFunction::LazyCtor
| BuiltinFunction::ChannelCtor
| BuiltinFunction::Print
| BuiltinFunction::Format
| BuiltinFunction::Snapshot
| BuiltinFunction::ObjectRest
| BuiltinFunction::IsNumber
| BuiltinFunction::IsString
| BuiltinFunction::IsBool
| BuiltinFunction::IsArray
| BuiltinFunction::IsObject
| BuiltinFunction::IsDataRow
| BuiltinFunction::ToString
| BuiltinFunction::ToNumber
| BuiltinFunction::ToBool
| BuiltinFunction::NativePtrSize
| BuiltinFunction::NativePtrNewCell
| BuiltinFunction::NativePtrFreeCell
| BuiltinFunction::NativePtrReadPtr
| BuiltinFunction::NativePtrWritePtr
| BuiltinFunction::NativeTableFromArrowC
| BuiltinFunction::NativeTableFromArrowCTyped
| BuiltinFunction::NativeTableBindType
| BuiltinFunction::ControlFold
| BuiltinFunction::IntrinsicMinimize
| BuiltinFunction::IntrinsicBspline2_3dBatch
| BuiltinFunction::IntrinsicMean
| BuiltinFunction::IntrinsicMin
| BuiltinFunction::IntrinsicMax
| BuiltinFunction::IntrinsicStd
| BuiltinFunction::IntrinsicVariance
| BuiltinFunction::IntrinsicRandom
| BuiltinFunction::IntrinsicRandomInt
| BuiltinFunction::IntrinsicRandomSeed
| BuiltinFunction::IntrinsicRandomNormal
| BuiltinFunction::IntrinsicRandomArray
| BuiltinFunction::IntrinsicDistUniform
| BuiltinFunction::IntrinsicDistLognormal
| BuiltinFunction::IntrinsicDistExponential
| BuiltinFunction::IntrinsicDistPoisson
| BuiltinFunction::IntrinsicDistSampleN
| BuiltinFunction::IntrinsicBrownianMotion
| BuiltinFunction::IntrinsicGbm
| BuiltinFunction::IntrinsicOuProcess
| BuiltinFunction::IntrinsicRandomWalk
| BuiltinFunction::IntrinsicRollingSum
| BuiltinFunction::IntrinsicRollingMean
| BuiltinFunction::IntrinsicRollingStd
| BuiltinFunction::IntrinsicRollingMin
| BuiltinFunction::IntrinsicRollingMax
| BuiltinFunction::IntrinsicEma
| BuiltinFunction::IntrinsicLinearRecurrence
| BuiltinFunction::IntrinsicShift
| BuiltinFunction::IntrinsicDiff
| BuiltinFunction::IntrinsicPctChange
| BuiltinFunction::IntrinsicFillna
| BuiltinFunction::IntrinsicCumsum
| BuiltinFunction::IntrinsicCumprod
| BuiltinFunction::IntrinsicClip
| BuiltinFunction::IntrinsicCorrelation
| BuiltinFunction::IntrinsicCovariance
| BuiltinFunction::IntrinsicPercentile
| BuiltinFunction::IntrinsicMedian
| BuiltinFunction::IntrinsicAtan2
| BuiltinFunction::IntrinsicSinh
| BuiltinFunction::IntrinsicCosh
| BuiltinFunction::IntrinsicTanh
| BuiltinFunction::IntrinsicCharCode
| BuiltinFunction::IntrinsicFromCharCode
| BuiltinFunction::IntrinsicSeries
| BuiltinFunction::IntrinsicVecAbs
| BuiltinFunction::IntrinsicVecSqrt
| BuiltinFunction::IntrinsicVecLn
| BuiltinFunction::IntrinsicVecExp
| BuiltinFunction::IntrinsicVecAdd
| BuiltinFunction::IntrinsicVecSub
| BuiltinFunction::IntrinsicVecMul
| BuiltinFunction::IntrinsicVecDiv
| BuiltinFunction::IntrinsicVecMax
| BuiltinFunction::IntrinsicVecMin
| BuiltinFunction::IntrinsicVecSelect
| BuiltinFunction::IntrinsicVecAddI64
| BuiltinFunction::IntrinsicMatMulVec
| BuiltinFunction::IntrinsicMatMulMat
| BuiltinFunction::IntrinsicMatAdd
| BuiltinFunction::IntrinsicMatSub
| BuiltinFunction::Sign
| BuiltinFunction::Gcd
| BuiltinFunction::Lcm
| BuiltinFunction::Hypot
| BuiltinFunction::Clamp
| BuiltinFunction::IsNaN
| BuiltinFunction::IsFinite
| BuiltinFunction::MatFromFlat
)
}
pub(super) fn has_any_user_defined_method(&self, method: &str) -> bool {
let dot_suffix = format!(".{}", method);
let colon_suffix = format!("::{}", method);
self.program
.functions
.iter()
.any(|f| f.name.ends_with(&dot_suffix) || f.name.ends_with(&colon_suffix))
}
pub(super) fn is_known_builtin_method(method: &str) -> bool {
matches!(method,
"map" | "filter" | "reduce" | "forEach" | "find" | "findIndex"
| "some" | "every" | "sort" | "groupBy" | "flatMap"
| "len" | "length" | "first" | "last" | "reverse" | "slice"
| "concat" | "take" | "drop" | "skip"
| "indexOf" | "includes"
| "join" | "flatten" | "unique" | "distinct" | "distinctBy"
| "sum" | "avg" | "min" | "max" | "count"
| "where" | "select" | "orderBy" | "thenBy" | "takeWhile"
| "skipWhile" | "single" | "any" | "all"
| "innerJoin" | "leftJoin" | "crossJoin"
| "union" | "intersect" | "except"
)
|| matches!(method,
"columns" | "column" | "head" | "tail" | "mean" | "std"
| "describe" | "aggregate" | "group_by" | "index_by" | "indexBy"
| "simulate" | "toMat" | "to_mat"
)
|| matches!(method, "toArray")
|| matches!(method, "resample" | "between")
|| matches!(method,
"toFixed" | "toInt" | "toNumber" | "to_number" | "floor" | "ceil" | "round"
| "abs" | "sign" | "clamp"
)
|| matches!(method,
"toUpperCase" | "toLowerCase" | "trim" | "contains" | "startsWith"
| "endsWith" | "split" | "replace" | "substring" | "charAt"
| "padStart" | "padEnd" | "repeat" | "toString"
)
|| matches!(method, "keys" | "values" | "has" | "get" | "set" | "len")
|| matches!(method, "format")
|| matches!(method, "type")
}
pub(super) fn try_track_datatable_type(
&mut self,
type_ann: &shape_ast::ast::TypeAnnotation,
slot: u16,
is_local: bool,
) -> shape_ast::error::Result<()> {
use shape_ast::ast::TypeAnnotation;
if let TypeAnnotation::Generic { name, args } = type_ann {
if name == "Table" && args.len() == 1 {
let inner_name = match &args[0] {
TypeAnnotation::Reference(t) => Some(t.as_str()),
TypeAnnotation::Basic(t) => Some(t.as_str()),
_ => None,
};
if let Some(type_name) = inner_name {
let schema_id = self
.type_tracker
.schema_registry()
.get(type_name)
.map(|s| s.id);
if let Some(sid) = schema_id {
let info = crate::type_tracking::VariableTypeInfo::datatable(
sid,
type_name.to_string(),
);
if is_local {
self.type_tracker.set_local_type(slot, info);
} else {
self.type_tracker.set_binding_type(slot, info);
}
} else if type_name.len() == 1
&& type_name
.chars()
.next()
.map_or(false, |c| c.is_ascii_uppercase())
{
} else {
return Err(shape_ast::error::ShapeError::SemanticError {
message: format!(
"Unknown type '{}' in Table<{}> annotation",
type_name, type_name
),
location: None,
});
}
}
}
}
Ok(())
}
pub(super) fn is_row_view_variable(&self, name: &str) -> bool {
if let Some(local_idx) = self.resolve_local(name) {
if let Some(info) = self.type_tracker.get_local_type(local_idx) {
return info.is_row_view();
}
}
if let Some(&binding_idx) = self.module_bindings.get(name) {
if let Some(info) = self.type_tracker.get_binding_type(binding_idx) {
return info.is_row_view();
}
}
false
}
pub(super) fn get_row_view_field_names(&self, name: &str) -> Option<Vec<String>> {
let type_name = if let Some(local_idx) = self.resolve_local(name) {
self.type_tracker
.get_local_type(local_idx)
.and_then(|info| {
if info.is_row_view() {
info.type_name.clone()
} else {
None
}
})
} else if let Some(&binding_idx) = self.module_bindings.get(name) {
self.type_tracker
.get_binding_type(binding_idx)
.and_then(|info| {
if info.is_row_view() {
info.type_name.clone()
} else {
None
}
})
} else {
None
};
if let Some(tn) = type_name {
if let Some(schema) = self.type_tracker.schema_registry().get(&tn) {
return Some(schema.field_names().map(|n| n.to_string()).collect());
}
}
None
}
pub(super) fn try_resolve_row_view_column(
&self,
var_name: &str,
field_name: &str,
) -> Option<u32> {
if let Some(local_idx) = self.resolve_local(var_name) {
return self
.type_tracker
.get_row_view_column_id(local_idx, true, field_name);
}
if let Some(&binding_idx) = self.module_bindings.get(var_name) {
return self
.type_tracker
.get_row_view_column_id(binding_idx, false, field_name);
}
None
}
pub(super) fn row_view_field_opcode(&self, var_name: &str, field_name: &str) -> OpCode {
use shape_runtime::type_schema::FieldType;
let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
self.type_tracker
.get_local_type(local_idx)
.and_then(|info| info.type_name.clone())
} else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
self.type_tracker
.get_binding_type(binding_idx)
.and_then(|info| info.type_name.clone())
} else {
None
};
if let Some(type_name) = type_name {
if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
if let Some(field) = schema.get_field(field_name) {
return match field.field_type {
FieldType::F64 => OpCode::LoadColF64,
FieldType::I64 | FieldType::Timestamp => OpCode::LoadColI64,
FieldType::Bool => OpCode::LoadColBool,
FieldType::String => OpCode::LoadColStr,
_ => OpCode::LoadColF64, };
}
}
}
OpCode::LoadColF64 }
pub(super) fn resolve_row_view_field_numeric_type(
&self,
var_name: &str,
field_name: &str,
) -> Option<crate::type_tracking::NumericType> {
use crate::type_tracking::NumericType;
use shape_runtime::type_schema::FieldType;
let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
self.type_tracker
.get_local_type(local_idx)
.and_then(|info| info.type_name.clone())
} else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
self.type_tracker
.get_binding_type(binding_idx)
.and_then(|info| info.type_name.clone())
} else {
None
};
if let Some(type_name) = type_name {
if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
if let Some(field) = schema.get_field(field_name) {
return match field.field_type {
FieldType::F64 => Some(NumericType::Number),
FieldType::I64 | FieldType::Timestamp => Some(NumericType::Int),
FieldType::Decimal => Some(NumericType::Decimal),
_ => None,
};
}
}
}
None
}
pub(super) fn type_annotation_to_field_type(
ann: &shape_ast::ast::TypeAnnotation,
) -> shape_runtime::type_schema::FieldType {
use shape_ast::ast::TypeAnnotation;
use shape_runtime::type_schema::FieldType;
match ann {
TypeAnnotation::Basic(s) => match s.as_str() {
"number" | "float" | "f64" | "f32" => FieldType::F64,
"i8" => FieldType::I8,
"u8" => FieldType::U8,
"i16" => FieldType::I16,
"u16" => FieldType::U16,
"i32" => FieldType::I32,
"u32" => FieldType::U32,
"u64" => FieldType::U64,
"int" | "i64" | "integer" | "isize" | "usize" | "byte" | "char" => FieldType::I64,
"string" | "str" => FieldType::String,
"decimal" => FieldType::Decimal,
"bool" | "boolean" => FieldType::Bool,
"timestamp" => FieldType::Timestamp,
other => FieldType::Object(other.to_string()),
},
TypeAnnotation::Reference(s) => FieldType::Object(s.to_string()),
TypeAnnotation::Array(inner) => {
FieldType::Array(Box::new(Self::type_annotation_to_field_type(inner)))
}
TypeAnnotation::Generic { name, args } => match name.as_str() {
"Option" if args.len() == 1 => FieldType::Option(Box::new(
Self::type_annotation_to_field_type(&args[0]),
)),
"HashMap" | "Map" if args.len() == 2 => FieldType::HashMap {
key: Box::new(Self::type_annotation_to_field_type(&args[0])),
value: Box::new(Self::type_annotation_to_field_type(&args[1])),
},
"Set" if args.len() == 1 => FieldType::Set(Box::new(
Self::type_annotation_to_field_type(&args[0]),
)),
"HashMap" | "Map" | "Result" | "Set" | "Option" => FieldType::Any,
other => FieldType::Object(other.to_string()),
},
TypeAnnotation::Tuple(_) => FieldType::Any,
TypeAnnotation::Object(_) => FieldType::Any,
TypeAnnotation::Function { .. } => FieldType::Any,
TypeAnnotation::Union(_) => FieldType::Any,
TypeAnnotation::Intersection(_) => FieldType::Any,
TypeAnnotation::Void => FieldType::Any,
TypeAnnotation::Never => FieldType::Any,
TypeAnnotation::Null => FieldType::Any,
TypeAnnotation::Undefined => FieldType::Any,
TypeAnnotation::Dyn(_) => FieldType::Any,
}
}
pub(super) fn eval_annotation_arg(expr: &shape_ast::ast::Expr) -> Option<String> {
use shape_ast::ast::{Expr, Literal};
match expr {
Expr::Literal(Literal::String(s), _) => Some(s.clone()),
Expr::Literal(Literal::Number(n), _) => Some(n.to_string()),
Expr::Literal(Literal::Int(i), _) => Some(i.to_string()),
Expr::Literal(Literal::Bool(b), _) => Some(b.to_string()),
_ => None,
}
}
pub(super) fn get_table_schema_id(
&self,
type_ann: &shape_ast::ast::TypeAnnotation,
) -> Option<u16> {
use shape_ast::ast::TypeAnnotation;
if let TypeAnnotation::Generic { name, args } = type_ann {
if name == "Table" && args.len() == 1 {
let inner_name = match &args[0] {
TypeAnnotation::Basic(t) => Some(t.as_str()),
TypeAnnotation::Reference(t) => Some(t.as_str()),
_ => None,
};
if let Some(type_name) = inner_name {
return self
.type_tracker
.schema_registry()
.get(type_name)
.map(|s| s.id as u16);
}
}
}
None
}
pub(super) fn push_drop_scope(&mut self) {
self.drop_locals.push(Vec::new());
self.ownership_drop_locals.push(Vec::new());
self.shared_drop_locals.push(Vec::new());
}
pub(super) fn pop_drop_scope(&mut self) -> Result<()> {
let ownership_locals = self.ownership_drop_locals.pop().unwrap_or_default();
if ownership_moves_enabled() {
for local_idx in ownership_locals.into_iter().rev() {
if self.slot_is_boxed(local_idx) || self.slot_is_shared(local_idx) {
continue;
}
if self.local_drop_kind(local_idx).is_some() {
continue;
}
self.emit(Instruction::new(
OpCode::DropLocal,
Some(Operand::Local(local_idx)),
));
}
}
if let Some(shared_locals) = self.shared_drop_locals.pop() {
for local_idx in shared_locals.into_iter().rev() {
self.emit(Instruction::new(
OpCode::DropSharedLocal,
Some(Operand::Local(local_idx)),
));
}
}
if let Some(locals) = self.drop_locals.pop() {
for (local_idx, is_async) in locals.into_iter().rev() {
self.emit_drop_call_for_local(local_idx, is_async);
}
}
Ok(())
}
pub(super) fn track_ownership_drop_local(&mut self, local_idx: u16) {
if let Some(scope) = self.ownership_drop_locals.last_mut() {
scope.push(local_idx);
}
}
pub(super) fn slot_has_inline_scalar_hint(&self, local_idx: u16) -> bool {
let Some(hint) = self
.type_tracker
.get_local_type(local_idx)
.and_then(|info| info.storage_hint)
else {
return false;
};
hint.is_numeric_family() || matches!(hint, StorageHint::Bool)
}
pub(super) fn slot_is_heap_backed_owned(&self, local_idx: u16) -> bool {
use crate::type_tracking::BindingStorageClass;
match self.mir_storage_class_for_slot(local_idx) {
Some(BindingStorageClass::UniqueHeap) => true,
Some(BindingStorageClass::Direct) => !self.slot_has_inline_scalar_hint(local_idx),
_ => false,
}
}
pub(super) fn binding_slot_needs_ownership_drop(
&self,
local_idx: u16,
var_kind: shape_ast::ast::VarKind,
) -> bool {
use crate::type_tracking::BindingStorageClass;
if self.slot_is_boxed(local_idx) || self.slot_is_shared(local_idx) {
return false;
}
match self.mir_storage_class_for_slot(local_idx) {
Some(BindingStorageClass::UniqueHeap) => true,
Some(BindingStorageClass::Direct) => {
matches!(
var_kind,
shape_ast::ast::VarKind::Let | shape_ast::ast::VarKind::Const
) && !self.slot_has_inline_scalar_hint(local_idx)
}
_ => false,
}
}
fn emit_drop_call_for_local(&mut self, local_idx: u16, is_async: bool) {
let type_name_opt = self
.type_tracker
.get_local_type(local_idx)
.and_then(|info| info.type_name.clone());
self.emit(Instruction::new(
OpCode::LoadLocal,
Some(Operand::Local(local_idx)),
));
let opcode = if is_async {
OpCode::DropCallAsync
} else {
OpCode::DropCall
};
if let Some(type_name) = type_name_opt {
let str_idx = self.program.add_string(type_name);
self.emit(Instruction::new(opcode, Some(Operand::Property(str_idx))));
} else {
self.emit(Instruction::simple(opcode));
}
}
pub(super) fn emit_drop_call_for_module_binding(&mut self, binding_idx: u16, is_async: bool) {
let type_name_opt = self
.type_tracker
.get_binding_type(binding_idx)
.and_then(|info| info.type_name.clone());
self.emit(Instruction::new(
OpCode::LoadModuleBinding,
Some(Operand::ModuleBinding(binding_idx)),
));
let opcode = if is_async {
OpCode::DropCallAsync
} else {
OpCode::DropCall
};
if let Some(type_name) = type_name_opt {
let str_idx = self.program.add_string(type_name);
self.emit(Instruction::new(opcode, Some(Operand::Property(str_idx))));
} else {
self.emit(Instruction::simple(opcode));
}
}
pub(super) fn track_drop_local(&mut self, local_idx: u16, is_async: bool) {
if let Some(scope) = self.drop_locals.last_mut() {
scope.push((local_idx, is_async));
}
}
pub(super) fn local_drop_kind(&self, local_idx: u16) -> Option<DropKind> {
let type_name = self
.type_tracker
.get_local_type(local_idx)
.and_then(|info| info.type_name.as_ref())?;
self.drop_type_info.get(type_name).copied()
}
pub(super) fn annotation_drop_kind(&self, type_ann: &TypeAnnotation) -> Option<DropKind> {
let type_name = Self::tracked_type_name_from_annotation(type_ann)?;
self.drop_type_info.get(&type_name).copied()
}
pub(super) fn emit_drops_for_early_exit(&mut self, scopes_to_exit: usize) -> Result<()> {
let total = self.drop_locals.len();
if scopes_to_exit > total {
return Ok(());
}
if ownership_moves_enabled() {
let ownership_total = self.ownership_drop_locals.len();
if scopes_to_exit <= ownership_total {
let mut ownership_scopes: Vec<Vec<u16>> = Vec::new();
for i in (ownership_total - scopes_to_exit..ownership_total).rev() {
let locals = self
.ownership_drop_locals
.get(i)
.cloned()
.unwrap_or_default();
ownership_scopes.push(locals);
}
for locals in ownership_scopes {
for local_idx in locals.into_iter().rev() {
if self.slot_is_boxed(local_idx) || self.slot_is_shared(local_idx) {
continue;
}
if self.local_drop_kind(local_idx).is_some() {
continue;
}
self.emit(Instruction::new(
OpCode::DropLocal,
Some(Operand::Local(local_idx)),
));
}
}
}
}
{
let shared_total = self.shared_drop_locals.len();
if scopes_to_exit <= shared_total {
let mut shared_scopes: Vec<Vec<u16>> = Vec::new();
for i in (shared_total - scopes_to_exit..shared_total).rev() {
let locals = self.shared_drop_locals.get(i).cloned().unwrap_or_default();
shared_scopes.push(locals);
}
for locals in shared_scopes {
for local_idx in locals.into_iter().rev() {
self.emit(Instruction::new(
OpCode::DropSharedLocal,
Some(Operand::Local(local_idx)),
));
}
}
}
}
let mut scopes: Vec<Vec<(u16, bool)>> = Vec::new();
for i in (total - scopes_to_exit..total).rev() {
let locals = self.drop_locals.get(i).cloned().unwrap_or_default();
scopes.push(locals);
}
for locals in scopes {
for (local_idx, is_async) in locals.into_iter().rev() {
self.emit_drop_call_for_local(local_idx, is_async);
}
}
Ok(())
}
pub(super) fn track_drop_module_binding(&mut self, binding_idx: u16, is_async: bool) {
self.drop_module_bindings.push((binding_idx, is_async));
}
}
#[inline]
pub(crate) fn owned_mutable_typed_load_opcode(
kind: shape_value::v2::struct_layout::FieldKind,
) -> OpCode {
use shape_value::v2::struct_layout::FieldKind;
match kind {
FieldKind::I64 => OpCode::LoadOwnedMutableCaptureI64,
FieldKind::U64 => OpCode::LoadOwnedMutableCaptureU64,
FieldKind::F64 => OpCode::LoadOwnedMutableCaptureF64,
FieldKind::I32 => OpCode::LoadOwnedMutableCaptureI32,
FieldKind::U32 => OpCode::LoadOwnedMutableCaptureU32,
FieldKind::I16 => OpCode::LoadOwnedMutableCaptureI16,
FieldKind::U16 => OpCode::LoadOwnedMutableCaptureU16,
FieldKind::I8 => OpCode::LoadOwnedMutableCaptureI8,
FieldKind::U8 => OpCode::LoadOwnedMutableCaptureU8,
FieldKind::Bool => OpCode::LoadOwnedMutableCaptureBool,
FieldKind::Ptr => OpCode::LoadOwnedMutableCapturePtr,
}
}
#[inline]
pub(crate) fn owned_mutable_typed_store_opcode(
kind: shape_value::v2::struct_layout::FieldKind,
) -> OpCode {
use shape_value::v2::struct_layout::FieldKind;
match kind {
FieldKind::I64 => OpCode::StoreOwnedMutableCaptureI64,
FieldKind::U64 => OpCode::StoreOwnedMutableCaptureU64,
FieldKind::F64 => OpCode::StoreOwnedMutableCaptureF64,
FieldKind::I32 => OpCode::StoreOwnedMutableCaptureI32,
FieldKind::U32 => OpCode::StoreOwnedMutableCaptureU32,
FieldKind::I16 => OpCode::StoreOwnedMutableCaptureI16,
FieldKind::U16 => OpCode::StoreOwnedMutableCaptureU16,
FieldKind::I8 => OpCode::StoreOwnedMutableCaptureI8,
FieldKind::U8 => OpCode::StoreOwnedMutableCaptureU8,
FieldKind::Bool => OpCode::StoreOwnedMutableCaptureBool,
FieldKind::Ptr => OpCode::StoreOwnedMutableCapturePtr,
}
}
#[inline]
pub(crate) fn shared_typed_load_opcode(
kind: shape_value::v2::struct_layout::FieldKind,
) -> OpCode {
use shape_value::v2::struct_layout::FieldKind;
match kind {
FieldKind::I64 => OpCode::LoadSharedCaptureI64,
FieldKind::U64 => OpCode::LoadSharedCaptureU64,
FieldKind::F64 => OpCode::LoadSharedCaptureF64,
FieldKind::I32 => OpCode::LoadSharedCaptureI32,
FieldKind::U32 => OpCode::LoadSharedCaptureU32,
FieldKind::I16 => OpCode::LoadSharedCaptureI16,
FieldKind::U16 => OpCode::LoadSharedCaptureU16,
FieldKind::I8 => OpCode::LoadSharedCaptureI8,
FieldKind::U8 => OpCode::LoadSharedCaptureU8,
FieldKind::Bool => OpCode::LoadSharedCaptureBool,
FieldKind::Ptr => OpCode::LoadSharedCapturePtr,
}
}
#[inline]
pub(crate) fn shared_typed_store_opcode(
kind: shape_value::v2::struct_layout::FieldKind,
) -> OpCode {
use shape_value::v2::struct_layout::FieldKind;
match kind {
FieldKind::I64 => OpCode::StoreSharedCaptureI64,
FieldKind::U64 => OpCode::StoreSharedCaptureU64,
FieldKind::F64 => OpCode::StoreSharedCaptureF64,
FieldKind::I32 => OpCode::StoreSharedCaptureI32,
FieldKind::U32 => OpCode::StoreSharedCaptureU32,
FieldKind::I16 => OpCode::StoreSharedCaptureI16,
FieldKind::U16 => OpCode::StoreSharedCaptureU16,
FieldKind::I8 => OpCode::StoreSharedCaptureI8,
FieldKind::U8 => OpCode::StoreSharedCaptureU8,
FieldKind::Bool => OpCode::StoreSharedCaptureBool,
FieldKind::Ptr => OpCode::StoreSharedCapturePtr,
}
}
#[inline]
pub(crate) fn storage_hint_to_field_kind(
hint: StorageHint,
) -> Option<shape_value::v2::struct_layout::FieldKind> {
use shape_value::v2::struct_layout::FieldKind;
Some(match hint {
StorageHint::Float64 => FieldKind::F64,
StorageHint::Int64 => FieldKind::I64,
StorageHint::UInt64 => FieldKind::U64,
StorageHint::Int32 => FieldKind::I32,
StorageHint::UInt32 => FieldKind::U32,
StorageHint::Int16 => FieldKind::I16,
StorageHint::UInt16 => FieldKind::U16,
StorageHint::Int8 => FieldKind::I8,
StorageHint::UInt8 => FieldKind::U8,
StorageHint::IntSize | StorageHint::UIntSize => return None,
StorageHint::Bool => FieldKind::Bool,
StorageHint::Float32 | StorageHint::Char => return None,
StorageHint::String => return None,
StorageHint::StringV2 | StorageHint::DecimalV2 => return None,
StorageHint::NullableFloat64
| StorageHint::NullableInt8
| StorageHint::NullableUInt8
| StorageHint::NullableInt16
| StorageHint::NullableUInt16
| StorageHint::NullableInt32
| StorageHint::NullableUInt32
| StorageHint::NullableInt64
| StorageHint::NullableUInt64
| StorageHint::NullableIntSize
| StorageHint::NullableUIntSize => return None,
StorageHint::Ptr(_) => return None,
StorageHint::Null => return None,
})
}
#[cfg(debug_assertions)]
pub(crate) mod typed_emit_metrics {
use crate::type_tracking::StorageHint;
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
static CATEGORY_COUNTERS: OnceLock<Mutex<HashMap<&'static str, u64>>> = OnceLock::new();
static JOINT_COUNTERS: OnceLock<Mutex<HashMap<(&'static str, &'static str), u64>>> =
OnceLock::new();
pub(crate) fn storage_hint_label(hint: StorageHint) -> &'static str {
match hint {
StorageHint::Float64 => "f64",
StorageHint::NullableFloat64 => "nullable_f64",
StorageHint::Int8 => "i8",
StorageHint::NullableInt8 => "nullable_i8",
StorageHint::UInt8 => "u8",
StorageHint::NullableUInt8 => "nullable_u8",
StorageHint::Int16 => "i16",
StorageHint::NullableInt16 => "nullable_i16",
StorageHint::UInt16 => "u16",
StorageHint::NullableUInt16 => "nullable_u16",
StorageHint::Int32 => "i32",
StorageHint::NullableInt32 => "nullable_i32",
StorageHint::UInt32 => "u32",
StorageHint::NullableUInt32 => "nullable_u32",
StorageHint::Int64 => "i64",
StorageHint::NullableInt64 => "nullable_i64",
StorageHint::UInt64 => "u64",
StorageHint::NullableUInt64 => "nullable_u64",
StorageHint::IntSize => "isize",
StorageHint::NullableIntSize => "nullable_isize",
StorageHint::UIntSize => "usize",
StorageHint::NullableUIntSize => "nullable_usize",
StorageHint::Bool => "bool",
StorageHint::Float32 => "f32",
StorageHint::Char => "char",
StorageHint::String => "string",
StorageHint::StringV2 => "string_v2",
StorageHint::DecimalV2 => "decimal_v2",
StorageHint::Ptr(_) => "ptr",
StorageHint::Null => "null",
}
}
pub(crate) fn record_polymorphic_fallback(category: &'static str, hint: StorageHint) {
let cat = CATEGORY_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(mut g) = cat.lock() {
*g.entry(category).or_insert(0) += 1;
}
let joint = JOINT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(mut g) = joint.lock() {
*g.entry((category, storage_hint_label(hint))).or_insert(0) += 1;
}
}
pub fn snapshot() -> Vec<(&'static str, u64)> {
let counters = CATEGORY_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
let g = counters.lock().expect("typed_emit_metrics lock poisoned");
let mut v: Vec<_> = g.iter().map(|(k, v)| (*k, *v)).collect();
v.sort_by_key(|(k, _)| *k);
v
}
pub fn snapshot_joint() -> Vec<((&'static str, &'static str), u64)> {
let counters = JOINT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
let g = counters.lock().expect("typed_emit_metrics joint lock poisoned");
let mut v: Vec<_> = g.iter().map(|(k, v)| (*k, *v)).collect();
v.sort_by_key(|(k, _)| *k);
v
}
pub fn reset() {
let cat = CATEGORY_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(mut g) = cat.lock() {
g.clear();
}
let joint = JOINT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(mut g) = joint.lock() {
g.clear();
}
}
}
#[cfg(not(debug_assertions))]
pub(crate) mod typed_emit_metrics {
use crate::type_tracking::StorageHint;
pub(crate) fn storage_hint_label(_hint: StorageHint) -> &'static str {
""
}
pub(crate) fn record_polymorphic_fallback(_category: &'static str, _hint: StorageHint) {}
pub fn snapshot() -> Vec<(&'static str, u64)> {
Vec::new()
}
pub fn snapshot_joint() -> Vec<((&'static str, &'static str), u64)> {
Vec::new()
}
pub fn reset() {}
}
#[inline]
pub(crate) fn primitive_type_name_to_storage_hint(name: &str) -> Option<StorageHint> {
Some(match name {
"int" | "Int" | "i64" => StorageHint::Int64,
"u64" | "UInt" => StorageHint::UInt64,
"i8" => StorageHint::Int8,
"u8" => StorageHint::UInt8,
"i16" => StorageHint::Int16,
"u16" => StorageHint::UInt16,
"i32" => StorageHint::Int32,
"u32" => StorageHint::UInt32,
"isize" => StorageHint::IntSize,
"usize" => StorageHint::UIntSize,
"number" | "Number" | "f32" | "f64" => StorageHint::Float64,
"bool" | "Bool" => StorageHint::Bool,
"string" | "String" => StorageHint::String,
_ => return None,
})
}
#[inline]
pub(crate) fn typed_load_local_opcode(hint: StorageHint) -> Option<OpCode> {
use shape_value::v2::struct_layout::FieldKind;
Some(match storage_hint_to_field_kind(hint)? {
FieldKind::I64 => OpCode::LoadLocalI64,
FieldKind::U64 => OpCode::LoadLocalU64,
FieldKind::F64 => OpCode::LoadLocalF64,
FieldKind::I32 => OpCode::LoadLocalI32,
FieldKind::U32 => OpCode::LoadLocalU32,
FieldKind::I16 => OpCode::LoadLocalI16,
FieldKind::U16 => OpCode::LoadLocalU16,
FieldKind::I8 => OpCode::LoadLocalI8,
FieldKind::U8 => OpCode::LoadLocalU8,
FieldKind::Bool => OpCode::LoadLocalBool,
FieldKind::Ptr => OpCode::LoadLocalPtr,
})
}
#[inline]
pub(crate) fn typed_store_local_opcode(hint: StorageHint) -> Option<OpCode> {
use shape_value::v2::struct_layout::FieldKind;
Some(match storage_hint_to_field_kind(hint)? {
FieldKind::I64 => OpCode::StoreLocalI64,
FieldKind::U64 => OpCode::StoreLocalU64,
FieldKind::F64 => OpCode::StoreLocalF64,
FieldKind::I32 => OpCode::StoreLocalI32,
FieldKind::U32 => OpCode::StoreLocalU32,
FieldKind::I16 => OpCode::StoreLocalI16,
FieldKind::U16 => OpCode::StoreLocalU16,
FieldKind::I8 => OpCode::StoreLocalI8,
FieldKind::U8 => OpCode::StoreLocalU8,
FieldKind::Bool => OpCode::StoreLocalBool,
FieldKind::Ptr => OpCode::StoreLocalPtr,
})
}
#[inline]
pub(crate) fn typed_load_module_binding_opcode(hint: StorageHint) -> Option<OpCode> {
use shape_value::v2::struct_layout::FieldKind;
Some(match storage_hint_to_field_kind(hint)? {
FieldKind::I64 => OpCode::LoadModuleBindingI64,
FieldKind::U64 => OpCode::LoadModuleBindingU64,
FieldKind::F64 => OpCode::LoadModuleBindingF64,
FieldKind::I32 => OpCode::LoadModuleBindingI32,
FieldKind::U32 => OpCode::LoadModuleBindingU32,
FieldKind::I16 => OpCode::LoadModuleBindingI16,
FieldKind::U16 => OpCode::LoadModuleBindingU16,
FieldKind::I8 => OpCode::LoadModuleBindingI8,
FieldKind::U8 => OpCode::LoadModuleBindingU8,
FieldKind::Bool => OpCode::LoadModuleBindingBool,
FieldKind::Ptr => OpCode::LoadModuleBindingPtr,
})
}
#[inline]
pub(crate) fn typed_store_module_binding_opcode(hint: StorageHint) -> Option<OpCode> {
use shape_value::v2::struct_layout::FieldKind;
Some(match storage_hint_to_field_kind(hint)? {
FieldKind::I64 => OpCode::StoreModuleBindingI64,
FieldKind::U64 => OpCode::StoreModuleBindingU64,
FieldKind::F64 => OpCode::StoreModuleBindingF64,
FieldKind::I32 => OpCode::StoreModuleBindingI32,
FieldKind::U32 => OpCode::StoreModuleBindingU32,
FieldKind::I16 => OpCode::StoreModuleBindingI16,
FieldKind::U16 => OpCode::StoreModuleBindingU16,
FieldKind::I8 => OpCode::StoreModuleBindingI8,
FieldKind::U8 => OpCode::StoreModuleBindingU8,
FieldKind::Bool => OpCode::StoreModuleBindingBool,
FieldKind::Ptr => OpCode::StoreModuleBindingPtr,
})
}
#[inline]
pub(crate) fn typed_return_value_opcode(hint: StorageHint) -> Option<OpCode> {
use shape_value::v2::struct_layout::FieldKind;
Some(match storage_hint_to_field_kind(hint)? {
FieldKind::I64 => OpCode::ReturnValueI64,
FieldKind::U64 => OpCode::ReturnValueU64,
FieldKind::F64 => OpCode::ReturnValueF64,
FieldKind::I32 => OpCode::ReturnValueI32,
FieldKind::U32 => OpCode::ReturnValueU32,
FieldKind::I16 => OpCode::ReturnValueI16,
FieldKind::U16 => OpCode::ReturnValueU16,
FieldKind::I8 => OpCode::ReturnValueI8,
FieldKind::U8 => OpCode::ReturnValueU8,
FieldKind::Bool => OpCode::ReturnValueBool,
FieldKind::Ptr => OpCode::ReturnValuePtr,
})
}
impl BytecodeCompiler {
pub(super) fn emit_load_local_for_hint(&mut self, slot: u16, hint: StorageHint) {
let opcode = typed_load_local_opcode(hint).unwrap_or_else(|| {
typed_emit_metrics::record_polymorphic_fallback("load_local", hint);
OpCode::LoadLocal
});
self.emit(Instruction::new(opcode, Some(Operand::Local(slot))));
}
pub(super) fn emit_store_local_for_hint(&mut self, slot: u16, hint: StorageHint) {
let opcode = typed_store_local_opcode(hint).unwrap_or_else(|| {
typed_emit_metrics::record_polymorphic_fallback("store_local", hint);
OpCode::StoreLocal
});
self.emit(Instruction::new(opcode, Some(Operand::Local(slot))));
}
pub(super) fn emit_load_module_binding_for_hint(
&mut self,
binding_idx: u16,
hint: StorageHint,
) {
let opcode = typed_load_module_binding_opcode(hint).unwrap_or_else(|| {
typed_emit_metrics::record_polymorphic_fallback("load_module_binding", hint);
OpCode::LoadModuleBinding
});
self.emit(Instruction::new(
opcode,
Some(Operand::ModuleBinding(binding_idx)),
));
}
pub(super) fn emit_store_module_binding_for_hint(
&mut self,
binding_idx: u16,
hint: StorageHint,
) {
let opcode = typed_store_module_binding_opcode(hint).unwrap_or_else(|| {
typed_emit_metrics::record_polymorphic_fallback("store_module_binding", hint);
OpCode::StoreModuleBinding
});
self.emit(Instruction::new(
opcode,
Some(Operand::ModuleBinding(binding_idx)),
));
}
pub(super) fn emit_return_value_for_hint(&mut self, hint: StorageHint) {
let opcode = typed_return_value_opcode(hint).unwrap_or_else(|| {
typed_emit_metrics::record_polymorphic_fallback("return_value", hint);
OpCode::ReturnValue
});
self.emit(Instruction::simple(opcode));
}
}
#[cfg(test)]
mod tests {
use super::super::BytecodeCompiler;
use crate::compiler::ParamPassMode;
use crate::type_tracking::BindingStorageClass;
use shape_ast::ast::{Expr, Span, TypeAnnotation};
use shape_runtime::type_schema::FieldType;
#[test]
fn test_type_annotation_to_field_type_array_recursive() {
let ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Basic("int".to_string())));
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(ft, FieldType::Array(Box::new(FieldType::I64)));
}
#[test]
fn test_type_annotation_to_field_type_optional() {
let ann = TypeAnnotation::Generic {
name: "Option".into(),
args: vec![TypeAnnotation::Basic("int".to_string())],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_ne!(ft, FieldType::Any, "Option<int> must resolve structurally");
}
#[test]
fn test_type_annotation_to_field_type_generic_hashmap() {
let ann = TypeAnnotation::Generic {
name: "HashMap".into(),
args: vec![
TypeAnnotation::Basic("string".to_string()),
TypeAnnotation::Basic("int".to_string()),
],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_ne!(ft, FieldType::Any, "HashMap<string,int> must resolve structurally");
}
#[test]
fn test_type_annotation_to_field_type_generic_user_struct() {
let ann = TypeAnnotation::Generic {
name: "MyContainer".into(),
args: vec![TypeAnnotation::Basic("string".to_string())],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(ft, FieldType::Object("MyContainer".to_string()));
}
#[test]
fn test_flexible_storage_promotion_is_monotonic() {
let mut compiler = BytecodeCompiler::new();
compiler.push_scope();
let slot = compiler.declare_local("value").expect("declare local");
compiler.type_tracker.set_local_binding_semantics(
slot,
BytecodeCompiler::binding_semantics_for_ownership_class(
crate::type_tracking::BindingOwnershipClass::Flexible,
),
);
compiler.promote_flexible_binding_storage_for_slot(
slot,
true,
BindingStorageClass::UniqueHeap,
);
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::UniqueHeap)
);
compiler.promote_flexible_binding_storage_for_slot(slot, true, BindingStorageClass::Direct);
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::UniqueHeap)
);
compiler.promote_flexible_binding_storage_for_slot(
slot,
true,
BindingStorageClass::SharedCow,
);
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::SharedCow)
);
}
#[test]
fn test_escape_planner_marks_array_element_identifier_as_unique_heap() {
let mut compiler = BytecodeCompiler::new();
compiler.push_scope();
let slot = compiler.declare_local("value").expect("declare local");
compiler.type_tracker.set_local_binding_semantics(
slot,
BytecodeCompiler::binding_semantics_for_ownership_class(
crate::type_tracking::BindingOwnershipClass::Flexible,
),
);
let expr = Expr::Array(
vec![Expr::Identifier("value".to_string(), Span::DUMMY)],
Span::DUMMY,
);
compiler.plan_flexible_binding_escape_from_expr(&expr);
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::UniqueHeap)
);
}
#[test]
fn test_escape_planner_marks_if_branch_identifier_as_unique_heap() {
let mut compiler = BytecodeCompiler::new();
compiler.push_scope();
let slot = compiler.declare_local("value").expect("declare local");
compiler.type_tracker.set_local_binding_semantics(
slot,
BytecodeCompiler::binding_semantics_for_ownership_class(
crate::type_tracking::BindingOwnershipClass::Flexible,
),
);
let expr = Expr::If(
Box::new(shape_ast::ast::IfExpr {
condition: Box::new(Expr::Literal(
shape_ast::ast::Literal::Bool(true),
Span::DUMMY,
)),
then_branch: Box::new(Expr::Identifier("value".to_string(), Span::DUMMY)),
else_branch: None,
}),
Span::DUMMY,
);
compiler.plan_flexible_binding_escape_from_expr(&expr);
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::UniqueHeap)
);
}
#[test]
fn test_escape_planner_marks_async_let_rhs_identifier_as_unique_heap() {
let mut compiler = BytecodeCompiler::new();
compiler.push_scope();
let slot = compiler.declare_local("value").expect("declare local");
compiler.type_tracker.set_local_binding_semantics(
slot,
BytecodeCompiler::binding_semantics_for_ownership_class(
crate::type_tracking::BindingOwnershipClass::Flexible,
),
);
let expr = Expr::AsyncLet(
Box::new(shape_ast::ast::AsyncLetExpr {
name: "task".to_string(),
expr: Box::new(Expr::Identifier("value".to_string(), Span::DUMMY)),
span: Span::DUMMY,
}),
Span::DUMMY,
);
compiler.plan_flexible_binding_escape_from_expr(&expr);
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::UniqueHeap)
);
}
#[test]
fn test_call_args_mark_by_value_identifier_as_unique_heap() {
let mut compiler = BytecodeCompiler::new();
compiler.push_scope();
let slot = compiler.declare_local("value").expect("declare local");
compiler.type_tracker.set_local_binding_semantics(
slot,
BytecodeCompiler::binding_semantics_for_ownership_class(
crate::type_tracking::BindingOwnershipClass::Flexible,
),
);
compiler
.compile_call_args(&[Expr::Identifier("value".to_string(), Span::DUMMY)], None)
.expect("call args should compile");
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::UniqueHeap)
);
}
#[test]
fn test_call_args_leave_by_ref_identifier_storage_unchanged() {
let mut compiler = BytecodeCompiler::new();
compiler.push_scope();
let slot = compiler.declare_local("value").expect("declare local");
compiler.type_tracker.set_local_binding_semantics(
slot,
BytecodeCompiler::binding_semantics_for_ownership_class(
crate::type_tracking::BindingOwnershipClass::Flexible,
),
);
compiler
.compile_call_args(
&[Expr::Identifier("value".to_string(), Span::DUMMY)],
Some(&[ParamPassMode::ByRefShared]),
)
.expect("reference call args should compile");
assert_eq!(
compiler
.type_tracker
.get_local_binding_semantics(slot)
.map(|semantics| semantics.storage_class),
Some(BindingStorageClass::Deferred)
);
}
fn last_emitted_opcode(compiler: &BytecodeCompiler) -> crate::bytecode::OpCode {
compiler
.program
.instructions
.last()
.expect("compiler program is empty")
.opcode
}
#[test]
fn emit_binary_op_int_int_add_emits_add_int() {
use crate::bytecode::OpCode;
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use crate::type_tracking::NumericType;
use shape_ast::ast::BinaryOp;
let mut compiler = BytecodeCompiler::new();
let handled = emit_binary_op(
&mut compiler,
BinaryOp::Add,
BinOperandKind::Numeric(NumericType::Int),
BinOperandKind::Numeric(NumericType::Int),
)
.expect("emit_binary_op should succeed");
assert!(handled, "Add should be a handled op");
assert_eq!(last_emitted_opcode(&compiler), OpCode::AddInt);
}
#[test]
fn emit_binary_op_number_number_add_emits_add_number() {
use crate::bytecode::OpCode;
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use crate::type_tracking::NumericType;
use shape_ast::ast::BinaryOp;
let mut compiler = BytecodeCompiler::new();
let handled = emit_binary_op(
&mut compiler,
BinaryOp::Add,
BinOperandKind::Numeric(NumericType::Number),
BinOperandKind::Numeric(NumericType::Number),
)
.expect("emit_binary_op should succeed");
assert!(handled);
assert_eq!(last_emitted_opcode(&compiler), OpCode::AddNumber);
}
#[test]
fn emit_binary_op_number_number_mul_emits_mul_number() {
use crate::bytecode::OpCode;
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use crate::type_tracking::NumericType;
use shape_ast::ast::BinaryOp;
let mut compiler = BytecodeCompiler::new();
emit_binary_op(
&mut compiler,
BinaryOp::Mul,
BinOperandKind::Numeric(NumericType::Number),
BinOperandKind::Numeric(NumericType::Number),
)
.expect("emit_binary_op should succeed");
assert_eq!(last_emitted_opcode(&compiler), OpCode::MulNumber);
}
#[test]
fn emit_binary_op_int_int_cmp_emits_typed_cmp_opcodes() {
use crate::bytecode::OpCode;
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use crate::type_tracking::NumericType;
use shape_ast::ast::BinaryOp;
let cases = [
(BinaryOp::Greater, OpCode::GtInt),
(BinaryOp::Less, OpCode::LtInt),
(BinaryOp::GreaterEq, OpCode::GteInt),
(BinaryOp::LessEq, OpCode::LteInt),
(BinaryOp::Equal, OpCode::EqInt),
(BinaryOp::NotEqual, OpCode::NeqInt),
];
for (op, expected) in cases {
let mut compiler = BytecodeCompiler::new();
emit_binary_op(
&mut compiler,
op,
BinOperandKind::Numeric(NumericType::Int),
BinOperandKind::Numeric(NumericType::Int),
)
.expect("emit_binary_op should succeed");
assert_eq!(
last_emitted_opcode(&compiler),
expected,
"wrong opcode for Int {:?}",
op
);
}
}
#[test]
fn emit_binary_op_decimal_decimal_emits_typed_decimal_opcodes() {
use crate::bytecode::OpCode;
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use crate::type_tracking::NumericType;
use shape_ast::ast::BinaryOp;
let cases = [
(BinaryOp::Add, OpCode::AddDecimal),
(BinaryOp::Sub, OpCode::SubDecimal),
(BinaryOp::Mul, OpCode::MulDecimal),
(BinaryOp::Div, OpCode::DivDecimal),
(BinaryOp::Equal, OpCode::EqDecimal),
];
for (op, expected) in cases {
let mut compiler = BytecodeCompiler::new();
emit_binary_op(
&mut compiler,
op,
BinOperandKind::Numeric(NumericType::Decimal),
BinOperandKind::Numeric(NumericType::Decimal),
)
.expect("emit_binary_op should succeed");
assert_eq!(
last_emitted_opcode(&compiler),
expected,
"wrong opcode for Decimal {:?}",
op
);
}
}
#[test]
fn emit_binary_op_unknown_operands_return_false() {
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use crate::type_tracking::NumericType;
use shape_ast::ast::BinaryOp;
let cases: &[(BinaryOp, BinOperandKind, BinOperandKind)] = &[
(
BinaryOp::Add,
BinOperandKind::Numeric(NumericType::Int),
BinOperandKind::Unknown,
),
(
BinaryOp::Add,
BinOperandKind::Unknown,
BinOperandKind::Unknown,
),
(
BinaryOp::Equal,
BinOperandKind::Unknown,
BinOperandKind::Unknown,
),
(
BinaryOp::Add,
BinOperandKind::Numeric(NumericType::Int),
BinOperandKind::Numeric(NumericType::Number),
),
(
BinaryOp::Equal,
BinOperandKind::Bool,
BinOperandKind::Bool,
),
];
for (op, lhs, rhs) in cases {
let mut compiler = BytecodeCompiler::new();
let handled = emit_binary_op(&mut compiler, *op, *lhs, *rhs)
.expect("emit_binary_op should succeed");
assert!(
!handled,
"{:?} with {:?},{:?} must return Ok(false) post-Phase-2",
op, lhs, rhs
);
assert!(
compiler.program.instructions.is_empty(),
"no instruction must be emitted for {:?} with {:?},{:?}",
op, lhs, rhs
);
}
}
#[test]
fn emit_binary_op_string_string_add_emits_string_concat_typed() {
use crate::bytecode::OpCode;
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use shape_ast::ast::BinaryOp;
let mut compiler = BytecodeCompiler::new();
emit_binary_op(
&mut compiler,
BinaryOp::Add,
BinOperandKind::String,
BinOperandKind::String,
)
.expect("emit_binary_op should succeed");
assert_eq!(last_emitted_opcode(&compiler), OpCode::StringConcatTyped);
}
#[test]
fn emit_binary_op_returns_false_for_unsupported_ops() {
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use shape_ast::ast::BinaryOp;
let unsupported = [
BinaryOp::And,
BinaryOp::Or,
BinaryOp::BitAnd,
BinaryOp::BitOr,
BinaryOp::BitXor,
BinaryOp::BitShl,
BinaryOp::BitShr,
BinaryOp::NullCoalesce,
BinaryOp::ErrorContext,
BinaryOp::Pipe,
BinaryOp::FuzzyEqual,
BinaryOp::FuzzyGreater,
BinaryOp::FuzzyLess,
];
for op in unsupported {
let mut compiler = BytecodeCompiler::new();
let handled = emit_binary_op(
&mut compiler,
op,
BinOperandKind::Unknown,
BinOperandKind::Unknown,
)
.expect("emit_binary_op should succeed");
assert!(!handled, "{:?} should be unhandled (Ok(false))", op);
assert!(
compiler.program.instructions.is_empty(),
"{:?} must not emit any instruction on refusal",
op
);
}
}
#[test]
fn emit_binary_op_preserves_numeric_hint_for_arithmetic_only() {
use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
use crate::type_tracking::NumericType;
use shape_ast::ast::BinaryOp;
let mut compiler = BytecodeCompiler::new();
emit_binary_op(
&mut compiler,
BinaryOp::Add,
BinOperandKind::Numeric(NumericType::Int),
BinOperandKind::Numeric(NumericType::Int),
)
.expect("ok");
assert_eq!(compiler.last_expr_numeric_type, Some(NumericType::Int));
let mut compiler = BytecodeCompiler::new();
compiler.last_expr_numeric_type = Some(NumericType::Number);
emit_binary_op(
&mut compiler,
BinaryOp::Less,
BinOperandKind::Numeric(NumericType::Int),
BinOperandKind::Numeric(NumericType::Int),
)
.expect("ok");
assert_eq!(compiler.last_expr_numeric_type, None);
}
#[test]
fn from_numeric_maps_none_to_unknown() {
use crate::compiler::helpers::BinOperandKind;
use crate::type_tracking::NumericType;
assert_eq!(BinOperandKind::from_numeric(None), BinOperandKind::Unknown);
assert_eq!(
BinOperandKind::from_numeric(Some(NumericType::Int)),
BinOperandKind::Numeric(NumericType::Int)
);
assert_eq!(
BinOperandKind::from_numeric(Some(NumericType::Number)),
BinOperandKind::Numeric(NumericType::Number)
);
}
fn compile_opcodes(code: &str) -> Vec<crate::bytecode::OpCode> {
use shape_ast::parser::parse_program;
let program = parse_program(code).expect("parse program");
let mut compiler = BytecodeCompiler::new();
compiler.allow_internal_builtins = true;
let bc = compiler.compile(&program).expect("compile program");
bc.instructions.iter().map(|ins| ins.opcode).collect()
}
fn compile_opcodes_with_typed_bitwise(
enabled: bool,
code: &str,
) -> Vec<crate::bytecode::OpCode> {
super::super::helpers::with_typed_bitwise_flag(enabled, || compile_opcodes(code))
}
#[test]
fn r51c_int_operands_emit_typed_bitand() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_typed_bitwise(
true,
r#"
let a: int = 5
let b: int = 3
a & b
"#,
);
assert!(
ops.contains(&OpCode::BitAndInt),
"expected BitAndInt for int & int, got ops: {:?}",
ops
);
assert!(
!ops.contains(&OpCode::BitAnd),
"Dynamic BitAnd must not be emitted when typed path fires, ops: {:?}",
ops
);
}
#[test]
fn r51c_int_operands_emit_typed_bitor_bitxor() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_typed_bitwise(
true,
r#"
let a: int = 5
let b: int = 3
a | b
a ^ b
"#,
);
assert!(
ops.contains(&OpCode::BitOrInt),
"expected BitOrInt for int | int, got ops: {:?}",
ops
);
assert!(
ops.contains(&OpCode::BitXorInt),
"expected BitXorInt for int ^ int, got ops: {:?}",
ops
);
}
#[test]
fn r51c_int_operands_emit_typed_shifts() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_typed_bitwise(
true,
r#"
let a: int = 5
a << 2
a >> 1
"#,
);
assert!(
ops.contains(&OpCode::BitShlInt),
"expected BitShlInt for int << int, got ops: {:?}",
ops
);
assert!(
ops.contains(&OpCode::BitShrInt),
"expected BitShrInt for int >> int, got ops: {:?}",
ops
);
}
#[test]
fn r51c_int_operand_emits_typed_bitnot() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_typed_bitwise(
true,
r#"
let a: int = 5
~a
"#,
);
assert!(
ops.contains(&OpCode::BitNotInt),
"expected BitNotInt for ~int, got ops: {:?}",
ops
);
assert!(
!ops.contains(&OpCode::BitNot),
"Dynamic BitNot must not be emitted when typed path fires, ops: {:?}",
ops
);
}
#[test]
fn r51c_flag_off_falls_back_to_dynamic_bitand() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_typed_bitwise(
false,
r#"
let a: int = 5
let b: int = 3
a & b
"#,
);
assert!(
ops.contains(&OpCode::BitAnd),
"flag off: expected Dynamic BitAnd, got ops: {:?}",
ops
);
assert!(
!ops.contains(&OpCode::BitAndInt),
"flag off: typed BitAndInt must not be emitted, got ops: {:?}",
ops
);
}
#[test]
fn r51c_flag_off_falls_back_to_dynamic_bitnot() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_typed_bitwise(
false,
r#"
let a: int = 5
~a
"#,
);
assert!(
ops.contains(&OpCode::BitNot),
"flag off: expected Dynamic BitNot, got ops: {:?}",
ops
);
assert!(
!ops.contains(&OpCode::BitNotInt),
"flag off: typed BitNotInt must not be emitted, got ops: {:?}",
ops
);
}
#[test]
#[ignore]
fn r51c_untyped_param_falls_back_to_dynamic_bitand() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_typed_bitwise(
true,
r#"
fn masked(a) {
a & 15
}
masked(5)
"#,
);
assert!(
ops.contains(&OpCode::BitAnd),
"untyped param: expected Dynamic BitAnd, got ops: {:?}",
ops
);
assert!(
!ops.contains(&OpCode::BitAndInt),
"untyped param: typed BitAndInt must not be emitted, got ops: {:?}",
ops
);
}
#[test]
fn r51c_int_bitwise_eval_produces_expected_values() {
use crate::VMConfig;
use crate::executor::VirtualMachine;
use shape_ast::parser::parse_program;
let eval_int = |code: &str| -> i64 {
let program = parse_program(code).expect("parse");
let mut compiler = BytecodeCompiler::new();
compiler.allow_internal_builtins = true;
let bc = compiler.compile(&program).expect("compile");
let mut vm = VirtualMachine::new(VMConfig::default());
vm.load_program(bc);
vm.execute(None)
.expect("execute")
.as_i64()
.expect("i64 result")
};
assert_eq!(
super::super::helpers::with_typed_bitwise_flag(true, || {
eval_int("let a: int = 12\nlet b: int = 10\na & b")
}),
8,
"12 & 10 = 8"
);
assert_eq!(
super::super::helpers::with_typed_bitwise_flag(true, || {
eval_int("let a: int = 12\nlet b: int = 10\na | b")
}),
14,
"12 | 10 = 14"
);
assert_eq!(
super::super::helpers::with_typed_bitwise_flag(true, || {
eval_int("let a: int = 12\nlet b: int = 10\na ^ b")
}),
6,
"12 ^ 10 = 6"
);
assert_eq!(
super::super::helpers::with_typed_bitwise_flag(true, || {
eval_int("let a: int = 1\na << 4")
}),
16,
"1 << 4 = 16"
);
assert_eq!(
super::super::helpers::with_typed_bitwise_flag(true, || {
eval_int("let a: int = 32\na >> 2")
}),
8,
"32 >> 2 = 8"
);
assert_eq!(
super::super::helpers::with_typed_bitwise_flag(true, || {
eval_int("let a: int = 0\n~a")
}),
-1,
"~0 = -1 (two's complement)"
);
}
fn compile_opcodes_with_string_coerce_concat(
enabled: bool,
code: &str,
) -> Vec<crate::bytecode::OpCode> {
super::super::helpers::with_typed_string_coerce_concat_flag(enabled, || {
compile_opcodes(code)
})
}
#[test]
fn r55_string_plus_int_emits_string_concat_int() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_string_coerce_concat(
true,
r#"
fn concat_test() {
let s: string = "Cash: "
let c: int = 42
s + c
}
concat_test()
"#,
);
assert!(
ops.contains(&OpCode::StringConcatInt),
"expected StringConcatInt for string + int, got ops: {:?}",
ops
);
}
#[test]
fn r55_string_plus_number_emits_string_concat_number() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_string_coerce_concat(
true,
r#"
fn concat_test() {
let n: number = 3.14
"X: " + n
}
concat_test()
"#,
);
assert!(
ops.contains(&OpCode::StringConcatNumber),
"expected StringConcatNumber for string + number, got ops: {:?}",
ops
);
}
#[test]
fn r55_string_plus_bool_emits_string_concat_bool() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_string_coerce_concat(
true,
r#"
fn concat_test() {
let s: string = "flag: "
let b: bool = true
s + b
}
concat_test()
"#,
);
assert!(
ops.contains(&OpCode::StringConcatBool),
"expected StringConcatBool for string + bool, got ops: {:?}",
ops
);
}
#[test]
fn r55_string_plus_string_does_not_emit_r55_scalar_opcodes() {
use crate::bytecode::OpCode;
let ops = compile_opcodes_with_string_coerce_concat(
true,
r#"
fn concat_test() {
let b: string = "bar"
"foo" + b
}
concat_test()
"#,
);
assert!(
!ops.contains(&OpCode::StringConcatInt)
&& !ops.contains(&OpCode::StringConcatNumber)
&& !ops.contains(&OpCode::StringConcatBool),
"string+string must not emit any R5.5 typed scalar opcode, ops: {:?}",
ops
);
}
#[test]
fn r55_string_plus_scalar_runtime_values() {
use crate::VMConfig;
use crate::executor::VirtualMachine;
use shape_ast::parser::parse_program;
let eval_str = |code: &str| -> String {
super::super::helpers::with_typed_string_coerce_concat_flag(true, || {
let program = parse_program(code).expect("parse");
let mut compiler = BytecodeCompiler::new();
compiler.allow_internal_builtins = true;
let bc = compiler.compile(&program).expect("compile");
let mut vm = VirtualMachine::new(VMConfig::default());
vm.load_program(bc);
vm.execute(None)
.expect("execute")
.as_str()
.map(|s| s.to_string())
.expect("string result")
})
};
assert_eq!(
eval_str(
r#"
fn f() {
let c: int = 42
"Cash: " + c
}
f()
"#,
),
"Cash: 42",
"string + int"
);
assert_eq!(
eval_str(
r#"
fn f() {
let n: number = 3.14
"X: " + n
}
f()
"#,
),
"X: 3.14",
"string + number"
);
assert_eq!(
eval_str(
r#"
fn f() {
let n: number = 2.0
"whole: " + n
}
f()
"#,
),
"whole: 2",
"string + whole number formats without decimal"
);
assert_eq!(
eval_str(
r#"
fn f() {
let b: bool = true
"flag: " + b
}
f()
"#,
),
"flag: true",
"string + bool true"
);
assert_eq!(
eval_str(
r#"
fn f() {
let b: bool = false
"flag: " + b
}
f()
"#,
),
"flag: false",
"string + bool false"
);
}
fn metrics_test_lock() -> std::sync::MutexGuard<'static, ()> {
use std::sync::{Mutex, OnceLock};
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
#[test]
#[cfg(debug_assertions)]
fn test_e4_typed_emit_helpers_pin_typed_vs_polymorphic() {
use super::typed_emit_metrics;
use crate::bytecode::OpCode;
use crate::type_tracking::StorageHint;
let _guard = metrics_test_lock();
typed_emit_metrics::reset();
let mut compiler = BytecodeCompiler::new();
let start = compiler.program.instructions.len();
let cases: &[(&str, OpCode)] = &[
("load_i64", OpCode::LoadLocalI64),
("load_f64", OpCode::LoadLocalF64),
("load_bool", OpCode::LoadLocalBool),
("load_string_falls_back", OpCode::LoadLocal),
("load_nullable_int64_falls_back", OpCode::LoadLocal),
("store_i64", OpCode::StoreLocalI64),
("store_f64", OpCode::StoreLocalF64),
("store_bool", OpCode::StoreLocalBool),
("store_string_falls_back", OpCode::StoreLocal),
("store_nullable_int64_falls_back", OpCode::StoreLocal),
("load_mb_i32", OpCode::LoadModuleBindingI32),
("load_mb_bool", OpCode::LoadModuleBindingBool),
("store_mb_i32", OpCode::StoreModuleBindingI32),
("store_mb_bool", OpCode::StoreModuleBindingBool),
("ret_f64", OpCode::ReturnValueF64),
("ret_bool", OpCode::ReturnValueBool),
("ret_string_falls_back", OpCode::ReturnValue),
];
compiler.emit_load_local_for_hint(0, StorageHint::Int64);
compiler.emit_load_local_for_hint(0, StorageHint::Float64);
compiler.emit_load_local_for_hint(0, StorageHint::Bool);
compiler.emit_load_local_for_hint(0, StorageHint::String);
compiler.emit_load_local_for_hint(0, StorageHint::NullableInt64);
compiler.emit_store_local_for_hint(0, StorageHint::Int64);
compiler.emit_store_local_for_hint(0, StorageHint::Float64);
compiler.emit_store_local_for_hint(0, StorageHint::Bool);
compiler.emit_store_local_for_hint(0, StorageHint::String);
compiler.emit_store_local_for_hint(0, StorageHint::NullableInt64);
compiler.emit_load_module_binding_for_hint(0, StorageHint::Int32);
compiler.emit_load_module_binding_for_hint(0, StorageHint::Bool);
compiler.emit_store_module_binding_for_hint(0, StorageHint::Int32);
compiler.emit_store_module_binding_for_hint(0, StorageHint::Bool);
compiler.emit_return_value_for_hint(StorageHint::Float64);
compiler.emit_return_value_for_hint(StorageHint::Bool);
compiler.emit_return_value_for_hint(StorageHint::String);
let emitted: Vec<_> = compiler.program.instructions[start..]
.iter()
.map(|i| i.opcode)
.collect();
assert_eq!(emitted.len(), cases.len(), "case count mismatch");
for (i, (label, expected)) in cases.iter().enumerate() {
assert_eq!(
emitted[i], *expected,
"case '{label}' (idx {i}): expected {expected:?}, got {:?}",
emitted[i]
);
}
let snap: std::collections::HashMap<&'static str, u64> =
typed_emit_metrics::snapshot().into_iter().collect();
assert_eq!(snap.get("load_local").copied().unwrap_or(0), 2);
assert_eq!(snap.get("store_local").copied().unwrap_or(0), 2);
assert_eq!(snap.get("load_module_binding").copied().unwrap_or(0), 0);
assert_eq!(snap.get("store_module_binding").copied().unwrap_or(0), 0);
assert_eq!(snap.get("return_value").copied().unwrap_or(0), 1);
let joint: std::collections::HashMap<(&'static str, &'static str), u64> =
typed_emit_metrics::snapshot_joint().into_iter().collect();
assert_eq!(joint.get(&("load_local", "string")).copied().unwrap_or(0), 1);
assert_eq!(
joint.get(&("load_local", "nullable_i64")).copied().unwrap_or(0),
1
);
assert_eq!(joint.get(&("return_value", "string")).copied().unwrap_or(0), 1);
}
#[test]
fn test_e4_storage_hint_to_field_kind_policy() {
use crate::type_tracking::StorageHint;
use shape_value::v2::struct_layout::FieldKind;
assert_eq!(super::storage_hint_to_field_kind(StorageHint::Float64), Some(FieldKind::F64));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int64), Some(FieldKind::I64));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt64), Some(FieldKind::U64));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int32), Some(FieldKind::I32));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt32), Some(FieldKind::U32));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int16), Some(FieldKind::I16));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt16), Some(FieldKind::U16));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int8), Some(FieldKind::I8));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt8), Some(FieldKind::U8));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::Bool), Some(FieldKind::Bool));
assert_eq!(super::storage_hint_to_field_kind(StorageHint::String), None);
assert_eq!(super::storage_hint_to_field_kind(StorageHint::IntSize), None);
assert_eq!(super::storage_hint_to_field_kind(StorageHint::UIntSize), None);
assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableFloat64), None);
assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableInt64), None);
assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableInt32), None);
assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableUInt8), None);
assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableIntSize), None);
}
}
#[cfg(test)]
mod call_return_kind_tests {
use super::*;
use crate::mir::types::{
BasicBlock, BasicBlockId, MirConstant, MirFunction, Operand,
Place, Point, Rvalue, SlotId, StatementKind, Terminator,
TerminatorKind,
};
use shape_value::v2::ConcreteType;
fn mk_span() -> shape_ast::Span {
shape_ast::Span::new(0, 0)
}
fn mk_mir_with_call(callee_name: &str, dst_slot: u16) -> MirFunction {
let span = mk_span();
let bb1 = BasicBlock {
id: BasicBlockId(1),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Return,
span,
},
};
let bb0 = BasicBlock {
id: BasicBlockId(0),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Call {
func: Operand::Constant(MirConstant::Function(
callee_name.to_string(),
)),
args: vec![],
destination: Place::Local(SlotId(dst_slot)),
next: BasicBlockId(1),
},
span,
},
};
let n_locals = (dst_slot as u16) + 1;
MirFunction {
name: "caller".to_string(),
blocks: vec![bb0, bb1],
num_locals: n_locals,
param_slots: vec![],
param_reference_kinds: vec![],
local_types: vec![
crate::mir::types::LocalTypeInfo::Unknown;
n_locals as usize
],
span,
field_name_table: Default::default(),
local_struct_type_names: Default::default(),
local_typed_array_element_types: Default::default(),
local_declared_scalar_types: Default::default(),
}
}
#[test]
fn call_terminator_destination_stamped_from_resolver() {
let mir = mk_mir_with_call("divide", 3);
let resolver = |name: &str| -> Option<ConcreteType> {
if name == "divide" {
Some(ConcreteType::Result(
Box::new(ConcreteType::I64),
Box::new(ConcreteType::String),
))
} else {
None
}
};
let result = infer_top_level_concrete_types_from_mir_with_returns(
&mir,
Some(&resolver),
);
assert_eq!(
result[3],
ConcreteType::Result(
Box::new(ConcreteType::I64),
Box::new(ConcreteType::String),
),
"Call destination slot 3 should be stamped Result(I64,String)"
);
assert_eq!(result[0], ConcreteType::Void);
}
#[test]
fn call_terminator_no_resolver_leaves_void() {
let mir = mk_mir_with_call("divide", 3);
let result = infer_top_level_concrete_types_from_mir(&mir);
assert_eq!(result[3], ConcreteType::Void);
}
#[test]
fn call_terminator_resolver_returns_none_leaves_void() {
let mir = mk_mir_with_call("unknown_callee", 2);
let resolver = |_name: &str| -> Option<ConcreteType> { None };
let result = infer_top_level_concrete_types_from_mir_with_returns(
&mir,
Some(&resolver),
);
assert_eq!(result[2], ConcreteType::Void);
}
#[test]
fn call_terminator_propagates_through_move() {
let span = mk_span();
let bb1 = BasicBlock {
id: BasicBlockId(1),
statements: vec![crate::mir::types::MirStatement {
kind: StatementKind::Assign(
Place::Local(SlotId(5)),
Rvalue::Use(Operand::Move(Place::Local(SlotId(3)))),
),
span,
point: Point(0),
}],
terminator: Terminator {
kind: TerminatorKind::Return,
span,
},
};
let bb0 = BasicBlock {
id: BasicBlockId(0),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Call {
func: Operand::Constant(MirConstant::Function(
"divide".to_string(),
)),
args: vec![],
destination: Place::Local(SlotId(3)),
next: BasicBlockId(1),
},
span,
},
};
let mir = MirFunction {
name: "caller".to_string(),
blocks: vec![bb0, bb1],
num_locals: 6,
param_slots: vec![],
param_reference_kinds: vec![],
local_types: vec![
crate::mir::types::LocalTypeInfo::Unknown;
6
],
span,
field_name_table: Default::default(),
local_struct_type_names: Default::default(),
local_typed_array_element_types: Default::default(),
local_declared_scalar_types: Default::default(),
};
let resolver = |name: &str| -> Option<ConcreteType> {
if name == "divide" {
Some(ConcreteType::Result(
Box::new(ConcreteType::I64),
Box::new(ConcreteType::String),
))
} else {
None
}
};
let result = infer_top_level_concrete_types_from_mir_with_returns(
&mir,
Some(&resolver),
);
let expected = ConcreteType::Result(
Box::new(ConcreteType::I64),
Box::new(ConcreteType::String),
);
assert_eq!(result[3], expected, "Call destination slot stamped");
assert_eq!(result[5], expected, "Move destination propagated");
}
#[test]
fn empty_struct_literal_conduit_stamps_struct() {
let span = mk_span();
let bb0 = BasicBlock {
id: BasicBlockId(0),
statements: vec![
crate::mir::types::MirStatement {
kind: StatementKind::Assign(
Place::Local(SlotId(2)),
Rvalue::Aggregate(vec![]),
),
span,
point: Point(0),
},
crate::mir::types::MirStatement {
kind: StatementKind::ObjectStore {
container_slot: SlotId(2),
operands: vec![],
field_names: vec![],
schema_id: None,
},
span,
point: Point(0),
},
],
terminator: Terminator {
kind: TerminatorKind::Return,
span,
},
};
let mir = MirFunction {
name: "empty_struct".to_string(),
blocks: vec![bb0],
num_locals: 4,
param_slots: vec![],
param_reference_kinds: vec![],
local_types: vec![
crate::mir::types::LocalTypeInfo::Unknown;
4
],
span,
field_name_table: Default::default(),
local_struct_type_names: Default::default(),
local_typed_array_element_types: Default::default(),
local_declared_scalar_types: Default::default(),
};
let result = infer_top_level_concrete_types_from_mir(&mir);
assert!(
matches!(result[2], ConcreteType::Struct(_)),
"empty-operands ObjectStore must still stamp Struct, got {:?}",
result[2]
);
}
#[test]
fn trait_method_call_destination_stamps_from_method_returns_resolver() {
let span = mk_span();
let bb0 = BasicBlock {
id: BasicBlockId(0),
statements: vec![crate::mir::types::MirStatement {
kind: StatementKind::ObjectStore {
container_slot: SlotId(2),
operands: vec![],
field_names: vec![],
schema_id: None,
},
span,
point: Point(0),
}],
terminator: Terminator {
kind: TerminatorKind::Call {
func: Operand::Constant(MirConstant::Method(
"name".to_string(),
)),
args: vec![Operand::Move(Place::Local(SlotId(2)))],
destination: Place::Local(SlotId(3)),
next: BasicBlockId(1),
},
span,
},
};
let bb1 = BasicBlock {
id: BasicBlockId(1),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Return,
span,
},
};
let mut local_struct_type_names = std::collections::HashMap::new();
local_struct_type_names.insert(SlotId(2), "X".to_string());
let mir = MirFunction {
name: "smoke3".to_string(),
blocks: vec![bb0, bb1],
num_locals: 5,
param_slots: vec![],
param_reference_kinds: vec![],
local_types: vec![
crate::mir::types::LocalTypeInfo::Unknown;
5
],
span,
field_name_table: Default::default(),
local_struct_type_names,
local_typed_array_element_types: std::collections::HashMap::new(),
local_declared_scalar_types: std::collections::HashMap::new(),
};
let method_returns =
|type_name: &str, method_name: &str| -> Option<ConcreteType> {
if type_name == "X" && method_name == "name" {
Some(ConcreteType::String)
} else {
None
}
};
let result = infer_top_level_concrete_types_from_mir_with_resolvers(
&mir,
None,
Some(&method_returns),
None,
None,
);
assert_eq!(
result[3],
ConcreteType::String,
"Call destination slot must be stamped ConcreteType::String \
from the method_returns resolver for `t.name()` where \
`local_struct_type_names[t] = \"X\"` and \
`method_returns(\"X\", \"name\") = Some(String)`"
);
}
#[test]
fn trait_method_call_propagates_struct_identity_through_slot_move() {
let span = mk_span();
let bb0 = BasicBlock {
id: BasicBlockId(0),
statements: vec![
crate::mir::types::MirStatement {
kind: StatementKind::ObjectStore {
container_slot: SlotId(2),
operands: vec![],
field_names: vec![],
schema_id: None,
},
span,
point: Point(0),
},
crate::mir::types::MirStatement {
kind: StatementKind::Assign(
Place::Local(SlotId(3)),
Rvalue::Use(Operand::Move(Place::Local(SlotId(2)))),
),
span,
point: Point(1),
},
],
terminator: Terminator {
kind: TerminatorKind::Call {
func: Operand::Constant(MirConstant::Method(
"name".to_string(),
)),
args: vec![Operand::Move(Place::Local(SlotId(3)))],
destination: Place::Local(SlotId(4)),
next: BasicBlockId(1),
},
span,
},
};
let bb1 = BasicBlock {
id: BasicBlockId(1),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Return,
span,
},
};
let mut local_struct_type_names = std::collections::HashMap::new();
local_struct_type_names.insert(SlotId(2), "X".to_string());
let mir = MirFunction {
name: "smoke3_moved".to_string(),
blocks: vec![bb0, bb1],
num_locals: 6,
param_slots: vec![],
param_reference_kinds: vec![],
local_types: vec![
crate::mir::types::LocalTypeInfo::Unknown;
6
],
span,
field_name_table: Default::default(),
local_struct_type_names,
local_typed_array_element_types: std::collections::HashMap::new(),
local_declared_scalar_types: std::collections::HashMap::new(),
};
let method_returns =
|type_name: &str, method_name: &str| -> Option<ConcreteType> {
if type_name == "X" && method_name == "name" {
Some(ConcreteType::String)
} else {
None
}
};
let result = infer_top_level_concrete_types_from_mir_with_resolvers(
&mir,
None,
Some(&method_returns),
None,
None,
);
assert_eq!(
result[4],
ConcreteType::String,
"Call destination must be stamped String even when receiver \
flows through `let u = t` — struct identity propagated \
through slot moves alongside concrete_types"
);
}
#[test]
fn trait_method_no_resolver_leaves_destination_void() {
let span = mk_span();
let mut local_struct_type_names = std::collections::HashMap::new();
local_struct_type_names.insert(SlotId(2), "X".to_string());
let bb0 = BasicBlock {
id: BasicBlockId(0),
statements: vec![crate::mir::types::MirStatement {
kind: StatementKind::ObjectStore {
container_slot: SlotId(2),
operands: vec![],
field_names: vec![],
schema_id: None,
},
span,
point: Point(0),
}],
terminator: Terminator {
kind: TerminatorKind::Call {
func: Operand::Constant(MirConstant::Method(
"name".to_string(),
)),
args: vec![Operand::Move(Place::Local(SlotId(2)))],
destination: Place::Local(SlotId(3)),
next: BasicBlockId(1),
},
span,
},
};
let bb1 = BasicBlock {
id: BasicBlockId(1),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Return,
span,
},
};
let mir = MirFunction {
name: "smoke3_no_resolver".to_string(),
blocks: vec![bb0, bb1],
num_locals: 5,
param_slots: vec![],
param_reference_kinds: vec![],
local_types: vec![
crate::mir::types::LocalTypeInfo::Unknown;
5
],
span,
field_name_table: Default::default(),
local_struct_type_names,
local_typed_array_element_types: std::collections::HashMap::new(),
local_declared_scalar_types: std::collections::HashMap::new(),
};
let result =
infer_top_level_concrete_types_from_mir_with_resolvers(&mir, None, None, None, None);
assert_eq!(
result[3],
ConcreteType::Void,
"Without a method-returns resolver, the trait-method \
classifier must not fabricate a kind — destination stays Void"
);
}
#[test]
fn trait_method_no_struct_identity_leaves_destination_void() {
let span = mk_span();
let bb0 = BasicBlock {
id: BasicBlockId(0),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Call {
func: Operand::Constant(MirConstant::Method(
"name".to_string(),
)),
args: vec![Operand::Move(Place::Local(SlotId(2)))],
destination: Place::Local(SlotId(3)),
next: BasicBlockId(1),
},
span,
},
};
let bb1 = BasicBlock {
id: BasicBlockId(1),
statements: vec![],
terminator: Terminator {
kind: TerminatorKind::Return,
span,
},
};
let mir = MirFunction {
name: "smoke3_no_struct_id".to_string(),
blocks: vec![bb0, bb1],
num_locals: 5,
param_slots: vec![],
param_reference_kinds: vec![],
local_types: vec![
crate::mir::types::LocalTypeInfo::Unknown;
5
],
span,
field_name_table: Default::default(),
local_struct_type_names: std::collections::HashMap::new(),
local_typed_array_element_types: std::collections::HashMap::new(),
local_declared_scalar_types: std::collections::HashMap::new(),
};
let method_returns = |_type_name: &str, _method_name: &str| -> Option<ConcreteType> {
Some(ConcreteType::String)
};
let result = infer_top_level_concrete_types_from_mir_with_resolvers(
&mir,
None,
Some(&method_returns),
None,
None,
);
assert_eq!(
result[3],
ConcreteType::Void,
"Without struct identity on the receiver slot, the trait-method \
classifier must not invoke the resolver — destination stays Void"
);
}
}
#[cfg(test)]
mod w17_3_4_2_type_annotation_lowering_tests {
use super::*;
use shape_ast::ast::TypeAnnotation;
use shape_runtime::type_schema::FieldType;
#[test]
fn type_annotation_hashmap_threads_kv() {
let ann = TypeAnnotation::Generic {
name: shape_ast::ast::type_path::TypePath::simple("HashMap"),
args: vec![
TypeAnnotation::Basic("string".to_string()),
TypeAnnotation::Basic("int".to_string()),
],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(
ft,
FieldType::HashMap {
key: Box::new(FieldType::String),
value: Box::new(FieldType::I64),
}
);
}
#[test]
fn type_annotation_map_alias_threads_kv() {
let ann = TypeAnnotation::Generic {
name: shape_ast::ast::type_path::TypePath::simple("Map"),
args: vec![
TypeAnnotation::Basic("string".to_string()),
TypeAnnotation::Basic("bool".to_string()),
],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(
ft,
FieldType::HashMap {
key: Box::new(FieldType::String),
value: Box::new(FieldType::Bool),
}
);
}
#[test]
fn type_annotation_set_threads_elem() {
let ann = TypeAnnotation::Generic {
name: shape_ast::ast::type_path::TypePath::simple("Set"),
args: vec![TypeAnnotation::Basic("int".to_string())],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(ft, FieldType::Set(Box::new(FieldType::I64)));
}
#[test]
fn type_annotation_hashmap_of_array_threads_nested() {
let ann = TypeAnnotation::Generic {
name: shape_ast::ast::type_path::TypePath::simple("HashMap"),
args: vec![
TypeAnnotation::Basic("string".to_string()),
TypeAnnotation::Array(Box::new(TypeAnnotation::Basic(
"int".to_string(),
))),
],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(
ft,
FieldType::HashMap {
key: Box::new(FieldType::String),
value: Box::new(FieldType::Array(Box::new(FieldType::I64))),
}
);
}
#[test]
fn type_annotation_hashmap_malformed_arity_falls_back_to_any() {
let ann = TypeAnnotation::Generic {
name: shape_ast::ast::type_path::TypePath::simple("HashMap"),
args: vec![TypeAnnotation::Basic("int".to_string())],
};
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(ft, FieldType::Any);
}
#[test]
fn type_annotation_array_threads_elem() {
let ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Basic(
"int".to_string(),
)));
let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
assert_eq!(ft, FieldType::Array(Box::new(FieldType::I64)));
}
}