Skip to content

Commit b3d7bd1

Browse files
committed
feat: close ZynML stdlib coverage gaps (RC1-RC6)
Fix 6 root causes blocking ZynML stdlib features, bringing test pass rate from ~60% to 14/15 (93%). - Fix method return type resolution for extern types (lowering + SSA) by matching Type::Extern and Type::Unresolved impl blocks alongside Type::Named, and checking inherent methods on type definitions - Fix f64 binary ops by storing function parameter types in var_typed_ast_types for proper float detection in resolve_actual_type - Add float-aware Add/Sub/Mul in cranelift backend as defensive fallback - Fix unary operator parsing: split unary_prefix_expr grammar rule to properly emit TypedExpression::Unary nodes instead of silently discarding the operator - Add unary trait dispatch (Neg/Not) in SSA builder, following the binary operator dispatch pattern for extern types like Tensor - Propagate Unary/Binary expression types from operands in the Grammar2 interpreter instead of defaulting to Unit - Implement tensor_eye in ZRTL plugin and register $Tensor$eye symbol - Add tensor_eye builtin mapping in ml.zyn
1 parent 0c1ffe5 commit b3d7bd1

7 files changed

Lines changed: 313 additions & 33 deletions

File tree

.env.markflow

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ MARKFLOW_TEXT_SECONDARY=#555566
1818
MARKFLOW_TEXT_MUTED=#888899
1919
MARKFLOW_BORDER=rgba(0,0,0,0.08)
2020
MARKFLOW_CODE_BG=#1a1a2e
21+
MARKFLOW_CODE_INLINE_BG=rgba(0,0,0,0.06)
22+
MARKFLOW_CODE_INLINE_COLOR=#8b3dff
23+
MARKFLOW_BG_HOVER=rgba(0,0,0,0.04)
24+
MARKFLOW_ACCENT_GLOW=rgba(26,26,26,0.06)
25+
MARKFLOW_HEADER_BG=rgba(234,234,242,0.85)
26+
MARKFLOW_FOOTER_BG=rgba(234,234,242,0.9)
27+
MARKFLOW_BORDER_ACTIVE=rgba(0,0,0,0.15)
2128

2229
MARKFLOW_SOCIAL_GITHUB=https://github.com/darmie/zyntax
2330

crates/compiler/src/cranelift_backend.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,9 +1239,27 @@ impl CraneliftBackend {
12391239
});
12401240

12411241
let value = match op {
1242-
BinaryOp::Add => builder.ins().iadd(lhs, rhs),
1243-
BinaryOp::Sub => builder.ins().isub(lhs, rhs),
1244-
BinaryOp::Mul => builder.ins().imul(lhs, rhs),
1242+
BinaryOp::Add => {
1243+
if ty.is_float() {
1244+
builder.ins().fadd(lhs, rhs)
1245+
} else {
1246+
builder.ins().iadd(lhs, rhs)
1247+
}
1248+
}
1249+
BinaryOp::Sub => {
1250+
if ty.is_float() {
1251+
builder.ins().fsub(lhs, rhs)
1252+
} else {
1253+
builder.ins().isub(lhs, rhs)
1254+
}
1255+
}
1256+
BinaryOp::Mul => {
1257+
if ty.is_float() {
1258+
builder.ins().fmul(lhs, rhs)
1259+
} else {
1260+
builder.ins().imul(lhs, rhs)
1261+
}
1262+
}
12451263
BinaryOp::Div => {
12461264
if ty.is_float() {
12471265
builder.ins().fdiv(lhs, rhs)
@@ -5814,7 +5832,7 @@ impl CraneliftBackend {
58145832
sig.params
58155833
.push(cranelift_codegen::ir::AbiParam::new(arg_ty));
58165834
}
5817-
// For now, assume void return unless we have result
5835+
// For now, assume i64 return unless we have no result
58185836
if result.is_some() {
58195837
sig.returns
58205838
.push(cranelift_codegen::ir::AbiParam::new(types::I64));

crates/compiler/src/lowering.rs

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,26 +1180,78 @@ impl LoweringContext {
11801180
..
11811181
} = &receiver_type
11821182
{
1183+
// Also get the type name for matching extern impls
1184+
let receiver_type_name = self.type_registry
1185+
.get_type_by_id(*receiver_type_id)
1186+
.map(|td| td.name);
11831187
// Look up the trait implementation
11841188
for (_trait_id, impls) in self.type_registry.iter_implementations() {
11851189
for impl_def in impls {
1186-
if let Type::Named {
1187-
id: impl_type_id, ..
1188-
} = &impl_def.for_type
1189-
{
1190-
if *impl_type_id == *receiver_type_id {
1191-
// Find the method in this impl
1192-
for method in &impl_def.methods {
1193-
if method.signature.name == method_call.method {
1194-
// Update the expression type
1195-
expr.ty = method.signature.return_type.clone();
1196-
break;
1197-
}
1190+
let impl_matches = match &impl_def.for_type {
1191+
Type::Named { id: impl_type_id, .. } => {
1192+
*impl_type_id == *receiver_type_id
1193+
}
1194+
Type::Extern { name, .. } => {
1195+
receiver_type_name.map_or(false, |n| n == *name)
1196+
}
1197+
Type::Unresolved(name) => {
1198+
receiver_type_name.map_or(false, |n| n == *name)
1199+
}
1200+
_ => false,
1201+
};
1202+
if impl_matches {
1203+
// Find the method in this impl
1204+
for method in &impl_def.methods {
1205+
if method.signature.name == method_call.method {
1206+
expr.ty = method.signature.return_type.clone();
1207+
break;
11981208
}
11991209
}
12001210
}
12011211
}
12021212
}
1213+
// Also check inherent methods on the type definition (for extern struct methods)
1214+
if matches!(expr.ty, Type::Any | Type::Unknown) {
1215+
if let Some(type_def) = self.type_registry.get_type_by_id(*receiver_type_id) {
1216+
for method in &type_def.methods {
1217+
if method.name == method_call.method {
1218+
expr.ty = method.return_type.clone();
1219+
break;
1220+
}
1221+
}
1222+
}
1223+
}
1224+
} else if let Type::Extern { name: extern_name, .. } = &receiver_type {
1225+
// Look up methods in impl blocks for extern types (e.g., Tensor)
1226+
// This enables chained method calls like t.reshape([2,3]).transpose()
1227+
for (_trait_id, impls) in self.type_registry.iter_implementations() {
1228+
for impl_def in impls {
1229+
let impl_for_this_extern = match &impl_def.for_type {
1230+
Type::Extern { name, .. } => name == extern_name,
1231+
Type::Unresolved(name) => name == extern_name,
1232+
_ => false,
1233+
};
1234+
if impl_for_this_extern {
1235+
for method in &impl_def.methods {
1236+
if method.signature.name == method_call.method {
1237+
expr.ty = method.signature.return_type.clone();
1238+
break;
1239+
}
1240+
}
1241+
}
1242+
}
1243+
}
1244+
// Also check inherent methods on the type definition
1245+
if matches!(expr.ty, Type::Any | Type::Unknown) {
1246+
if let Some(type_def) = self.type_registry.get_type_by_name(*extern_name) {
1247+
for method in &type_def.methods {
1248+
if method.name == method_call.method {
1249+
expr.ty = method.return_type.clone();
1250+
break;
1251+
}
1252+
}
1253+
}
1254+
}
12031255
}
12041256
}
12051257
}

crates/compiler/src/ssa.rs

Lines changed: 180 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@ impl SsaBuilder {
522522
// Store parameter type for SSA variable tracking
523523
self.var_types.insert(param.name, param.ty.clone());
524524

525+
// Also store typed AST type for binary op float detection
526+
let typed_ast_type = self.hir_type_to_typed_ast_type(&param.ty);
527+
self.var_typed_ast_types.insert(param.name, typed_ast_type);
528+
525529
eprintln!(
526530
"[PARAM DEBUG] Inserted into var_types: var_types['{}'] = {:?}",
527531
param.name.resolve_global().unwrap_or_default(),
@@ -1406,10 +1410,26 @@ impl SsaBuilder {
14061410
let hir_type = self.convert_type(&let_stmt.ty);
14071411
self.var_types.insert(let_stmt.name, hir_type.clone());
14081412

1409-
// For TypedAST type, use initializer's type if variable type is Any
1413+
// For TypedAST type, use initializer's type if variable type is Any/Unknown
14101414
// This works around the issue where type inference doesn't update the AST
1411-
let typed_ast_type = if matches!(let_stmt.ty, Type::Any) {
1412-
value.ty.clone()
1415+
let typed_ast_type = if matches!(let_stmt.ty, Type::Any | Type::Unknown) {
1416+
// For unary expressions, the node type may also be Unknown;
1417+
// in that case, look through to the operand's type
1418+
let init_ty = &value.ty;
1419+
if matches!(init_ty, Type::Any | Type::Unknown) {
1420+
if let TypedExpression::Unary(unary) = &value.node {
1421+
let operand_ty = self.resolve_actual_type(&unary.operand.node, &unary.operand.ty);
1422+
if !matches!(operand_ty, Type::Any | Type::Unknown) {
1423+
operand_ty
1424+
} else {
1425+
init_ty.clone()
1426+
}
1427+
} else {
1428+
init_ty.clone()
1429+
}
1430+
} else {
1431+
init_ty.clone()
1432+
}
14131433
} else {
14141434
let_stmt.ty.clone()
14151435
};
@@ -2628,8 +2648,32 @@ impl SsaBuilder {
26282648
TypedExpression::Unary(unary) => {
26292649
let op = &unary.op;
26302650
let operand = &unary.operand;
2651+
2652+
// Resolve actual operand type for trait dispatch
2653+
let operand_actual_ty = self.resolve_actual_type(&operand.node, &operand.ty);
2654+
2655+
// Try trait dispatch for non-primitive types (e.g., -tensor → $Tensor$neg)
2656+
let mut operand_with_type = operand.clone();
2657+
operand_with_type.ty = operand_actual_ty;
2658+
2659+
if let Some(trait_call) = self.try_unary_operator_trait_dispatch(
2660+
block_id,
2661+
op,
2662+
&operand_with_type,
2663+
&expr.ty,
2664+
)? {
2665+
return Ok(trait_call);
2666+
}
2667+
2668+
// Regular unary operations for primitive types
26312669
let operand_val = self.translate_expression(block_id, operand)?;
2632-
let result_type = self.convert_type(&expr.ty);
2670+
// For unary ops, result type = operand type (e.g., -int → int)
2671+
// Use operand type when expression type is Unknown/Any
2672+
let result_type = if matches!(expr.ty, Type::Unknown | Type::Any) {
2673+
self.convert_type(&operand_with_type.ty)
2674+
} else {
2675+
self.convert_type(&expr.ty)
2676+
};
26332677

26342678
let hir_op = self.convert_unary_op(op);
26352679
let result = self.create_value(result_type.clone(), HirValueKind::Instruction);
@@ -3674,20 +3718,32 @@ impl SsaBuilder {
36743718
} else if let Type::Named { id, .. } = &receiver_type {
36753719
// Fall back to looking up in trait implementations for named types
36763720
let receiver_type_id = *id;
3721+
// Also get the type name for matching extern impls
3722+
let receiver_type_name = self.type_registry
3723+
.get_type_by_id(receiver_type_id)
3724+
.map(|td| td.name);
36773725
let mut method_return_type = None;
36783726
for (_trait_id, impls) in self.type_registry.iter_implementations() {
36793727
for impl_def in impls {
3680-
if let Type::Named {
3681-
id: impl_type_id, ..
3682-
} = &impl_def.for_type
3683-
{
3684-
if *impl_type_id == receiver_type_id {
3685-
for method in &impl_def.methods {
3686-
if method.signature.name == method_call.method {
3687-
method_return_type =
3688-
Some(method.signature.return_type.clone());
3689-
break;
3690-
}
3728+
let impl_matches = match &impl_def.for_type {
3729+
Type::Named { id: impl_type_id, .. } => {
3730+
*impl_type_id == receiver_type_id
3731+
}
3732+
Type::Extern { name, .. } => {
3733+
// Extern type impls match by name
3734+
receiver_type_name.map_or(false, |n| n == *name)
3735+
}
3736+
Type::Unresolved(name) => {
3737+
receiver_type_name.map_or(false, |n| n == *name)
3738+
}
3739+
_ => false,
3740+
};
3741+
if impl_matches {
3742+
for method in &impl_def.methods {
3743+
if method.signature.name == method_call.method {
3744+
method_return_type =
3745+
Some(method.signature.return_type.clone());
3746+
break;
36913747
}
36923748
}
36933749
}
@@ -5543,6 +5599,115 @@ impl SsaBuilder {
55435599
))
55445600
}
55455601

5602+
/// Try to dispatch a unary operator to a trait method call.
5603+
/// Returns Some(result_value) if the operator should be dispatched via trait,
5604+
/// or None if the regular unary instruction should be used.
5605+
fn try_unary_operator_trait_dispatch(
5606+
&mut self,
5607+
block_id: HirId,
5608+
op: &zyntax_typed_ast::typed_ast::UnaryOp,
5609+
operand: &zyntax_typed_ast::TypedNode<zyntax_typed_ast::typed_ast::TypedExpression>,
5610+
_result_ty: &Type,
5611+
) -> CompilerResult<Option<HirId>> {
5612+
use zyntax_typed_ast::typed_ast::UnaryOp as FrontendOp;
5613+
5614+
// Only consider trait dispatch for non-primitive types
5615+
let operand_type = &operand.ty;
5616+
if !self.is_trait_dispatchable_type(operand_type) {
5617+
return Ok(None);
5618+
}
5619+
5620+
// Get the method and trait names for this operator
5621+
let (method_name, trait_name) = match op {
5622+
FrontendOp::Minus => ("neg", "Neg"),
5623+
FrontendOp::Not => ("not", "Not"),
5624+
_ => return Ok(None),
5625+
};
5626+
5627+
// Get the type name for constructing the method symbol
5628+
let type_name = self.get_type_symbol_prefix(operand_type);
5629+
if type_name.is_none() {
5630+
return Ok(None);
5631+
}
5632+
let type_name = type_name.unwrap();
5633+
5634+
// Build candidate names (same pattern as binary dispatch):
5635+
// 1) Type$method
5636+
// 2) Type$Trait$method
5637+
// 3) $Type$method (runtime symbol for extern-backed types)
5638+
let mut function_candidates = Vec::with_capacity(2);
5639+
let runtime_symbol = if let Some(stripped) = type_name.strip_prefix('$') {
5640+
function_candidates.push(format!("{}${}", stripped, method_name));
5641+
function_candidates.push(format!("{}${}${}", stripped, trait_name, method_name));
5642+
format!("{}${}", type_name, method_name)
5643+
} else {
5644+
function_candidates.push(format!("{}${}", type_name, method_name));
5645+
function_candidates.push(format!("{}${}${}", type_name, trait_name, method_name));
5646+
format!("${}${}", type_name, method_name)
5647+
};
5648+
5649+
let mut matched_function: Option<(HirId, String)> = None;
5650+
for candidate in &function_candidates {
5651+
let candidate_interned = InternedString::new_global(candidate);
5652+
if let Some(&func_id) = self.function_symbols.get(&candidate_interned) {
5653+
matched_function = Some((func_id, candidate.clone()));
5654+
break;
5655+
}
5656+
}
5657+
5658+
let callee = if let Some((func_id, matched_name)) = matched_function {
5659+
log::debug!(
5660+
"[SSA] Unary trait dispatch: using function '{}' for type {}",
5661+
matched_name,
5662+
type_name
5663+
);
5664+
crate::hir::HirCallable::Function(func_id)
5665+
} else if type_name.starts_with('$') {
5666+
log::debug!(
5667+
"[SSA] Unary trait dispatch: using runtime symbol '{}' for type {}",
5668+
runtime_symbol,
5669+
type_name
5670+
);
5671+
crate::hir::HirCallable::Symbol(runtime_symbol)
5672+
} else {
5673+
return Ok(None);
5674+
};
5675+
5676+
// Translate the operand
5677+
let operand_val = self.translate_expression(block_id, operand)?;
5678+
5679+
// Result type should be the same as the operand type (e.g., -Tensor = Tensor)
5680+
let hir_result_type = self.convert_type(operand_type);
5681+
log::debug!(
5682+
"[SSA] Unary trait dispatch result type: {:?} (from operand type: {:?})",
5683+
hir_result_type,
5684+
operand_type
5685+
);
5686+
5687+
// Create call instruction to the trait method
5688+
let result = if hir_result_type != HirType::Void {
5689+
Some(self.create_value(hir_result_type.clone(), HirValueKind::Instruction))
5690+
} else {
5691+
None
5692+
};
5693+
5694+
let inst = HirInstruction::Call {
5695+
result,
5696+
callee,
5697+
args: vec![operand_val],
5698+
type_args: vec![],
5699+
const_args: vec![],
5700+
is_tail: false,
5701+
};
5702+
5703+
self.add_instruction(block_id, inst);
5704+
self.add_use(operand_val, result.unwrap_or(operand_val));
5705+
5706+
Ok(Some(
5707+
result.unwrap_or_else(|| self.create_undef(HirType::Void)),
5708+
))
5709+
}
5710+
55465711
/// Resolve the actual type for an expression, looking up variable types if needed.
55475712
/// This is needed because expression nodes may have type `Any` even when the
55485713
/// variable has a more specific type recorded in var_typed_ast_types.

crates/zyn_peg/src/runtime2/interpreter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,10 @@ impl<'g> GrammarInterpreter<'g> {
827827
TypedExpression::Compute(_) => Type::Any,
828828
// Struct literal gets its type from the struct name - use Unresolved for compiler to resolve
829829
TypedExpression::Struct(lit) => Type::Unresolved(lit.name),
830+
// Unary operations (negation, not) have the same type as their operand
831+
TypedExpression::Unary(unary) => unary.operand.ty.clone(),
832+
// Binary operations have the same type as their left operand
833+
TypedExpression::Binary(binary) => binary.left.ty.clone(),
830834
_ => Type::Primitive(PrimitiveType::Unit),
831835
};
832836

0 commit comments

Comments
 (0)