use crate::bytecode::{BuiltinFunction, OpCode, Operand};
use crate::executor::tests::test_utils::{compile, eval, eval_result};
use shape_value::{ValueWord, ValueWordExt};
#[test]
fn test_add_trait_overload() {
let result = eval(
r#"
type Vec2 { x: number, y: number }
impl Add for Vec2 {
method add(other: Vec2) -> Vec2 {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
let a = Vec2 { x: 1.0, y: 2.0 }
let b = Vec2 { x: 3.0, y: 4.0 }
let c = a + b
c.x + c.y
"#,
);
let val = result.as_number_coerce().expect("should be a number");
assert_eq!(val, 10.0, "Vec2(1,2) + Vec2(3,4) = Vec2(4,6), x+y = 10");
}
#[test]
fn test_sub_trait_overload() {
let result = eval(
r#"
type Vec2 { x: number, y: number }
impl Sub for Vec2 {
method sub(other: Vec2) -> Vec2 {
Vec2 { x: self.x - other.x, y: self.y - other.y }
}
}
let a = Vec2 { x: 5.0, y: 10.0 }
let b = Vec2 { x: 1.0, y: 3.0 }
let c = a - b
c.x + c.y
"#,
);
let val = result.as_number_coerce().expect("should be a number");
assert_eq!(val, 11.0, "Vec2(5,10) - Vec2(1,3) = Vec2(4,7), x+y = 11");
}
#[test]
fn test_mul_trait_overload() {
let result = eval(
r#"
type Vec2 { x: number, y: number }
impl Mul for Vec2 {
method mul(other: Vec2) -> Vec2 {
Vec2 { x: self.x * other.x, y: self.y * other.y }
}
}
let a = Vec2 { x: 2.0, y: 3.0 }
let b = Vec2 { x: 4.0, y: 5.0 }
let c = a * b
c.x + c.y
"#,
);
let val = result.as_number_coerce().expect("should be a number");
assert_eq!(val, 23.0, "Vec2(2,3) * Vec2(4,5) = Vec2(8,15), x+y = 23");
}
#[test]
fn test_div_trait_overload() {
let result = eval(
r#"
type Vec2 { x: number, y: number }
impl Div for Vec2 {
method div(other: Vec2) -> Vec2 {
Vec2 { x: self.x / other.x, y: self.y / other.y }
}
}
let a = Vec2 { x: 10.0, y: 20.0 }
let b = Vec2 { x: 2.0, y: 5.0 }
let c = a / b
c.x + c.y
"#,
);
let val = result.as_number_coerce().expect("should be a number");
assert_eq!(val, 9.0, "Vec2(10,20) / Vec2(2,5) = Vec2(5,4), x+y = 9");
}
#[test]
fn test_neg_trait_overload() {
let result = eval(
r#"
type Vec2 { x: number, y: number }
impl Neg for Vec2 {
method neg() -> Vec2 {
Vec2 { x: -self.x, y: -self.y }
}
}
let a = Vec2 { x: 3.0, y: -7.0 }
let b = -a
b.x + b.y
"#,
);
let val = result.as_number_coerce().expect("should be a number");
assert_eq!(val, 4.0, "-Vec2(3,-7) = Vec2(-3,7), x+y = 4");
}
#[test]
fn test_multiple_operator_traits() {
let result = eval(
r#"
type Money { cents: int }
impl Add for Money {
method add(other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
impl Sub for Money {
method sub(other: Money) -> Money {
Money { cents: self.cents - other.cents }
}
}
let a = Money { cents: 500 }
let b = Money { cents: 200 }
let sum = a + b
let diff = a - b
sum.cents + diff.cents
"#,
);
let val = result.as_i64().expect("should be an int");
assert_eq!(
val, 1000,
"Money(500)+Money(200)=700, Money(500)-Money(200)=300, total=1000"
);
}
#[test]
fn test_builtin_arithmetic_still_works() {
let result = eval("2 + 3");
assert_eq!(result.as_i64().unwrap(), 5);
let result = eval("10.0 - 3.0");
assert_eq!(result.as_number_coerce().unwrap(), 7.0);
let result = eval("4 * 5");
assert_eq!(result.as_i64().unwrap(), 20);
let result = eval("20 / 4");
assert_eq!(result.as_i64().unwrap(), 5);
let result = eval("-42");
assert_eq!(result.as_i64().unwrap(), -42);
}
#[test]
fn test_string_concat_still_works() {
let result = eval(r#""hello " + "world""#);
assert_eq!(result.as_str().unwrap(), "hello world");
}
fn all_opcodes(program: &crate::bytecode::BytecodeProgram) -> Vec<OpCode> {
program.instructions.iter().map(|i| i.opcode).collect()
}
#[test]
fn test_r5_2a_user_add_compiles_to_call_method_not_add_dynamic() {
let program = compile(
r#"
type Vec2 { x: number, y: number }
impl Add for Vec2 {
method add(other: Vec2) -> Vec2 {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
let a = Vec2 { x: 1.0, y: 2.0 }
let b = Vec2 { x: 3.0, y: 4.0 }
let c = a + b
"#,
);
let ops = all_opcodes(&program);
let call_method_count = ops.iter().filter(|&&o| o == OpCode::CallMethod).count();
assert!(
call_method_count >= 1,
"R5.2A regression: no CallMethod emitted for user-defined operator \
trait dispatch. Ops emitted: {:?}",
ops
);
}
#[test]
fn test_r5_2b_user_add_retargets_single_schema_fallback() {
let program = compile(
r#"
type Vec2 { x: number, y: number }
impl Add for Vec2 {
method add(other: Vec2) -> Vec2 {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
// Identity-style function with an untyped parameter: its inferred
// return type is a free type variable (not a concrete schema), so
// the call-site value has no TypedObject schema attached.
fn pick(x) { return x }
let a = Vec2 { x: 1.0, y: 2.0 }
let b = Vec2 { x: 3.0, y: 4.0 }
// `pick(b)` returns a schema-less value at compile time: the
// identity return type is a fresh type variable, so after
// `compile_expr(right)` the compiler's `last_expr_schema` is None.
// This defeats the priority-1 both-schemas fast path at L665-684
// and forces the Add branch into the `CoercedNeedsGeneric | NoPlan`
// arm — the gap R5.2B closes. The R5.2B-inserted
// `try_emit_trait_dispatch` call then picks up `left_schema = Vec2`
// and retargets to `CallMethod("add")` at compile time.
let r = a + pick(b)
"#,
);
let ops = all_opcodes(&program);
let call_method_count = ops.iter().filter(|&&o| o == OpCode::CallMethod).count();
assert!(
call_method_count >= 1,
"R5.2B regression: no CallMethod emitted for the single-schema \
user-op Add fallback. Ops emitted: {:?}",
ops
);
}
#[test]
fn test_r5_3b_datetime_arithmetic_retargets_to_call_method() {
{
let program = compile(
r#"
fn test() {
let dt = @"2024-01-15"
let dur = 3d
dt + dur
}
test()
"#,
);
let ops = all_opcodes(&program);
assert!(
ops.contains(&OpCode::CallMethod),
"R5.3B regression: let-local DateTime + Duration did not emit \
CallMethod. Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(dt: DateTime, dur: Duration) {
dt - dur
}
test(@"2024-01-15", 1d)
"#,
);
let ops = all_opcodes(&program);
assert!(
ops.contains(&OpCode::CallMethod),
"R5.3B regression: typed-param DateTime - Duration did not emit \
CallMethod. Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(a: DateTime, b: DateTime) {
a - b
}
test(@"2024-01-15", @"2024-01-10")
"#,
);
let ops = all_opcodes(&program);
assert!(
ops.contains(&OpCode::CallMethod),
"R5.3B regression: typed-param DateTime - DateTime did not emit \
CallMethod. Ops emitted: {:?}",
ops
);
}
}
#[test]
fn test_operator_overload_without_trait_fails() {
let result = eval_result(
r#"
type Foo { x: int }
let a = Foo { x: 1 }
let b = Foo { x: 2 }
a - b
"#,
);
assert!(
result.is_err(),
"Subtracting two Foo without impl Sub should fail"
);
}
fn has_builtin_call(program: &crate::bytecode::BytecodeProgram, builtin: BuiltinFunction) -> bool {
program.instructions.iter().any(|ins| {
ins.opcode == OpCode::BuiltinCall
&& matches!(ins.operand, Some(Operand::Builtin(b)) if b == builtin)
})
}
#[test]
fn test_r5_4e_matrix_vec_arithmetic_retargets_to_intrinsics() {
{
let program = compile(
r#"
fn test(a: Mat<number>, b: Mat<number>) { a + b }
"#,
);
let ops = all_opcodes(&program);
assert!(
has_builtin_call(&program, BuiltinFunction::IntrinsicMatAdd),
"R5.4E retarget: Mat+Mat must emit BuiltinCall(IntrinsicMatAdd). \
Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(a: Mat<number>, b: Mat<number>) { a - b }
"#,
);
let ops = all_opcodes(&program);
assert!(
has_builtin_call(&program, BuiltinFunction::IntrinsicMatSub),
"R5.4E retarget: Mat-Mat must emit BuiltinCall(IntrinsicMatSub). \
Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(a: Vec<number>, b: Vec<number>) { a + b }
"#,
);
let ops = all_opcodes(&program);
assert!(
has_builtin_call(&program, BuiltinFunction::IntrinsicVecAdd),
"R5.4E retarget: Vec<number>+Vec<number> must emit \
BuiltinCall(IntrinsicVecAdd). Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(a: Vec<number>, b: Vec<number>) { a - b }
"#,
);
let ops = all_opcodes(&program);
assert!(
has_builtin_call(&program, BuiltinFunction::IntrinsicVecSub),
"R5.4E retarget: Vec<number>-Vec<number> must emit \
BuiltinCall(IntrinsicVecSub). Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(a: Vec<number>, b: Vec<number>) { a * b }
"#,
);
let ops = all_opcodes(&program);
assert!(
has_builtin_call(&program, BuiltinFunction::IntrinsicVecMul),
"R5.4E retarget: Vec<number>*Vec<number> must emit \
BuiltinCall(IntrinsicVecMul). Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(a: Vec<number>, b: Vec<number>) { a / b }
"#,
);
let ops = all_opcodes(&program);
assert!(
has_builtin_call(&program, BuiltinFunction::IntrinsicVecDiv),
"R5.4E retarget: Vec<number>/Vec<number> must emit \
BuiltinCall(IntrinsicVecDiv). Ops emitted: {:?}",
ops
);
}
{
let program = compile(
r#"
fn test(a: Vec<int>, b: Vec<int>) { a + b }
"#,
);
let ops = all_opcodes(&program);
assert!(
has_builtin_call(&program, BuiltinFunction::IntrinsicVecAddI64),
"R5.4E retarget: Vec<int>+Vec<int> must emit \
BuiltinCall(IntrinsicVecAddI64). Ops emitted: {:?}",
ops
);
}
}
#[test]
fn test_r5_4e_mat_add_runtime_returns_correct_values() {
let result = eval(
r#"
let a: Mat<number> = [[1.0, 2.0], [3.0, 4.0]]
let b: Mat<number> = [[10.0, 20.0], [30.0, 40.0]]
let c = a + b
c[0][0] + c[1][1]
"#,
);
let n = result
.as_number_coerce()
.expect("R5.4E: Mat+Mat result should be numeric at c[0][0]+c[1][1]");
assert!(
(n - 55.0).abs() < 1e-10,
"R5.4E: Mat+Mat expected 55.0, got {}",
n
);
}