Skip to content

Commit c347dcb

Browse files
committed
allow with to specify effect bindings without the type/trait key
1 parent 2feae99 commit c347dcb

17 files changed

Lines changed: 554 additions & 94 deletions

File tree

crates/hir/src/analysis/ty/ty_check/env.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,10 @@ impl<'db> TyCheckEnv<'db> {
559559
}
560560
}
561561

562+
pub(super) fn insert_unkeyed_effect_binding(&mut self, binding: ProvidedEffect<'db>) {
563+
self.effect_env.insert_unkeyed(binding);
564+
}
565+
562566
pub(super) fn effect_candidates_in_scope(
563567
&self,
564568
key_path: PathId<'db>,
@@ -604,8 +608,32 @@ impl<'db> TyCheckEnv<'db> {
604608
_ => {}
605609
}
606610
}
611+
612+
for provided in frame.unkeyed.iter() {
613+
if provided.ty.has_invalid(self.db) {
614+
continue;
615+
}
616+
match &path_res {
617+
PathRes::Ty(req) | PathRes::TyAlias(_, req) => {
618+
if provided.ty.is_ty_var(self.db)
619+
|| req.base_ty(self.db).as_scope(self.db)
620+
== provided.ty.base_ty(self.db).as_scope(self.db)
621+
{
622+
out.push(*provided);
623+
}
624+
}
625+
PathRes::Trait(_) => {
626+
// Trait satisfaction is checked at the call site so we
627+
// can consider type arguments and current assumptions.
628+
out.push(*provided);
629+
}
630+
_ => {}
631+
}
632+
}
607633
}
608634

635+
out.sort_by_key(|p| (p.ty, p.is_mut));
636+
out.dedup_by_key(|p| (p.ty, p.is_mut));
609637
out
610638
}
611639

@@ -983,6 +1011,7 @@ pub(super) enum EffectKey<'db> {
9831011
#[derive(Default)]
9841012
struct EffectFrame<'db> {
9851013
bindings: FxHashMap<EffectKey<'db>, ProvidedEffect<'db>>,
1014+
unkeyed: Vec<ProvidedEffect<'db>>,
9861015
}
9871016

9881017
pub(super) struct EffectEnv<'db> {
@@ -1014,6 +1043,14 @@ impl<'db> EffectEnv<'db> {
10141043
.insert(key, binding);
10151044
}
10161045

1046+
pub fn insert_unkeyed(&mut self, binding: ProvidedEffect<'db>) {
1047+
self.frames
1048+
.last_mut()
1049+
.expect("EffectEnv must always have at least one frame")
1050+
.unkeyed
1051+
.push(binding);
1052+
}
1053+
10171054
pub fn lookup(&self, key: EffectKey<'db>) -> Option<ProvidedEffect<'db>> {
10181055
self.frames
10191056
.iter()

crates/hir/src/analysis/ty/ty_check/expr.rs

Lines changed: 146 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use either::Either;
2+
use smallvec1::SmallVec;
23

34
use crate::core::hir_def::{
45
ArithBinOp, BinOp, CallableDef, Expr, ExprId, FieldIndex, IdentId, Partial, Pat, PatId, PathId,
@@ -43,6 +44,7 @@ use crate::analysis::{
4344
},
4445
};
4546

47+
#[derive(Debug, Clone, Copy)]
4648
enum EffectRequirement<'db> {
4749
Type(TyId<'db>),
4850
Trait(TraitInstId<'db>),
@@ -203,9 +205,6 @@ impl<'db> TyChecker<'db> {
203205

204206
for binding in bindings {
205207
let value_prop = self.check_expr_unknown(binding.value);
206-
let Some(key_path) = binding.key_path.to_opt() else {
207-
continue;
208-
};
209208

210209
let is_mut = value_prop
211210
.binding
@@ -220,7 +219,16 @@ impl<'db> TyChecker<'db> {
220219
is_mut,
221220
};
222221

223-
self.env.insert_effect_binding(key_path, provided);
222+
match binding.key_path {
223+
Some(key_path) => {
224+
if let Some(key_path) = key_path.to_opt() {
225+
self.env.insert_effect_binding(key_path, provided);
226+
}
227+
}
228+
None => {
229+
self.env.insert_unkeyed_effect_binding(provided);
230+
}
231+
}
224232
}
225233

226234
let result = self.check_expr(body_expr, expected);
@@ -294,7 +302,8 @@ impl<'db> TyChecker<'db> {
294302
return;
295303
}
296304

297-
let call_span = expr.span(self.body());
305+
let body = self.body();
306+
let call_span = expr.span(body);
298307
let callee_assumptions = collect_func_def_constraints(self.db, func.into(), true)
299308
.instantiate_identity()
300309
.extend_all_bounds(self.db);
@@ -321,8 +330,135 @@ impl<'db> TyChecker<'db> {
321330
let cands =
322331
self.env
323332
.effect_candidates_in_scope(key_path, func.scope(), callee_assumptions);
324-
let provided = match cands.as_slice() {
333+
if cands.is_empty() {
334+
let diag = BodyDiag::MissingEffect {
335+
primary: call_span.clone().into(),
336+
func,
337+
key: key_path,
338+
};
339+
self.push_diag(diag);
340+
continue;
341+
}
342+
343+
let required_mut = effect.is_mut(self.db);
344+
let mut_compatible: SmallVec<[ProvidedEffect<'db>; 2]> = cands
345+
.iter()
346+
.copied()
347+
.filter(|p| !required_mut || p.is_mut)
348+
.collect();
349+
350+
let provided_span = |provided: ProvidedEffect<'db>| match provided.origin {
351+
EffectOrigin::With { value_expr } => Some(value_expr.span(body).into()),
352+
EffectOrigin::Param { .. } => None,
353+
};
354+
355+
if mut_compatible.is_empty() {
356+
// Effects are present but don't satisfy mutability.
357+
let diag = BodyDiag::EffectMutabilityMismatch {
358+
primary: call_span.clone().into(),
359+
func,
360+
key: key_path,
361+
provided_span: cands.first().copied().and_then(provided_span),
362+
};
363+
self.push_diag(diag);
364+
continue;
365+
}
366+
367+
let ingot = self.env.body().top_mod(self.db).ingot(self.db);
368+
let mut viable: SmallVec<[(ProvidedEffect<'db>, EffectRequirement<'db>); 2]> =
369+
SmallVec::new();
370+
for provided in mut_compatible.iter().copied() {
371+
let Some(requirement) = self.resolve_effect_requirement(
372+
key_path,
373+
callable,
374+
func.scope(),
375+
callee_assumptions,
376+
provided.ty,
377+
) else {
378+
continue;
379+
};
380+
381+
match requirement {
382+
EffectRequirement::Type(expected) => {
383+
let snapshot = self.table.snapshot();
384+
let ok = self.table.unify(expected, provided.ty).is_ok();
385+
self.table.rollback_to(snapshot);
386+
if ok {
387+
viable.push((provided, EffectRequirement::Type(expected)));
388+
}
389+
}
390+
EffectRequirement::Trait(trait_req) => {
391+
let canonical = Canonicalized::new(self.db, trait_req);
392+
match is_goal_satisfiable(
393+
self.db,
394+
ingot,
395+
canonical.value,
396+
self.env.assumptions(),
397+
) {
398+
GoalSatisfiability::UnSat(_) => {}
399+
GoalSatisfiability::ContainsInvalid => {}
400+
_ => viable.push((provided, EffectRequirement::Trait(trait_req))),
401+
}
402+
}
403+
}
404+
}
405+
406+
let (provided, requirement) = match viable.as_slice() {
325407
[] => {
408+
// Preserve detailed mismatch diagnostics when there's a single candidate.
409+
if mut_compatible.len() == 1 {
410+
let provided = mut_compatible[0];
411+
let Some(requirement) = self.resolve_effect_requirement(
412+
key_path,
413+
callable,
414+
func.scope(),
415+
callee_assumptions,
416+
provided.ty,
417+
) else {
418+
continue;
419+
};
420+
match requirement {
421+
EffectRequirement::Type(expected) => {
422+
if self.table.unify(expected, provided.ty).is_err() {
423+
let diag = BodyDiag::EffectTypeMismatch {
424+
primary: call_span.clone().into(),
425+
func,
426+
key: key_path,
427+
expected,
428+
given: provided.ty,
429+
provided_span: provided_span(provided),
430+
};
431+
self.push_diag(diag);
432+
}
433+
}
434+
EffectRequirement::Trait(trait_req) => {
435+
let canonical = Canonicalized::new(self.db, trait_req);
436+
match is_goal_satisfiable(
437+
self.db,
438+
ingot,
439+
canonical.value,
440+
self.env.assumptions(),
441+
) {
442+
GoalSatisfiability::UnSat(_) => {
443+
let diag = BodyDiag::EffectTraitUnsatisfied {
444+
primary: call_span.clone().into(),
445+
func,
446+
key: key_path,
447+
trait_req,
448+
given: provided.ty,
449+
provided_span: provided_span(provided),
450+
};
451+
self.push_diag(diag);
452+
}
453+
GoalSatisfiability::ContainsInvalid => {}
454+
_ => {}
455+
}
456+
}
457+
}
458+
continue;
459+
}
460+
461+
// Multiple candidates exist, but none can satisfy this effect.
326462
let diag = BodyDiag::MissingEffect {
327463
primary: call_span.clone().into(),
328464
func,
@@ -331,7 +467,7 @@ impl<'db> TyChecker<'db> {
331467
self.push_diag(diag);
332468
continue;
333469
}
334-
[one] => *one,
470+
[(provided, requirement)] => (*provided, *requirement),
335471
_ => {
336472
let diag = BodyDiag::AmbiguousEffect {
337473
primary: call_span.clone().into(),
@@ -343,74 +479,37 @@ impl<'db> TyChecker<'db> {
343479
}
344480
};
345481

346-
if effect.is_mut(self.db) && !provided.is_mut {
347-
let provided_span = match provided.origin {
348-
EffectOrigin::With { value_expr } => Some(value_expr.span(self.body()).into()),
349-
EffectOrigin::Param { .. } => None,
350-
};
351-
let diag = BodyDiag::EffectMutabilityMismatch {
352-
primary: call_span.clone().into(),
353-
func,
354-
key: key_path,
355-
provided_span,
356-
};
357-
self.push_diag(diag);
358-
continue;
359-
}
360-
361-
let Some(requirement) = self.resolve_effect_requirement(
362-
key_path,
363-
callable,
364-
func.scope(),
365-
callee_assumptions,
366-
provided.ty,
367-
) else {
368-
continue;
369-
};
370-
371482
match requirement {
372483
EffectRequirement::Type(expected) => {
484+
// Commit unification for the selected candidate.
373485
if self.table.unify(expected, provided.ty).is_err() {
374-
let provided_span = match provided.origin {
375-
EffectOrigin::With { value_expr } => {
376-
Some(value_expr.span(self.body()).into())
377-
}
378-
EffectOrigin::Param { .. } => None,
379-
};
380486
let diag = BodyDiag::EffectTypeMismatch {
381487
primary: call_span.clone().into(),
382488
func,
383489
key: key_path,
384490
expected,
385491
given: provided.ty,
386-
provided_span,
492+
provided_span: provided_span(provided),
387493
};
388494
self.push_diag(diag);
389495
}
390496
}
391497
EffectRequirement::Trait(trait_req) => {
392498
let canonical = Canonicalized::new(self.db, trait_req);
393-
let ingot = self.env.body().top_mod(self.db).ingot(self.db);
394499
match is_goal_satisfiable(
395500
self.db,
396501
ingot,
397502
canonical.value,
398503
self.env.assumptions(),
399504
) {
400505
GoalSatisfiability::UnSat(_) => {
401-
let provided_span = match provided.origin {
402-
EffectOrigin::With { value_expr } => {
403-
Some(value_expr.span(self.body()).into())
404-
}
405-
EffectOrigin::Param { .. } => None,
406-
};
407506
let diag = BodyDiag::EffectTraitUnsatisfied {
408507
primary: call_span.clone().into(),
409508
func,
410509
key: key_path,
411510
trait_req,
412511
given: provided.ty,
413-
provided_span,
512+
provided_span: provided_span(provided),
414513
};
415514
self.push_diag(diag);
416515
}

crates/hir/src/core/hir_def/expr.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ impl<'db> Field<'db> {
200200

201201
#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)]
202202
pub struct WithBinding<'db> {
203-
pub key_path: Partial<PathId<'db>>, // Unresolved path key
203+
/// Effect key path (e.g. `Ctx` / `Storage<u8>`). When absent, the binding is
204+
/// shorthand and the key is inferred from the bound value and effect usage.
205+
pub key_path: Option<Partial<PathId<'db>>>,
204206
pub value: ExprId,
205207
}

crates/hir/src/core/lower/expr.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,18 @@ impl<'db> Expr<'db> {
172172
}
173173

174174
ast::ExprKind::With(with_) => {
175-
// Lower `with (K = v, ..) { body }` into HIR::Expr::With(bindings, body)
175+
// Lower `with (K = v, ..) { body }` and `with (v, ..) { body }`
176+
// into HIR::Expr::With(bindings, body)
176177
let mut bindings = Vec::new();
177178
if let Some(params) = with_.params() {
178179
for p in params {
179180
let value = Self::push_to_body_opt(ctxt, p.value_expr());
180-
// Lower key path directly so multi-segment paths are preserved.
181-
let key_path = PathId::lower_ast_partial(ctxt.f_ctxt, p.path());
181+
let key_path = if p.eq().is_some() {
182+
// Lower key path directly so multi-segment paths are preserved.
183+
Some(PathId::lower_ast_partial(ctxt.f_ctxt, p.path()))
184+
} else {
185+
None
186+
};
182187
bindings.push(super::super::hir_def::expr::WithBinding { key_path, value });
183188
}
184189
}

crates/hir/test_files/ty_check/contract_effects.fe

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ fn test_withdraw() {
7474
let ctx = MockCtx { caller: Address { inner: 0xbeef } }
7575
let owner = Address { inner: 0xfe }
7676
let mut balance = 100
77-
with (Ctx = ctx, Address = owner, u256 = balance) { // xxx with (ctx, owner, balance)
77+
with (ctx, owner, balance) {
7878
let ok = withdraw(amount: 100)
7979
}
8080
}

0 commit comments

Comments
 (0)