use super::*;
use tree_sitter::Point;
fn p(row: usize, col: usize) -> Point {
Point { row, column: col }
}
fn span(r0: usize, c0: usize, r1: usize, c1: usize) -> Span {
Span {
start: p(r0, c0),
end: p(r1, c1),
}
}
fn wvar(name: &str, scope: u32, payload: WitnessPayload) -> Witness {
Witness {
attachment: WitnessAttachment::Variable {
name: name.to_string(),
scope: ScopeId(scope),
},
source: WitnessSource::Builder("test".into()),
payload,
span: span(0, 0, 0, 0),
}
}
#[test]
fn witness_bag_stores_and_retrieves_by_attachment() {
let mut bag = WitnessBag::new();
bag.push(wvar(
"$self",
0,
WitnessPayload::Observation(TypeObservation::FirstParamInMethod {
package: "Foo".into(),
}),
));
bag.push(wvar(
"$self",
0,
WitnessPayload::Observation(TypeObservation::HashRefAccess),
));
bag.push(wvar(
"$other",
0,
WitnessPayload::Observation(TypeObservation::ArrayRefAccess),
));
let att = WitnessAttachment::Variable {
name: "$self".into(),
scope: ScopeId(0),
};
let hits = bag.for_attachment(&att);
assert_eq!(hits.len(), 2);
}
#[test]
fn mojo_sub_name_does_not_flip_type_to_hashref() {
let mut bag = WitnessBag::new();
bag.push(wvar(
"$self",
0,
WitnessPayload::Observation(TypeObservation::FirstParamInMethod {
package: "Mojolicious::Routes::Route".into(),
}),
));
bag.push(wvar(
"$self",
0,
WitnessPayload::Observation(TypeObservation::HashRefAccess),
));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$self".into(),
scope: ScopeId(0),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::MojoBase,
arity_hint: None, receiver: None, context: None,
};
let v = reg.query(&bag, &q);
assert_eq!(
v,
ReducedValue::Type(InferredType::ClassName("Mojolicious::Routes::Route".into()))
);
}
#[test]
fn plain_hashref_access_without_class_evidence() {
let mut bag = WitnessBag::new();
bag.push(wvar(
"$cfg",
1,
WitnessPayload::Observation(TypeObservation::HashRefAccess),
));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$cfg".into(),
scope: ScopeId(1),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::HashRef)
);
}
#[test]
fn bless_target_array_with_class_assertion_keeps_class() {
let mut bag = WitnessBag::new();
bag.push(wvar(
"$x",
2,
WitnessPayload::Observation(TypeObservation::ClassAssertion("Arr::Foo".into())),
));
bag.push(wvar(
"$x",
2,
WitnessPayload::Observation(TypeObservation::BlessTarget(Rep::Array)),
));
bag.push(wvar(
"$x",
2,
WitnessPayload::Observation(TypeObservation::ArrayRefAccess),
));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$x".into(),
scope: ScopeId(2),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::ClassName("Arr::Foo".into()))
);
}
#[test]
fn core_class_still_holds_class_against_hashref_access() {
let mut bag = WitnessBag::new();
bag.push(wvar(
"$self",
0,
WitnessPayload::Observation(TypeObservation::FirstParamInMethod {
package: "MyApp::Thing".into(),
}),
));
bag.push(wvar(
"$self",
0,
WitnessPayload::Observation(TypeObservation::HashRefAccess),
));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$self".into(),
scope: ScopeId(0),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::CoreClass,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::ClassName("MyApp::Thing".into()))
);
}
#[test]
fn fluent_chain_get_to_resolves_via_edge_chase() {
let mut bag = WitnessBag::new();
let get_ref = RefIdx(42);
let get_sid = SymbolId(7);
bag.push(Witness {
attachment: WitnessAttachment::Expression(get_ref),
source: WitnessSource::Builder("chain".into()),
payload: WitnessPayload::Edge(WitnessAttachment::Symbol(get_sid)),
span: span(0, 0, 0, 0),
});
bag.push(Witness {
attachment: WitnessAttachment::Symbol(get_sid),
source: WitnessSource::Builder("local_return".into()),
payload: WitnessPayload::InferredType(InferredType::ClassName(
"Mojolicious::Routes::Route".into(),
)),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Expression(get_ref);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::ClassName("Mojolicious::Routes::Route".into()))
);
}
#[test]
fn edge_with_unresolved_target_yields_none() {
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Expression(RefIdx(7)),
source: WitnessSource::Builder("chain".into()),
payload: WitnessPayload::Edge(WitnessAttachment::Symbol(SymbolId(99))),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Expression(RefIdx(7));
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(reg.query(&bag, &q), ReducedValue::None);
}
#[test]
fn edge_chase_terminates_on_cycle() {
let mut bag = WitnessBag::new();
let a = WitnessAttachment::Symbol(SymbolId(1));
let b = WitnessAttachment::Symbol(SymbolId(2));
bag.push(Witness {
attachment: a.clone(),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::Edge(b.clone()),
span: span(0, 0, 0, 0),
});
bag.push(Witness {
attachment: b.clone(),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::Edge(a.clone()),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let q = ReducerQuery {
attachment: &a,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(reg.query(&bag, &q), ReducedValue::None);
}
#[test]
fn edge_chase_resolves_transitively() {
let mut bag = WitnessBag::new();
let a = WitnessAttachment::Symbol(SymbolId(1));
let b = WitnessAttachment::Symbol(SymbolId(2));
bag.push(Witness {
attachment: a.clone(),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::Edge(b.clone()),
span: span(0, 0, 0, 0),
});
bag.push(Witness {
attachment: b.clone(),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::InferredType(InferredType::String),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let q = ReducerQuery {
attachment: &a,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::String)
);
}
#[test]
fn narrowed_span_wins_over_outer_witness_at_inside_point() {
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Variable {
name: "$v".into(),
scope: ScopeId(0),
},
source: WitnessSource::Builder("outer".into()),
payload: WitnessPayload::InferredType(InferredType::HashRef),
span: span(0, 0, 0, 0), });
bag.push(Witness {
attachment: WitnessAttachment::Variable {
name: "$v".into(),
scope: ScopeId(0),
},
source: WitnessSource::Builder("narrowing".into()),
payload: WitnessPayload::InferredType(InferredType::ArrayRef),
span: span(4, 0, 8, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$v".into(),
scope: ScopeId(0),
};
let inside = ReducerQuery {
attachment: &att,
point: Some(p(5, 4)),
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &inside),
ReducedValue::Type(InferredType::ArrayRef)
);
let outside = ReducerQuery {
attachment: &att,
point: Some(p(2, 0)),
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &outside),
ReducedValue::Type(InferredType::HashRef)
);
}
fn wbranch(name: &str, scope: u32, t: InferredType) -> Witness {
Witness {
attachment: WitnessAttachment::Variable {
name: name.to_string(),
scope: ScopeId(scope),
},
source: WitnessSource::Builder("branch_arm".into()),
payload: WitnessPayload::InferredType(t),
span: span(0, 0, 0, 0),
}
}
#[test]
fn branch_arms_agree_folds_to_that_type() {
let mut bag = WitnessBag::new();
bag.push(wbranch("$x", 0, InferredType::Numeric));
bag.push(wbranch("$x", 0, InferredType::Numeric));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$x".into(),
scope: ScopeId(0),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::Numeric)
);
}
#[test]
fn branch_arms_disagree_folds_to_none() {
let mut bag = WitnessBag::new();
bag.push(wbranch("$x", 0, InferredType::Numeric));
bag.push(wbranch("$x", 0, InferredType::String));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$x".into(),
scope: ScopeId(0),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(reg.query(&bag, &q), ReducedValue::None);
}
#[test]
fn branch_arm_edge_resolves_through_variable() {
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Variable {
name: "$a".into(),
scope: ScopeId(0),
},
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::InferredType(InferredType::String),
span: span(0, 0, 0, 0),
});
bag.push(Witness {
attachment: WitnessAttachment::Variable {
name: "$b".into(),
scope: ScopeId(0),
},
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::InferredType(InferredType::String),
span: span(0, 0, 0, 0),
});
bag.push(Witness {
attachment: WitnessAttachment::Variable {
name: "$x".into(),
scope: ScopeId(0),
},
source: WitnessSource::Builder("branch_arm".into()),
payload: WitnessPayload::Edge(WitnessAttachment::Variable {
name: "$a".into(),
scope: ScopeId(0),
}),
span: span(0, 0, 0, 0),
});
bag.push(Witness {
attachment: WitnessAttachment::Variable {
name: "$x".into(),
scope: ScopeId(0),
},
source: WitnessSource::Builder("branch_arm".into()),
payload: WitnessPayload::Edge(WitnessAttachment::Variable {
name: "$b".into(),
scope: ScopeId(0),
}),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$x".into(),
scope: ScopeId(0),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::String)
);
}
fn wsym(sym_id: u32, payload: WitnessPayload) -> Witness {
Witness {
attachment: WitnessAttachment::Symbol(crate::file_analysis::SymbolId(sym_id)),
source: WitnessSource::Builder("arity_detection".into()),
payload,
span: span(0, 0, 0, 0),
}
}
#[test]
fn arity_zero_returns_string_default_returns_self() {
let sub_id = 42;
let mut bag = WitnessBag::new();
bag.push(wsym(
sub_id,
WitnessPayload::ReturnExpr(ReturnExpr::UnionOnArgs {
branches: vec![
(ArgGuard::Empty, ReturnExpr::Concrete(InferredType::String)),
(ArgGuard::Any, ReturnExpr::Receiver),
],
}),
));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Symbol(crate::file_analysis::SymbolId(sub_id));
let receiver = InferredType::ClassName("MojoRoute".into());
let q0 = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: Some(0),
receiver: Some(receiver.clone()),
context: None,
};
assert_eq!(
reg.query(&bag, &q0),
ReducedValue::Type(InferredType::String)
);
let q1 = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: Some(1),
receiver: Some(receiver.clone()),
context: None,
};
assert_eq!(
reg.query(&bag, &q1),
ReducedValue::Type(receiver.clone())
);
let qn = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver: Some(receiver.clone()),
context: None,
};
assert_eq!(reg.query(&bag, &qn), ReducedValue::Type(receiver));
}
#[test]
fn numeric_then_string_prefers_numeric_noop_when_class_set() {
let mut bag = WitnessBag::new();
bag.push(wvar(
"$n",
0,
WitnessPayload::Observation(TypeObservation::NumericUse),
));
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Variable {
name: "$n".into(),
scope: ScopeId(0),
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(
reg.query(&bag, &q),
ReducedValue::Type(InferredType::Numeric)
);
}
#[test]
fn plugin_override_priority_dominates_builder_inferred_type() {
let sym_id = SymbolId(0);
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Symbol(sym_id),
source: WitnessSource::Builder("inferred".into()),
payload: WitnessPayload::InferredType(InferredType::HashRef),
span: span(0, 0, 0, 0),
});
bag.push(Witness {
attachment: WitnessAttachment::Symbol(sym_id),
source: WitnessSource::Plugin("test-plugin".into()),
payload: WitnessPayload::InferredType(InferredType::ClassName("Baz".into())),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Symbol(sym_id);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
match reg.query(&bag, &q) {
ReducedValue::Type(InferredType::ClassName(name)) => {
assert_eq!(
name, "Baz",
"Plugin priority must dominate Builder-source InferredType on the same Symbol attachment"
);
}
other => panic!("expected ClassName(Baz) from PluginOverrideReducer; got {other:?}"),
}
}
#[test]
fn plugin_override_reducer_declines_builder_priority_witnesses() {
let sym_id = SymbolId(7);
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Symbol(sym_id),
source: WitnessSource::Builder("inferred".into()),
payload: WitnessPayload::InferredType(InferredType::String),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Symbol(sym_id);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None, receiver: None, context: None,
};
assert_eq!(reg.query(&bag, &q), ReducedValue::Type(InferredType::String));
}
#[test]
fn return_expr_concrete_resolves_directly() {
let sym_id = SymbolId(11);
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Symbol(sym_id),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::ReturnExpr(ReturnExpr::Concrete(InferredType::HashRef)),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Symbol(sym_id);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver: None,
context: None,
};
assert_eq!(reg.query(&bag, &q), ReducedValue::Type(InferredType::HashRef));
}
#[test]
fn return_expr_receiver_substitutes_from_query() {
let sym_id = SymbolId(12);
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Symbol(sym_id),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::ReturnExpr(ReturnExpr::Receiver),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Symbol(sym_id);
let q_with = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver: Some(InferredType::ClassName("Foo".into())),
context: None,
};
assert_eq!(
reg.query(&bag, &q_with),
ReducedValue::Type(InferredType::ClassName("Foo".into()))
);
let q_without = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver: None,
context: None,
};
assert_eq!(reg.query(&bag, &q_without), ReducedValue::None);
}
#[test]
fn return_expr_union_on_args_dispatches_by_arity() {
let sym_id = SymbolId(13);
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Symbol(sym_id),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::ReturnExpr(ReturnExpr::UnionOnArgs {
branches: vec![
(ArgGuard::Empty, ReturnExpr::Concrete(InferredType::String)),
(ArgGuard::Any, ReturnExpr::Receiver),
],
}),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Symbol(sym_id);
let q_empty = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: Some(0),
receiver: Some(InferredType::ClassName("Foo".into())),
context: None,
};
assert_eq!(
reg.query(&bag, &q_empty),
ReducedValue::Type(InferredType::String),
"arity 0 must hit the Empty arm before Any"
);
let q_any = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: Some(1),
receiver: Some(InferredType::ClassName("Foo".into())),
context: None,
};
assert_eq!(
reg.query(&bag, &q_any),
ReducedValue::Type(InferredType::ClassName("Foo".into())),
"arity 1 falls through to Any => Receiver"
);
let q_no_hint = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver: Some(InferredType::ClassName("Foo".into())),
context: None,
};
assert_eq!(
reg.query(&bag, &q_no_hint),
ReducedValue::Type(InferredType::ClassName("Foo".into())),
"no hint falls through Empty (strict Some(0) only) and \
hits Any => Receiver — the catch-all"
);
}
#[test]
fn return_expr_operator_rowof_wraps_receiver() {
let sym_id = SymbolId(14);
let mut bag = WitnessBag::new();
bag.push(Witness {
attachment: WitnessAttachment::Symbol(sym_id),
source: WitnessSource::Builder("test".into()),
payload: WitnessPayload::ReturnExpr(ReturnExpr::Operator(
ParametricOp::RowOf(Box::new(ReturnExpr::Receiver)),
)),
span: span(0, 0, 0, 0),
});
let reg = ReducerRegistry::with_defaults();
let att = WitnessAttachment::Symbol(sym_id);
let receiver = InferredType::Parametric(ParametricType::ResultSet {
base: "DBIx::Class::ResultSet".into(),
row: "Schema::Result::Users".into(),
});
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver: Some(receiver.clone()),
context: None,
};
let result = reg.query(&bag, &q);
let ReducedValue::Type(InferredType::Parametric(ParametricType::RowOf(inner))) = result else {
panic!("expected Parametric(RowOf(_)); got {:?}", result);
};
assert_eq!(*inner, receiver, "RowOf must wrap the substituted receiver");
let parametric = ParametricType::RowOf(inner);
assert_eq!(parametric.class_name(), Some("Schema::Result::Users"));
}