Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support const evaulation #596

Closed
wants to merge 17 commits into from
Closed
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -12,3 +12,4 @@ chalk-parse/src/parser.rs

## IDE files
/.idea/
/.vscode/
27 changes: 24 additions & 3 deletions chalk-engine/src/slg.rs
Original file line number Diff line number Diff line change
@@ -448,7 +448,7 @@ impl<I: Interner> MayInvalidate<'_, I> {
}
}

/// Returns true if the two consts could be unequal.
/// Returns true if the two lifetimes could be unequal.
fn aggregate_lifetimes(&mut self, _: &Lifetime<I>, _: &Lifetime<I>) -> bool {
true
}
@@ -495,8 +495,29 @@ impl<I: Interner> MayInvalidate<'_, I> {
!c1.const_eq(new_ty, c2, interner)
}

// Only variants left are placeholder = concrete, which always fails
(ConstValue::Placeholder(_), _) | (ConstValue::Concrete(_), _) => true,
(ConstValue::Concrete(c), ConstValue::Unevaluated(u))
| (ConstValue::Unevaluated(u), ConstValue::Concrete(c)) => {
if let Ok(ev) = u.try_eval(new_ty, interner) {
!c.const_eq(new_ty, &ev, interner)
} else {
true
}
}

(ConstValue::Unevaluated(u1), ConstValue::Unevaluated(u2)) => {
if let (Ok(c1), Ok(c2)) = (
u1.try_eval(new_ty, interner),
u2.try_eval(current_ty, interner),
) {
!c1.const_eq(new_ty, &c2, interner)
} else {
true
}
}

// Only variants left are placeholder = concrete, which always fails, and
// placeholder = unevaluated, which we can't know for sure
(ConstValue::Placeholder(_), _) | (_, ConstValue::Placeholder(_)) => true,
}
}

38 changes: 38 additions & 0 deletions chalk-engine/src/slg/aggregate.rs
Original file line number Diff line number Diff line change
@@ -483,6 +483,44 @@ impl<I: Interner> AntiUnifier<'_, '_, I> {
}
}

(ConstValue::Concrete(e), ConstValue::Unevaluated(u))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial thought: Will we ever actually give an answer with an Unevaluated const?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why not. Chalk can return associated type projections in answers, and an unevalutated const is basically a projection.

| (ConstValue::Unevaluated(u), ConstValue::Concrete(e)) => {
let ev = match u.try_eval(&ty, interner) {
Ok(ev) => ev,
Err(_) => return self.new_const_variable(ty),
};

if e.const_eq(&ty, &ev, interner) {
ConstData {
ty: ty.clone(),
value: ConstValue::Concrete(ev),
}
.intern(interner)
} else {
self.new_const_variable(ty)
}
}

(ConstValue::Unevaluated(u1), ConstValue::Unevaluated(u2)) => {
match (u1.try_eval(&ty, interner), u2.try_eval(&ty, interner)) {
(Ok(e1), Ok(e2)) => {
if e1.const_eq(&ty, &e2, interner) {
ConstData {
ty: ty.clone(),
value: ConstValue::Concrete(e1),
}
.intern(interner)
} else {
self.new_const_variable(ty)
}
}

(Err(ConstEvalError::TooGeneric), _) | (_, Err(ConstEvalError::TooGeneric)) => {
self.new_const_variable(ty)
}
}
}

(ConstValue::Placeholder(_), _) | (_, ConstValue::Placeholder(_)) => {
self.new_const_variable(ty)
}
37 changes: 34 additions & 3 deletions chalk-engine/src/slg/resolvent.rs
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ use tracing::{debug, instrument};
//
// From EWFS:
//
// Let G be an X-clause A :- D | L1,...Ln, where N > 0, and Li be selected atom.
// Let G be an X-clause A :- D | L1,...Ln, where n > 0, and Li be selected atom.
//
// Let C be an X-clause with no delayed literals. Let
//
@@ -42,7 +42,7 @@ use tracing::{debug, instrument};
//
// Then:
//
// S(A :- D | L1...Li-1, L1'...L'm, Li+1...Ln)
// S(A :- D | L1...Li-1, L'1...L'm, Li+1...Ln)
//
// is the SLG resolvent of G with C.

@@ -507,14 +507,45 @@ impl<'i, I: Interner> Zipper<'i, I> for AnswerSubstitutor<'i, I> {
Ok(())
}

(ConstValue::Concrete(c), ConstValue::Unevaluated(u))
| (ConstValue::Unevaluated(u), ConstValue::Concrete(c)) => {
match u.try_eval(answer_ty, interner) {
Ok(ev) => assert!(c.const_eq(answer_ty, &ev, interner)),

Err(ConstEvalError::TooGeneric) => panic!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here...feels like we should have already have caught this before and the answer itself (or the goal) shouldn't have unevaluated consts. (Well...maybe the goal would?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out that unevaluated const expressions can appear in the answer and the goal in the resolvent if we so much as write impl Foo<3?> for Baz {}.

"structural mismatch between answer `{:?}` and pending goal `{:?}`",
answer, pending,
),
}
Ok(())
}

(ConstValue::Unevaluated(u1), ConstValue::Unevaluated(u2)) => {
match (
u1.try_eval(answer_ty, interner),
u2.try_eval(answer_ty, interner),
) {
(Ok(c1), Ok(c2)) => assert!(c1.const_eq(answer_ty, &c2, interner)),

(Err(ConstEvalError::TooGeneric), _) | (_, Err(ConstEvalError::TooGeneric)) => {
panic!(
"structural mismatch between answer `{:?}` and pending goal `{:?}`",
answer, pending,
)
}
}
Ok(())
}

(ConstValue::InferenceVar(_), _) | (_, ConstValue::InferenceVar(_)) => panic!(
"unexpected inference var in answer `{:?}` or pending goal `{:?}`",
answer, pending,
),

(ConstValue::BoundVar(_), _)
| (ConstValue::Placeholder(_), _)
| (ConstValue::Concrete(_), _) => panic!(
| (ConstValue::Concrete(_), _)
| (ConstValue::Unevaluated(_), _) => panic!(
"structural mismatch between answer `{:?}` and pending goal `{:?}`",
answer, pending,
),
31 changes: 30 additions & 1 deletion chalk-integration/src/interner.rs
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ use crate::tls;
use chalk_ir::interner::{HasInterner, Interner};
use chalk_ir::{
AdtId, AliasTy, ApplicationTy, AssocTypeId, CanonicalVarKind, CanonicalVarKinds, ConstData,
Constraint, FnDefId, Goals, InEnvironment, Lifetime, OpaqueTy, OpaqueTyId,
ConstEvalError, Constraint, FnDefId, Goals, InEnvironment, Lifetime, OpaqueTy, OpaqueTyId,
ProgramClauseImplication, ProgramClauses, ProjectionTy, QuantifiedWhereClauses,
SeparatorTraitRef, Substitution, TraitId, Ty, VariableKind, VariableKinds,
};
@@ -34,6 +34,23 @@ pub enum ChalkFnAbi {
C,
}

/// A mock unevaluated const expression.
/// `Some(n)` evaluates to `n`,
/// and `None` evaluates to a specific but unknown constant.
/// This exists for formatting purposes.
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct UnevaluatedConstData(pub Option<u32>);

impl Debug for UnevaluatedConstData {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self.0 {
Some(n) => write!(fmt, "{}?", n)?,
None => write!(fmt, "?")?,
}
Ok(())
}
}

/// The default "interner" and the only interner used by chalk
/// itself. In this interner, no interning actually occurs.
#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
@@ -44,6 +61,7 @@ impl Interner for ChalkIr {
type InternedLifetime = LifetimeData<ChalkIr>;
type InternedConst = Arc<ConstData<ChalkIr>>;
type InternedConcreteConst = u32;
type InternedUnevaluatedConst = UnevaluatedConstData;
type InternedGenericArg = GenericArgData<ChalkIr>;
type InternedGoal = Arc<GoalData<ChalkIr>>;
type InternedGoals = Vec<Goal<ChalkIr>>;
@@ -240,6 +258,17 @@ impl Interner for ChalkIr {
c1 == c2
}

fn try_eval_const(
&self,
_ty: &Arc<TyData<ChalkIr>>,
constant: &UnevaluatedConstData,
) -> Result<u32, ConstEvalError> {
match constant.0 {
Some(c) => Ok(c),
None => Err(ConstEvalError::TooGeneric),
}
}

fn intern_generic_arg(&self, generic_arg: GenericArgData<ChalkIr>) -> GenericArgData<ChalkIr> {
generic_arg
}
10 changes: 10 additions & 0 deletions chalk-integration/src/lowering.rs
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ use string_cache::DefaultAtom as Atom;
use tracing::{debug, instrument};

use crate::error::RustIrError;
use crate::interner::UnevaluatedConstData;
use crate::program::Program as LoweredProgram;
use crate::{Identifier as Ident, RawId, TypeKind, TypeSort};

@@ -1763,11 +1764,20 @@ impl LowerConst for Const {
})
.map(|c| c.clone())
}

Const::Value(value) => Ok(chalk_ir::ConstData {
ty: get_type_of_u32(),
value: chalk_ir::ConstValue::Concrete(chalk_ir::ConcreteConst { interned: *value }),
}
.intern(interner)),

Const::Unevaluated(unev) => Ok(chalk_ir::ConstData {
ty: get_type_of_u32(),
value: chalk_ir::ConstValue::Unevaluated(chalk_ir::UnevaluatedConst {
interned: UnevaluatedConstData(*unev),
}),
}
.intern(interner)),
}
}
}
7 changes: 7 additions & 0 deletions chalk-ir/src/debug.rs
Original file line number Diff line number Diff line change
@@ -59,6 +59,12 @@ impl<I: Interner> Debug for ConcreteConst<I> {
}
}

impl<I: Interner> Debug for UnevaluatedConst<I> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), Error> {
write!(fmt, "{:?}", self.interned)
}
}

impl<I: Interner> Debug for GenericArg<I> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), Error> {
I::debug_generic_arg(self, fmt).unwrap_or_else(|| write!(fmt, "{:?}", self.interned))
@@ -320,6 +326,7 @@ impl<I: Interner> Debug for ConstData<I> {
ConstValue::InferenceVar(var) => write!(fmt, "{:?}", var),
ConstValue::Placeholder(index) => write!(fmt, "{:?}", index),
ConstValue::Concrete(evaluated) => write!(fmt, "{:?}", evaluated),
ConstValue::Unevaluated(expr) => write!(fmt, "{:?}", expr),
}
}
}
10 changes: 10 additions & 0 deletions chalk-ir/src/fold.rs
Original file line number Diff line number Diff line change
@@ -550,6 +550,16 @@ where
}),
}
.intern(folder.target_interner())),

ConstValue::Unevaluated(unev) => Ok(ConstData {
ty: fold_ty()?,
value: ConstValue::Unevaluated(UnevaluatedConst {
interned: folder
.target_interner()
.transfer_unevaluated_const(&unev.interned),
}),
}
.intern(folder.target_interner())),
}
}
}
33 changes: 31 additions & 2 deletions chalk-ir/src/interner.rs
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ use crate::Ty;
use crate::TyData;
use crate::VariableKind;
use crate::VariableKinds;
use crate::{Const, ConstData};
use crate::{Const, ConstData, ConstEvalError};
use std::fmt::{self, Debug};
use std::hash::Hash;
use std::marker::PhantomData;
@@ -93,6 +93,15 @@ pub trait Interner: Debug + Copy + Eq + Ord + Hash {
/// evaluated consts.
type InternedConcreteConst: Debug + Clone + Eq + Hash;

/// "Interned" representation of an unevaluated const expression. In normal user code,
/// `Self::InternedUnevaluatedConst` is not referenced. Instead,
/// we refer to `UnevaluatedConst<Self>`, which wraps this type.
///
/// `InternedUnevaluatedConst` instances are not created by chalk,
/// it can only evaluate them with the [`try_eval_const`] method
/// and check for (syntactic for now) equality between them.
type InternedUnevaluatedConst: Debug + Clone + Eq + Hash;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we anticipate that this will carry a "substitutions"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect not, right?


/// "Interned" representation of a "generic parameter", which can
/// be either a type or a lifetime. In normal user code,
/// `Self::InternedGenericArg` is not referenced. Instead, we refer to
@@ -456,14 +465,21 @@ pub trait Interner: Debug + Copy + Eq + Ord + Hash {
/// Lookup the `ConstData` that was interned to create a `InternedConst`.
fn const_data<'a>(&self, constant: &'a Self::InternedConst) -> &'a ConstData<Self>;

/// Deterermine whether two concrete const values are equal.
/// Determine whether two concrete const values are equal.
fn const_eq(
&self,
ty: &Self::InternedType,
c1: &Self::InternedConcreteConst,
c2: &Self::InternedConcreteConst,
) -> bool;

/// Attempt to evaluate a const to a concrete const.
fn try_eval_const(
&self,
ty: &Self::InternedType,
constant: &Self::InternedUnevaluatedConst,
) -> Result<Self::InternedConcreteConst, ConstEvalError>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of expect this to take a set of substitutions. Consider how the rustc variant looks.


/// Create an "interned" parameter from `data`. This is not
/// normally invoked directly; instead, you invoke
/// `GenericArgData::intern` (which will ultimately call this
@@ -634,6 +650,12 @@ pub trait TargetInterner<I: Interner>: Interner {
const_evaluated: &I::InternedConcreteConst,
) -> Self::InternedConcreteConst;

/// Transfer unevaluated constant expressions to the target interner.
fn transfer_unevaluated_const(
&self,
const_unevaluated: &I::InternedUnevaluatedConst,
) -> Self::InternedUnevaluatedConst;

/// Transfer function ABI to the target interner.
fn transfer_abi(abi: I::FnAbi) -> Self::FnAbi;
}
@@ -666,6 +688,13 @@ impl<I: Interner> TargetInterner<I> for I {
const_evaluated.clone()
}

fn transfer_unevaluated_const(
&self,
const_unevaluated: &I::InternedUnevaluatedConst,
) -> Self::InternedUnevaluatedConst {
const_unevaluated.clone()
}

fn transfer_abi(abi: I::FnAbi) -> Self::FnAbi {
abi
}
Loading