diff --git a/crates/codegen/tests/fixtures/sonatina_ir/for_array.snap b/crates/codegen/tests/fixtures/sonatina_ir/for_array.snap index 3523b12f5f..c61d722682 100644 --- a/crates/codegen/tests/fixtures/sonatina_ir/for_array.snap +++ b/crates/codegen/tests/fixtures/sonatina_ir/for_array.snap @@ -18,29 +18,32 @@ func private %for_array_sum() -> i64 { jump block2; block2: - v51.i64 = phi (0.i64 block1) (v37 block7); - v30.i256 = phi (0.i256 block1) (v45 block7); + v51.i64 = phi (0.i64 block1) (v37 block8); + v30.i256 = phi (0.i256 block1) (v45 block8); v32.i1 = lt v30 v29; - br v32 block3 block4; + br v32 block3 block5; block3: v35.i64 = call %get v26 v30; (v37.i64, v38.i1) = uaddo v51 v35; - br v38 block5 block6; + br v38 block6 block7; block4: - return v51; + (v45.i256, v46.i1) = uaddo v30 1.i256; + br v46 block6 block8; block5: + return v51; + + block6: mstore 0.i256 35408467139433450592217433187231851964531694900788300625387963629091585785856.i256 i256; mstore 4.i256 17.i256 i256; evm_revert 0.i256 36.i256; - block6: - (v45.i256, v46.i1) = uaddo v30 1.i256; - br v46 block5 block7; - block7: + jump block4; + + block8: jump block2; } diff --git a/crates/codegen/tests/fixtures/sonatina_ir/for_array_large.snap b/crates/codegen/tests/fixtures/sonatina_ir/for_array_large.snap index ef686ec997..f837cc8547 100644 --- a/crates/codegen/tests/fixtures/sonatina_ir/for_array_large.snap +++ b/crates/codegen/tests/fixtures/sonatina_ir/for_array_large.snap @@ -18,29 +18,32 @@ func private %for_array_large_sum() -> i64 { jump block2; block2: - v36.i64 = phi (0.i64 block1) (v22 block7); - v15.i256 = phi (0.i256 block1) (v30 block7); + v36.i64 = phi (0.i64 block1) (v22 block8); + v15.i256 = phi (0.i256 block1) (v30 block8); v17.i1 = lt v15 v14; - br v17 block3 block4; + br v17 block3 block5; block3: v20.i64 = call %get v11 v15; (v22.i64, v23.i1) = uaddo v36 v20; - br v23 block5 block6; + br v23 block6 block7; block4: - return v36; + (v30.i256, v31.i1) = uaddo v15 1.i256; + br v31 block6 block8; block5: + return v36; + + block6: mstore 0.i256 35408467139433450592217433187231851964531694900788300625387963629091585785856.i256 i256; mstore 4.i256 17.i256 i256; evm_revert 0.i256 36.i256; - block6: - (v30.i256, v31.i1) = uaddo v15 1.i256; - br v31 block5 block7; - block7: + jump block4; + + block8: jump block2; } diff --git a/crates/codegen/tests/fixtures/sonatina_ir/for_range.snap b/crates/codegen/tests/fixtures/sonatina_ir/for_range.snap index 24789f717c..4831569e62 100644 --- a/crates/codegen/tests/fixtures/sonatina_ir/for_range.snap +++ b/crates/codegen/tests/fixtures/sonatina_ir/for_range.snap @@ -19,29 +19,32 @@ func private %for_range_sum() -> i256 { jump block2; block2: - v25.i256 = phi (0.i256 block1) (v11 block7); - v4.i256 = phi (0.i256 block1) (v19 block7); + v25.i256 = phi (0.i256 block1) (v11 block8); + v4.i256 = phi (0.i256 block1) (v19 block8); v6.i1 = lt v4 v3; - br v6 block3 block4; + br v6 block3 block5; block3: v9.i256 = call %get v2 v4; (v11.i256, v12.i1) = uaddo v25 v9; - br v12 block5 block6; + br v12 block6 block7; block4: - return v25; + (v19.i256, v20.i1) = uaddo v4 1.i256; + br v20 block6 block8; block5: + return v25; + + block6: mstore 0.i256 35408467139433450592217433187231851964531694900788300625387963629091585785856.i256 i256; mstore 4.i256 17.i256 i256; evm_revert 0.i256 36.i256; - block6: - (v19.i256, v20.i1) = uaddo v4 1.i256; - br v20 block5 block7; - block7: + jump block4; + + block8: jump block2; } diff --git a/crates/codegen/tests/fixtures/sonatina_ir/range_bounds.snap b/crates/codegen/tests/fixtures/sonatina_ir/range_bounds.snap index 7ca2800d63..59db96742a 100644 --- a/crates/codegen/tests/fixtures/sonatina_ir/range_bounds.snap +++ b/crates/codegen/tests/fixtures/sonatina_ir/range_bounds.snap @@ -58,29 +58,32 @@ func private %sum_const() -> i256 { jump block2; block2: - v20.i256 = phi (0.i256 block1) (v9 block7); - v3.i256 = phi (0.i256 block1) (v16 block7); + v20.i256 = phi (0.i256 block1) (v9 block8); + v3.i256 = phi (0.i256 block1) (v16 block8); v5.i1 = lt v3 v2; - br v5 block3 block4; + br v5 block3 block5; block3: v7.i256 = call %get v3; (v9.i256, v10.i1) = uaddo v20 v7; - br v10 block5 block6; + br v10 block6 block7; block4: - return v20; + (v16.i256, v17.i1) = uaddo v3 1.i256; + br v17 block6 block8; block5: + return v20; + + block6: mstore 0.i256 35408467139433450592217433187231851964531694900788300625387963629091585785856.i256 i256; mstore 4.i256 17.i256 i256; evm_revert 0.i256 36.i256; - block6: - (v16.i256, v17.i1) = uaddo v3 1.i256; - br v17 block5 block7; - block7: + jump block4; + + block8: jump block2; } diff --git a/crates/fe-web/vendor/tree-sitter-fe.wasm b/crates/fe-web/vendor/tree-sitter-fe.wasm index 69f1c53b5a..8a4259fad7 100755 Binary files a/crates/fe-web/vendor/tree-sitter-fe.wasm and b/crates/fe-web/vendor/tree-sitter-fe.wasm differ diff --git a/crates/fe-web/vendor/tree-sitter-fe.wasm.inputs b/crates/fe-web/vendor/tree-sitter-fe.wasm.inputs index 56363ad7ce..cb8fff5622 100644 --- a/crates/fe-web/vendor/tree-sitter-fe.wasm.inputs +++ b/crates/fe-web/vendor/tree-sitter-fe.wasm.inputs @@ -1 +1 @@ -fnv1a64-relpath-v1:49f6b7789718fee9 +fnv1a64-relpath-v1:69ba2fb78f2d1ade diff --git a/crates/fe/tests/fixtures/fe_test/for_continue_advances.fe b/crates/fe/tests/fixtures/fe_test/for_continue_advances.fe new file mode 100644 index 0000000000..45e0dd08ff --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/for_continue_advances.fe @@ -0,0 +1,162 @@ +#[test] +fn unconditional_continue_advances_sequence() { + let mut visits: usize = 0 + for i in 0..5 { + visits += 1 + assert!(visits <= 5) + continue + } + assert!(visits == 5) +} + +#[test] +fn nested_continue_advances_only_inner_sequence() { + let mut visits: usize = 0 + let mut sum: usize = 0 + for outer in 0..3 { + for inner in 0..4 { + visits += 1 + assert!(visits <= 12) + if inner % 2 == 0 { continue } + sum += outer + inner + } + } + assert!(visits == 12) + assert!(sum == 18) +} + +#[test] +fn continue_and_break_preserve_sequence_order() { + let mut visits: usize = 0 + for i in 0..8 { + visits += 1 + assert!(visits <= 5) + if i < 4 { continue } + assert!(i == 4) + break + } + assert!(visits == 5) +} + +#[test] +fn while_continue_does_not_advance_enclosing_sequence() { + let mut visits: usize = 0 + let mut sum: usize = 0 + for outer in 0..3 { + let mut inner: usize = 0 + while inner < 3 { + inner += 1 + visits += 1 + assert!(visits <= 9) + if inner < 3 { continue } + sum += outer + } + } + assert!(visits == 9) + assert!(sum == 3) +} + +#[test] +fn unconditional_break_leaves_after_first_element() { + let mut visits: usize = 0 + for i in 0..5 { + visits += 1 + break + } + assert!(visits == 1) +} + +fn first(_ values: [usize; 3]) -> usize { + for value in values { + return value + } + 0 +} + +#[test] +fn unconditional_return_yields_first_element() { + assert!(first([7, 8, 9]) == 7) +} + +fn leading_zero(_ values: [usize; 3]) -> bool { + for value in values { + if value == 0 { return true } else { break } + } + false +} + +#[test] +fn exiting_on_every_branch_inspects_only_first_element() { + assert!(leading_zero([0, 5, 5])) + assert!(!leading_zero([5, 0, 0])) +} + +fn abort() -> ! { + let code: u256 = 1 + std::evm::revert(code) +} + +#[test(should_revert)] +fn diverging_body_reverts() { + for i in 0..3 { + abort() + } +} + +fn ignore(_ value: usize, _ other: usize) {} + +#[test(should_revert)] +fn continue_after_diverging_argument_is_unreachable() { + for i in 0..3 { + ignore(abort(), { continue }) + } +} + +fn first_even(_ values: [u256; 5]) -> u256 { + for value in values { + if value % 2 == 1 { + continue + } else { + return value + } + } + return 0 +} + +#[test] +fn continue_arm_advances_when_other_arms_exit() { + assert!(first_even([1, 3, 6, 5, 8]) == 6) + assert!(first_even([1, 3, 5, 7, 9]) == 0) +} + +#[test] +fn continue_or_break_in_every_arm() { + let mut visits: usize = 0 + let mut last: usize = 0 + for i in 0..8 { + visits += 1 + assert!(visits <= 4) + last = i + match i < 3 { + true => { continue } + false => { break } + } + } + assert!(visits == 4) + assert!(last == 3) +} + +#[test] +fn only_inner_body_always_exits() { + let mut outer_visits: usize = 0 + let mut inner_visits: usize = 0 + for outer in 0..3 { + outer_visits += 1 + for inner in 0..4 { + inner_visits += 1 + break + } + } + assert!(outer_visits == 3) + assert!(inner_visits == 3) +} diff --git a/crates/fe/tests/fixtures/fe_test/generic_trait_arg_impl_assoc_type.fe b/crates/fe/tests/fixtures/fe_test/generic_trait_arg_impl_assoc_type.fe new file mode 100644 index 0000000000..45120f0c42 --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/generic_trait_arg_impl_assoc_type.fe @@ -0,0 +1,34 @@ +pub trait T { + type Out + fn f(_ a: A) -> Self::Out +} + +pub struct X {} + +pub struct Wrap

{} + +impl T for X { + type Out = u256 + fn f(_ a: u256) -> u256 { + a + 1 + } +} + +// The impl stays generic over the trait's own parameter `A`. +impl> T for Wrap

{ + type Out = (

>::Out, A) + fn f(_ a: A) -> Self::Out { + (P::f(a), a) + } +} + +pub fn use_it() -> (u256, u256) { + as T>::f(4) +} + +#[test] +fn generic_trait_arg_impl_normalizes_self_out() { + let r = use_it() + assert!(r.0 == 5) + assert!(r.1 == 4) +} diff --git a/crates/fe/tests/fixtures/fe_test/qualified_generic_method_arg.fe b/crates/fe/tests/fixtures/fe_test/qualified_generic_method_arg.fe new file mode 100644 index 0000000000..5a118ec9e5 --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/qualified_generic_method_arg.fe @@ -0,0 +1,41 @@ +// A `<<` after a method name opens generic arguments only before a qualified +// path, the same rule path segments use. `receiver.take<::Point>(x)` +// passes an explicit type argument, while `s.bits << 1` stays a shift. +pub trait Model { + type Point +} + +pub struct M {} + +impl Model for M { + type Point = u256 +} + +pub struct S { + bits: u256, +} + +impl S { + fn take(self, _ value: T) -> T { + value + } +} + +fn explicit_qualified_type_argument() -> u256 { + S { bits: 0 }.take<::Point>(42) +} + +fn field_shift(_ s: S) -> u256 { + s.bits << 1 +} + +fn field_shift_around_cast(_ s: S, _ bits: u8) -> u256 { + s.bits << bits as u256 >> 1 +} + +#[test] +fn qualified_generic_method_argument() { + assert!(explicit_qualified_type_argument() == 42) + assert!(field_shift(S { bits: 2 }) == 4) + assert!(field_shift_around_cast(S { bits: 1 }, 3) == 4) +} diff --git a/crates/fe/tests/fixtures/fe_test/raw_record_layout.fe b/crates/fe/tests/fixtures/fe_test/raw_record_layout.fe new file mode 100644 index 0000000000..98edd6f5d1 --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/raw_record_layout.fe @@ -0,0 +1,48 @@ +struct Inner { code: u16, ready: bool } +struct Packet { tag: u8, inner: Inner, samples: [u16; 3], tail: u8 } + +impl Copy for Inner {} +impl Copy for Packet {} + +#[test] +fn packed_record_bytes_and_stride() { + assert!(core::size_of() == 3) + assert!(core::size_of() == 11) + let mut packets = core::ptr::MemArray::new_zeroed(2) + packets[0] = Packet { + tag: 0xa1, + inner: Inner { code: 0x1234, ready: true }, + samples: [0x5678, 0x9abc, 0xdef0], + tail: 0x7e, + } + packets[1] = Packet { + tag: 0xb2, + inner: Inner { code: 0x4321, ready: false }, + samples: [0x1112, 0x1314, 0x1516], + tail: 0x6d, + } + assert!(packets.byte_len() == 22) + let bytes = packets.span() + let expected: [u8; 22] = [ + 0xa1, 0x12, 0x34, 1, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x7e, + 0xb2, 0x43, 0x21, 0, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x6d, + ] + let mut i: usize = 0 + while i < 22 { + assert!(bytes[i] == expected[i]) + i += 1 + } + let second = packets.ptr_at(1) + second.inner.code = 0x2468 + second.samples[1] = 0xabcd + assert!(packets[1].inner.code == 0x2468) + assert!(packets[1].samples[1] == 0xabcd) + assert!(packets[0].inner.code == 0x1234) + assert!(packets[0].tail == 0x7e) + let bytes = packets.span() + assert!(bytes[12] == 0x24) + assert!(bytes[13] == 0x68) + assert!(bytes[17] == 0xab) + assert!(bytes[18] == 0xcd) + assert!(bytes[21] == 0x6d) +} diff --git a/crates/fe/tests/fixtures/fe_test/shift_around_cast.fe b/crates/fe/tests/fixtures/fe_test/shift_around_cast.fe new file mode 100644 index 0000000000..b23847fccb --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/shift_around_cast.fe @@ -0,0 +1,36 @@ +// A cast inside a shift must not be read as a qualified generic argument: +// `value << bits as u256 >> 1` is `(value << (bits as u256)) >> 1`, not +// `value<>` followed by a stray `1`. +pub trait Model { + const BITS: u8 +} + +pub struct M {} + +impl Model for M { + const BITS: u8 = 3 +} + +fn cast_then_shift(_ value: u256, _ bits: u8) -> u256 { + value << bits as u256 >> 1 +} + +fn cast_then_shift_by_parenthesized(_ value: u256, _ bits: u8) -> u256 { + value << bits as u256 >> (1) +} + +fn cast_after_later_path_segment(_ bits: u8) -> u8 { + M::BITS << bits as u8 >> 1 +} + +fn cast_of_qualified_constant(_ value: u8) -> u8 { + value << ::BITS as u8 >> 1 +} + +#[test] +fn casts_inside_shifts_stay_shifts() { + assert!(cast_then_shift(1, 3) == 4) + assert!(cast_then_shift_by_parenthesized(1, 3) == 4) + assert!(cast_after_later_path_segment(2) == 6) + assert!(cast_of_qualified_constant(1) == 4) +} diff --git a/crates/fe/tests/fixtures/fe_test/shift_then_compare_qualified.fe b/crates/fe/tests/fixtures/fe_test/shift_then_compare_qualified.fe new file mode 100644 index 0000000000..4fec83e71e --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/shift_then_compare_qualified.fe @@ -0,0 +1,47 @@ +// A shift whose right operand is a qualified associated constant, followed by a +// comparison, must stay a shift: `(value << ::BITS) > limit`. +// The generic-argument trial for `Wrapped<::Point>` must not claim +// these tokens. +pub trait Model { + const BITS: u256 +} + +pub struct M {} + +impl Model for M { + const BITS: u256 = 3 +} + +fn gt(_ value: u256, _ limit: u256) -> bool { + value << ::BITS > limit +} + +fn ge(_ value: u256, _ limit: u256) -> bool { + value << ::BITS >= limit +} + +fn tight(_ value: u256, _ limit: u256) -> bool { + value<<::BITS > limit +} + +fn shr(_ value: u256) -> u256 { + value << ::BITS >> 1 +} + +fn identity(_ b: bool) -> bool { + b +} + +fn in_call(_ value: u256, _ limit: u256) -> bool { + identity(value << ::BITS > limit) +} + +#[test] +fn shift_then_compare_with_qualified_operand() { + assert!(gt(1, 7)) + assert!(!gt(1, 8)) + assert!(ge(1, 8)) + assert!(tight(1, 7)) + assert!(shr(1) == 4) + assert!(in_call(1, 7)) +} diff --git a/crates/fmt/src/ast/types.rs b/crates/fmt/src/ast/types.rs index e4f9263561..86ffbb727b 100644 --- a/crates/fmt/src/ast/types.rs +++ b/crates/fmt/src/ast/types.rs @@ -1014,6 +1014,39 @@ impl ToDoc for ast::QualifiedType { impl ToDoc for ast::GenericArgList { fn to_doc<'a>(&self, ctx: &'a RewriteContext<'a>) -> Doc<'a> { let indent = ctx.config.indent_width as isize; + // The parser reads `<<` as generic arguments only before a qualified + // path such as `Wrapped<::Item>`. Space out a bare + // qualified first argument, `Wrapped< >`, so it does not + // become a left shift. + let first_path = self + .iter() + .next() + .and_then(|arg| match arg.kind() { + GenericArgKind::Type(arg) => arg.ty(), + _ => None, + }) + .and_then(|ty| match ty.kind() { + TypeKind::Path(ty) => ty.path(), + _ => None, + }); + let starts_with_bare_qualified_type = first_path.is_some_and(|path| { + let mut segments = path.segments(); + segments + .next() + .is_some_and(|segment| segment.qualified_type().is_some()) + && segments.next().is_none() + }); + if starts_with_bare_qualified_type { + return block_list_spaced_auto( + ctx, + self.syntax(), + "<", + ">", + ast::GenericArg::cast, + indent, + true, + ); + } block_list_auto( ctx, self.syntax(), diff --git a/crates/fmt/tests/format_snapshots.rs b/crates/fmt/tests/format_snapshots.rs index 15b3327114..19f522be49 100644 --- a/crates/fmt/tests/format_snapshots.rs +++ b/crates/fmt/tests/format_snapshots.rs @@ -84,3 +84,46 @@ fn load(pointer: **u256) -> u256 { "{syntax:#?}", ); } + +#[test] +fn bare_qualified_first_generic_arguments_do_not_become_shifts() { + let source = r#" +fn wrap(_ x: Wrapped< >, _ y: Wrapped< ::Point>) { + let w = make< >(x) +} +"#; + let formatted = format_str(source, &Config::default()).expect("format should succeed"); + + assert!(formatted.contains("Wrapped< >"), "{formatted}"); + assert!(formatted.contains("make< >(x)"), "{formatted}"); + assert!( + formatted.contains("Wrapped<::Point>"), + "{formatted}" + ); + assert_eq!( + format_str(&formatted, &Config::default()).expect("reformat should succeed"), + formatted, + ); + + let (green, errors) = parse_source_file(&formatted, RecoveryMode::NoRecover); + assert!(errors.is_empty(), "{errors:#?}\n{formatted}"); + + let syntax = SyntaxNode::new_root(green); + assert_eq!( + syntax + .descendants() + .filter(|node| node.kind() == SyntaxKind::GenericArgList) + .filter(|node| node + .parent() + .is_some_and(|parent| parent.kind() == SyntaxKind::PathSegment)) + .count(), + 3, + "{syntax:#?}", + ); + assert!( + syntax + .descendants() + .all(|node| node.kind() != SyntaxKind::BinExpr), + "{syntax:#?}", + ); +} diff --git a/crates/hir/src/analysis/diagnostics.rs b/crates/hir/src/analysis/diagnostics.rs index c9c6989dea..9d52893313 100644 --- a/crates/hir/src/analysis/diagnostics.rs +++ b/crates/hir/src/analysis/diagnostics.rs @@ -189,11 +189,20 @@ fn cmp_trait_inst_by_name<'db>( ) -> Ordering { let a_name = a.def(db).name(db).unwrap().data(db); let b_name = b.def(db).name(db).unwrap().data(db); - a_name.cmp(b_name).then_with(|| { - let a_self = a.self_ty(db).pretty_print(db).to_string(); - let b_self = b.self_ty(db).pretty_print(db).to_string(); - a_self.cmp(&b_self) - }) + a_name + .cmp(b_name) + .then_with(|| { + let a_self = a.self_ty(db).pretty_print(db).to_string(); + let b_self = b.self_ty(db).pretty_print(db).to_string(); + a_self.cmp(&b_self) + }) + .then_with(|| qualifying_trait_path(db, *a).cmp(&qualifying_trait_path(db, *b))) +} + +/// The trait with its type arguments, as written after `as` in `>`. +/// Associated type bindings are left out: they are not part of the path. +fn qualifying_trait_path<'db>(db: &'db dyn SpannedHirAnalysisDb, inst: TraitInstId<'db>) -> String { + inst.trait_ref(db).as_predicate(db).pretty_print(db, false) } fn format_method_param_ty<'db>( @@ -988,19 +997,19 @@ impl DiagnosticVoucher for PathResDiag<'_> { for (trait_inst, ty) in candidates { let trait_def = trait_inst.def(db); - let trait_name = trait_def.name(db).unwrap().data(db); + let trait_path = qualifying_trait_path(db, *trait_inst); let span = |t: &Trait| t.span().name().resolve(db); let span = span(&trait_def); let msg = match ty.data(db) { TyData::AssocTy(_) | TyData::Invalid(_) | TyData::Never => { - format!("candidate: `{trait_name}`") + format!("candidate: `{trait_path}`") } _ => { // Render as: candidate: ::Name = Ty let self_ty = trait_inst.self_ty(db).pretty_print(db); let ty_str = ty.pretty_print(db); - format!("candidate: <{self_ty} as {trait_name}>::{name} = {ty_str}") + format!("candidate: <{self_ty} as {trait_path}>::{name} = {ty_str}") } }; @@ -1015,10 +1024,10 @@ impl DiagnosticVoucher for PathResDiag<'_> { .iter() .min_by(|(a, _), (b, _)| cmp_trait_inst_by_name(db, a, b)) .unwrap(); - let trait_name = inst.def(db).name(db).unwrap().data(db); + let trait_path = qualifying_trait_path(db, *inst); let self_ty = inst.self_ty(db).pretty_print(db); let hint = format!( - "hint: specify the trait explicitly: `<{self_ty} as {trait_name}>::{name}`" + "hint: specify the trait explicitly: `<{self_ty} as {trait_path}>::{name}`" ); CompleteDiagnostic { @@ -1347,10 +1356,10 @@ impl DiagnosticVoucher for PathResDiag<'_> { // Candidate labels at the trait declarations for inst in trait_insts { let trait_def = inst.def(db); - let trait_name = trait_def.name(db).unwrap().data(db); + let trait_path = qualifying_trait_path(db, *inst); let trait_name_span = trait_def.span().name().resolve(db); let self_ty = inst.self_ty(db).pretty_print(db); - let msg = format!("candidate: `<{self_ty} as {trait_name}>::{const_name}`"); + let msg = format!("candidate: `<{self_ty} as {trait_path}>::{const_name}`"); sub_diagnostics.push(SubDiagnostic { style: LabelStyle::Secondary, message: msg, @@ -1362,10 +1371,10 @@ impl DiagnosticVoucher for PathResDiag<'_> { .iter() .min_by(|a, b| cmp_trait_inst_by_name(db, a, b)) .unwrap(); - let trait_name = inst.def(db).name(db).unwrap().data(db); + let trait_path = qualifying_trait_path(db, *inst); let self_ty = inst.self_ty(db).pretty_print(db); let hint = format!( - "hint: specify the trait explicitly: `<{self_ty} as {trait_name}>::{const_name}`" + "hint: specify the trait explicitly: `<{self_ty} as {trait_path}>::{const_name}`" ); CompleteDiagnostic { diff --git a/crates/hir/src/analysis/name_resolution/path_resolver.rs b/crates/hir/src/analysis/name_resolution/path_resolver.rs index 3b897efe6a..413d0fcc9f 100644 --- a/crates/hir/src/analysis/name_resolution/path_resolver.rs +++ b/crates/hir/src/analysis/name_resolution/path_resolver.rs @@ -1168,6 +1168,7 @@ where LayoutHoleArgSite::Path(path), minter, ); + let evidence = assoc_ty_candidate_evidence(db, ty, assumptions); let mut dedup: IndexMap, (TraitInstId<'db>, TyId<'db>, TyId<'db>)> = IndexMap::new(); for (inst, ty_candidate) in assoc_tys.iter().copied() { @@ -1197,7 +1198,7 @@ where .get(&ident) .copied() .map_or(applied, |bound| TyId::foldl(db, bound, &seg_args)); - let norm = normalize_ty(db, candidate_ty, scope, assumptions); + let norm = normalize_ty(db, candidate_ty, scope, evidence); dedup.entry(norm).or_insert((inst, applied, norm)); } @@ -1687,6 +1688,50 @@ pub(crate) fn find_associated_type<'db>( find_associated_type_in_mode(db, scope, ty, name, assumptions, ConstBodyLowering::Eager) } +/// The bounds implied by a trait's own `Self: Trait` predicate. +/// +/// Header positions such as a trait method signature deliberately withhold the +/// self-predicate from `assumptions`, because assuming it while the trait's +/// interface is still being lowered can recurse through the in-progress +/// definition (see `header_constraints_for`). Its implied bounds are still +/// sound for naming and comparing associated types, which never discharges a +/// goal, so they are reconstructed locally where they are needed. +fn trait_self_implied_bounds<'db>( + db: &'db dyn HirAnalysisDb, + trait_: Trait<'db>, +) -> PredicateListId<'db> { + PredicateListId::new(db, vec![trait_self_predicate(db, trait_)]).extend_all_bounds(db) +} + +/// Evidence for comparing associated-type candidates with each other: the +/// caller's assumptions and everything they imply, plus the enclosing trait's +/// implied bounds when the receiver is that trait's `Self`. +/// +/// Without the implied bounds a supertrait equality such as +/// `Left: Base` is invisible while normalizing, so a binding-free +/// projection reached through a second supertrait cannot be recognized as the +/// same type, and two paths to one declaration are reported as an ambiguity. +fn assoc_ty_candidate_evidence<'db>( + db: &'db dyn HirAnalysisDb, + ty: TyId<'db>, + assumptions: PredicateListId<'db>, +) -> PredicateListId<'db> { + let evidence = assumptions.extend_all_bounds(db); + if let TyData::TyParam(param) = ty.data(db) + && param.is_trait_self() + && let Some(trait_) = param.owner.resolve_to::(db) + { + let mut list = evidence.list(db).to_vec(); + for &bound in trait_self_implied_bounds(db, trait_).list(db) { + if !list.contains(&bound) { + list.push(bound); + } + } + return PredicateListId::new(db, list); + } + evidence +} + fn find_associated_type_in_mode<'db>( db: &'db dyn HirAnalysisDb, scope: ScopeId<'db>, @@ -1714,17 +1759,36 @@ fn find_associated_type_in_mode<'db>( } let scope_ingot = scope.ingot(db); + let mut candidates = SmallVec::new(); if let TyData::TyParam(param) = original_ty.data(db) { - // Trait self, in trait or impl trait. Associated type must be in this trait. + // Trait self, in trait or impl trait. Associated type must be in this trait + // or, inside a trait, in one of its supertraits. if param.is_trait_self() { if let Some(trait_) = param.owner.resolve_to::(db) { + let trait_inst = trait_self_predicate(db, trait_); if trait_.assoc_ty(db, name).is_some() { - let trait_inst = - TraitInstId::new(db, trait_, trait_.params(db).to_vec(), IndexMap::new()); let assoc_ty = TyId::assoc_ty(db, trait_inst.trait_ref(db), name); return Ok(smallvec![(trait_inst, assoc_ty)]); } + + // The trait's `Self` also satisfies the trait's declared supertraits, + // so their associated types are reachable as `Self::Name` too. Bounds + // on the trait's own associated types have another self type and are + // skipped. In a header position such as a trait method signature + // `assumptions` does not carry the enclosing trait's own `Self` + // bounds, so these are merged with the contextual candidates + // collected below rather than returned here: a method-level + // `where Self: Extra` is an equally valid source and must + // participate in ambiguity resolution. + for &bound in trait_self_implied_bounds(db, trait_).list(db) { + if bound.def(db) != trait_ + && bound.self_ty(db) == original_ty + && let Some(assoc_ty) = bound.project_assoc_ty(db, name) + { + candidates.push((bound, assoc_ty)); + } + } } else if let Some(impl_trait) = param.owner.resolve_to::(db) && let Some(trait_inst) = impl_trait.trait_inst(db) && let Some(assoc_ty) = trait_inst.project_assoc_ty(db, name) @@ -1734,7 +1798,6 @@ fn find_associated_type_in_mode<'db>( } } - let mut candidates = SmallVec::new(); let search_ingots = [ Some(scope_ingot), original_ty.ingot(db).filter(|&ingot| ingot != scope_ingot), @@ -1745,11 +1808,13 @@ fn find_associated_type_in_mode<'db>( // Only consult explicit bounds for type-parameter receivers; concrete // receivers get their candidates from impl lookup to avoid spurious - // ambiguity between bounds and implementations. + // ambiguity between bounds and implementations. The parameters in + // these bounds are the ones in scope, so they are matched as they + // are: freshening them would let a bound on `T` match `U` or `Self`. if let TyData::TyParam(_) = original_ty.data(db) { for &trait_inst in assumptions.list(db) { let snapshot = cx.snapshot(); - let pred_self_ty = cx.instantiate_with_fresh_vars(trait_inst.self_ty(db)); + let pred_self_ty = cx.materialize(trait_inst.self_ty(db)); if cx.unify::>(lhs_ty, pred_self_ty).is_ok() { let trait_inst = cx.materialize(trait_inst); @@ -1865,6 +1930,12 @@ fn find_associated_type_in_mode<'db>( Ok(()) })?; + // Supertrait and contextual bounds can reach the same projection, e.g. a + // method that restates a bound the enclosing trait already implies. Two + // paths to one declaration are not an ambiguity. + let mut seen = IndexSet::new(); + candidates.retain(|candidate| seen.insert(*candidate)); + Ok(candidates) } diff --git a/crates/hir/src/analysis/semantic/lower/body.rs b/crates/hir/src/analysis/semantic/lower/body.rs index 336ee65834..bc253d2e1d 100644 --- a/crates/hir/src/analysis/semantic/lower/body.rs +++ b/crates/hir/src/analysis/semantic/lower/body.rs @@ -216,6 +216,7 @@ struct SmirLowerInputs<'a, 'db> { pub(super) struct LoopScope { pub(super) continue_bb: SBlockId, pub(super) break_bb: SBlockId, + pub(super) has_reachable_continue: bool, } impl<'a, 'db> SmirLowerCtxt<'a, 'db> { @@ -1288,16 +1289,11 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { Stmt::While(cond, body_expr) => self.lower_while(*cond, *body_expr), Stmt::For(pat, iter, body_expr, _) => self.lower_for(stmt, *pat, *iter, *body_expr), Stmt::Continue => { - let scope = self - .loop_stack - .last() - .copied() - .expect("continue outside loop"); - self.set_terminator( - self.current, - origin, - STerminatorKind::Goto(scope.continue_bb), - ); + let is_reachable = !self.is_terminated(self.current); + let scope = self.loop_stack.last_mut().expect("continue outside loop"); + scope.has_reachable_continue |= is_reachable; + let continue_bb = scope.continue_bb; + self.set_terminator(self.current, origin, STerminatorKind::Goto(continue_bb)); } Stmt::Break => { let scope = self.loop_stack.last().copied().expect("break outside loop"); @@ -1333,6 +1329,7 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { self.loop_stack.push(LoopScope { continue_bb: cond_bb, break_bb: exit_bb, + has_reachable_continue: false, }); self.switch_to(body_bb); let _ = self.lower_expr(body_expr); @@ -1382,6 +1379,7 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { let cond_bb = self.new_block(); let body_bb = self.new_block(); + let advance_bb = self.new_block(); let exit_bb = self.new_block(); self.set_synthetic_terminator(self.current, STerminatorKind::Goto(cond_bb)); @@ -1404,8 +1402,9 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { ); self.loop_stack.push(LoopScope { - continue_bb: cond_bb, + continue_bb: advance_bb, break_bb: exit_bb, + has_reachable_continue: false, }); self.switch_to(body_bb); let get_effect_args = self.lower_effect_arg_slice(&for_loop_call_sites.get.effect_args); @@ -1430,7 +1429,14 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { } self.bind_pattern(pat, elem); let _ = self.lower_expr(body_expr); - if !self.is_terminated(self.current) { + let falls_through = !self.is_terminated(self.current); + if falls_through { + self.set_synthetic_terminator(self.current, STerminatorKind::Goto(advance_bb)); + } + let scope = self.loop_stack.pop().expect("for loop scope"); + if falls_through || scope.has_reachable_continue { + // Both normal fallthrough and `continue` must advance the sequence. + self.switch_to(advance_bb); let one = self.emit_expr( usize_ty, SExpr::Const(SConst::from_trusted_source( @@ -1451,8 +1457,12 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { expr: SExpr::UseValue(SOperand::synthetic(next)), }); self.set_synthetic_terminator(self.current, STerminatorKind::Goto(cond_bb)); + } else { + // Every body path leaves the loop, so nothing reaches the advance + // block. Close it like a dead `if`/`match` join rather than leaving + // it to read `idx_local` from a block unreachable from entry. + self.set_synthetic_terminator(advance_bb, STerminatorKind::Goto(advance_bb)); } - self.loop_stack.pop(); self.switch_to(exit_bb); } diff --git a/crates/hir/src/analysis/ty/layout_holes.rs b/crates/hir/src/analysis/ty/layout_holes.rs index 844336d1e6..8c41221754 100644 --- a/crates/hir/src/analysis/ty/layout_holes.rs +++ b/crates/hir/src/analysis/ty/layout_holes.rs @@ -368,6 +368,57 @@ fn layout_view_state_is_strict_subterm<'db>( }) } +/// Whether structural expansion must still be checked against an earlier +/// type. Repeating a constructor with unrelated arguments is not by itself +/// growth: `Option` can contain `Option>` through a +/// declared field of `Argument`. +/// +/// Coupling matching constructors and descending into arguments detects +/// expansion even when new wrappers are inserted. Const values are treated as +/// one class here so changing numeric arguments cannot evade the recurrence +/// check. This is only a growth guard, not semantic type equality; the ordinary +/// recurrence classifier still checks exact states and finite permutations. +/// Opaque or invalid types retain the conservative check: their internal +/// arguments are not exposed by `decompose_ty_app`. +/// +/// The walk is not memoized. On a pair that does not embed it tries both +/// descents and can reach the same pair of subterms more than once, so its +/// cost grows faster than the sizes of the two types. Program types are +/// shallow, and growth is caught at its first repeat, so the walk stays small. +pub(crate) fn structural_layout_type_embeds<'db>( + db: &'db dyn HirAnalysisDb, + earlier: TyId<'db>, + later: TyId<'db>, +) -> bool { + if layout_view_ty_descends_from(db, later, earlier) + || matches!( + (earlier.data(db), later.data(db)), + (TyData::ConstTy(_), TyData::ConstTy(_)) + ) + { + return true; + } + let (earlier_base, earlier_args) = earlier.decompose_ty_app(db); + let (later_base, later_args) = later.decompose_ty_app(db); + if [earlier_base, later_base].into_iter().any(|base| { + matches!( + base.data(db), + TyData::AssocTy(_) | TyData::QualifiedTy(_) | TyData::TyVar(_) | TyData::Invalid(_) + ) + }) { + return true; + } + (earlier_base == later_base + && earlier_args.len() == later_args.len() + && earlier_args + .iter() + .zip(later_args) + .all(|(earlier, later)| structural_layout_type_embeds(db, *earlier, *later))) + || later_args + .iter() + .any(|later| structural_layout_type_embeds(db, earlier, *later)) +} + /// The action required when a semantic layout-view walk reaches `value`. /// /// Provider targets and callable layout schemas must make this decision from diff --git a/crates/hir/src/analysis/ty/ty_lower.rs b/crates/hir/src/analysis/ty/ty_lower.rs index 5bbab4b5ec..456971e922 100644 --- a/crates/hir/src/analysis/ty/ty_lower.rs +++ b/crates/hir/src/analysis/ty/ty_lower.rs @@ -37,7 +37,8 @@ use super::{ collect_unique_layout_placeholders_in_order, instantiate_layout_template, layout_hole_fallback_ty, layout_root_descends_from, layout_root_id, reanchor_template_holes, rewrite_structural_holes, structural_hole_id, - substitute_layout_holes_by_placeholder, substitute_layout_holes_by_placeholder_in, + structural_layout_type_embeds, substitute_layout_holes_by_placeholder, + substitute_layout_holes_by_placeholder_in, }, normalize::{normalize_from_assumptions, normalize_ty}, provider::{EffectHandleResolution, resolve_effect_handle}, @@ -1107,6 +1108,19 @@ impl<'db> CallableLayoutProjectionCollector<'db> { .iter() .enumerate() .rev() + // Nominal repetition through ordinary fields can be finite + // even when the enclosed types are unrelated. Keep structural + // ancestors that exhibit growth. Provider families retain the + // stricter selected-implementation recurrence contract. + .filter(|(_, frame)| { + !matches!( + (family, frame.family), + ( + CallableLayoutExpansionFamily::Adt(_), + CallableLayoutExpansionFamily::Adt(_) + ) + ) || structural_layout_type_embeds(self.db, frame.ty, ty) + }) .map(|(idx, frame)| (idx, frame.ty, frame.family)), ) { LayoutViewRecurrence::BackEdge { ancestor } => { diff --git a/crates/hir/test_files/layout_evidence/expanding_structural_arguments.fe b/crates/hir/test_files/layout_evidence/expanding_structural_arguments.fe new file mode 100644 index 0000000000..05c8d63be9 --- /dev/null +++ b/crates/hir/test_files/layout_evidence/expanding_structural_arguments.fe @@ -0,0 +1,10 @@ +// `Growing` embeds `Growing<(T, T)>`, so its layout grows without bound +// and must be reported as a non-regular cycle rather than expanded forever. +struct Rooted {} + +struct Growing { + marker: Rooted, + next: Growing<(T, T), ROOT>, +} + +fn inspect(value: Growing) {} diff --git a/crates/hir/test_files/layout_evidence/expanding_through_wrappers.fe b/crates/hir/test_files/layout_evidence/expanding_through_wrappers.fe new file mode 100644 index 0000000000..f07388ac5f --- /dev/null +++ b/crates/hir/test_files/layout_evidence/expanding_through_wrappers.fe @@ -0,0 +1,24 @@ +// Two layouts that grow without bound, each through a wrapper around the +// repeated type: `Wrapped` holds `Wrapped>`, and `Ping` holds +// `Ping>` through `Pong`. Each must be reported as a +// non-regular cycle, in bounded time, rather than expanded forever. +struct Rooted {} + +struct Wrapped { + marker: Rooted, + next: Wrapped, ROOT>, +} + +struct Ping { + marker: Rooted, + pong: Pong<(T, u8), ROOT>, +} + +struct Pong { + marker: Rooted, + ping: Ping, ROOT>, +} + +fn inspect_wrapped(value: Wrapped) {} + +fn inspect_ping(value: Ping) {} diff --git a/crates/hir/test_files/layout_evidence/finite_nested_options.fe b/crates/hir/test_files/layout_evidence/finite_nested_options.fe new file mode 100644 index 0000000000..cf073e1bd9 --- /dev/null +++ b/crates/hir/test_files/layout_evidence/finite_nested_options.fe @@ -0,0 +1,11 @@ +// The same nominal type (`Option`) repeats through ordinary fields, but the +// enclosed types are unrelated, so the layout is finite. +struct Argument { + label: Option, +} + +struct Call { + argument: Option, +} + +fn inspect(value: Call) {} diff --git a/crates/hir/test_files/trait_resolution_conformance/qualified_nested_associated_type.fe b/crates/hir/test_files/trait_resolution_conformance/qualified_nested_associated_type.fe new file mode 100644 index 0000000000..c5163c5d6f --- /dev/null +++ b/crates/hir/test_files/trait_resolution_conformance/qualified_nested_associated_type.fe @@ -0,0 +1,27 @@ +trait Model { + type Point +} + +struct Wrapped { + value: T, +} + +trait Admit: Model { + fn admit(_ point: Wrapped<::Point>) -> Wrapped<::Point> +} + +struct Plane {} + +impl Model for Plane { + type Point = u32 +} + +impl Admit for Plane { + fn admit(_ point: Wrapped) -> Wrapped { + point + } +} + +fn main() -> u32 { + Plane::admit(Wrapped { value: 7 }).value +} diff --git a/crates/hir/tests/layout_evidence.rs b/crates/hir/tests/layout_evidence.rs index 1649651eed..4d20d1b5a5 100644 --- a/crates/hir/tests/layout_evidence.rs +++ b/crates/hir/tests/layout_evidence.rs @@ -26,7 +26,7 @@ use fe_hir::{ }, }, core::semantic::ContractLayoutError, - hir_def::{CallableDef, IdentId, ItemKind}, + hir_def::{CallableDef, IdentId, ItemKind, TopLevelMod}, test_db::{HirAnalysisTestDb, find_contract, find_func}, }; use layout_test_support::{parse_module, parse_ok}; @@ -67,6 +67,87 @@ fn assert_trusted_layoutizes(name: &str, src: &str) { assert_layoutizes_in(name, src, true); } +/// Reads a fixture from `test_files/layout_evidence`, returning its real path +/// and its text. +fn layout_evidence_fixture(name: &str) -> (Utf8PathBuf, String) { + let path = Utf8PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_files/layout_evidence" + )) + .join(name); + let text = std::fs::read_to_string(&path).expect("fixture should be readable"); + (path, text) +} + +#[test] +fn finite_nested_options_through_named_fields_have_finite_layouts() { + let (path, text) = layout_evidence_fixture("finite_nested_options.fe"); + assert_layoutizes(path.as_str(), &text); +} + +#[test] +fn expanding_structural_type_arguments_do_not_expand_layouts_forever() { + let (path, text) = layout_evidence_fixture("expanding_structural_arguments.fe"); + let mut db = HirAnalysisTestDb::default(); + let file = db.new_stand_alone(path, &text); + let (top_mod, _) = db.top_mod(file); + assert!(reports_non_regular_cycle(&db, top_mod, "inspect")); +} + +/// Whether the layout schema of the first input of function `name` reports a +/// non-regular view cycle. +fn reports_non_regular_cycle<'db>( + db: &'db HirAnalysisTestDb, + top_mod: TopLevelMod<'db>, + name: &str, +) -> bool { + let inspect = get_or_build_semantic_instance( + db, + identity_semantic_instance_key(db, BodyOwner::Func(find_func(db, top_mod, name))), + ); + let signature = inspect.key(db).layout_bundle_signature(db); + signature.inputs[0] + .interface + .schema + .non_regular_view_cycle + .is_some() +} + +/// Growth through an inserted wrapper, directly or through a second type, is +/// still caught by the growth guard. The check runs on a worker thread so a +/// layout walk that never terminates fails the test instead of hanging it. +#[test] +fn expanding_layouts_through_wrappers_are_rejected_in_bounded_time() { + let (path, text) = layout_evidence_fixture("expanding_through_wrappers.fe"); + let (sender, receiver) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut db = HirAnalysisTestDb::default(); + let file = db.new_stand_alone(path, &text); + let (top_mod, _) = db.top_mod(file); + for name in ["inspect_wrapped", "inspect_ping"] { + let rejected = reports_non_regular_cycle(&db, top_mod, name); + sender + .send((name, rejected)) + .expect("test thread is waiting"); + } + }); + for _ in 0..2 { + let (name, rejected) = match receiver.recv_timeout(std::time::Duration::from_secs(60)) { + Ok(result) => result, + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + panic!("layout growth check did not finish within 60 seconds") + } + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { + panic!("layout growth check panicked") + } + }; + assert!( + rejected, + "`{name}` should be reported as a non-regular cycle" + ); + } +} + fn assert_layoutizes_in(name: &str, src: &str, std_module: bool) { let mut db = HirAnalysisTestDb::default(); let path = Utf8PathBuf::from(name); diff --git a/crates/parser/src/parser/expr.rs b/crates/parser/src/parser/expr.rs index cfccb211ef..59a312b14e 100644 --- a/crates/parser/src/parser/expr.rs +++ b/crates/parser/src/parser/expr.rs @@ -10,7 +10,7 @@ use super::{ expr_atom::{self, is_expr_atom_head}, param::{CallArgListScope, GenericArgListScope}, pat::parse_pat, - path::is_qualified_type, + path::{is_qualified_type, lshift_opens_generic_args}, token_stream::TokenStream, }; use crate::{ExpectedKind, ParseError, SyntaxKind, TextRange}; @@ -805,12 +805,18 @@ fn is_method_call(parser: &mut Parser) -> bool { // After the identifier, require `<` or `(` to be on the same line parser.set_newline_as_trivia(false); - if parser.current_kind() == Some(SyntaxKind::Lt) - && (is_lt_eq(parser) - || is_lshift(parser) - || !parser.parses_without_error(GenericArgListScope::default())) - { - return false; + if parser.current_kind() == Some(SyntaxKind::Lt) { + // A `<<` here opens generic arguments only before a qualified path, + // the same rule path segments use; otherwise it is a left shift on a + // field, as in `value.bits << 1`. + let opens_generic_args = if is_lshift(parser) { + lshift_opens_generic_args(parser) + } else { + !is_lt_eq(parser) + }; + if !opens_generic_args || !parser.parses_without_error(GenericArgListScope::default()) { + return false; + } } if parser.current_kind() != Some(SyntaxKind::LParen) { diff --git a/crates/parser/src/parser/mod.rs b/crates/parser/src/parser/mod.rs index b97e824e8f..8999725813 100644 --- a/crates/parser/src/parser/mod.rs +++ b/crates/parser/src/parser/mod.rs @@ -1,4 +1,7 @@ -use std::{collections::VecDeque, convert::Infallible}; +use std::{ + collections::{HashMap, VecDeque}, + convert::Infallible, +}; pub(crate) use item::ItemListScope; use smallvec::SmallVec; @@ -66,10 +69,31 @@ pub struct Parser { dry_run_states: Vec>, dry_run_next_trivias_pool: Vec>, + /// Outcomes of the speculative probes run so far, see [`Parser::probe`]. + probe_cache: HashMap<(rowan::TextSize, bool, ProbeKind), bool>, + /// `parents.len()` on entry to each [`Parser::probe`] currently running, so + /// `recover` can confine itself to the scopes the innermost probe created. + probe_scope_floors: Vec, + /// Whether or not to recover from syntax errors automatically. recovery_mode: RecoveryMode, } +/// A speculative parse used only to disambiguate what follows, identified so +/// its outcome can be reused at the same position. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum ProbeKind { + /// ``, as opposed to a comparison or shift. + QualifiedType, + /// `<<` opening generic arguments whose first argument is a qualified + /// path, as opposed to a left shift. + LShiftOpensGenericArgs, + /// A path followed by `(`, i.e. a call in const generic argument position. + ConstCall, + /// A generic argument list, as opposed to a comparison. + GenericArgList { is_expr: bool }, +} + impl Parser { /// Create a parser with the given token stream. pub fn new(stream: S, recovery_mode: RecoveryMode) -> Self { @@ -84,6 +108,8 @@ impl Parser { next_trivias: VecDeque::new(), dry_run_states: Vec::new(), dry_run_next_trivias_pool: Vec::new(), + probe_cache: HashMap::new(), + probe_scope_floors: Vec::new(), recovery_mode, } } @@ -384,6 +410,43 @@ impl Parser { self.try_recover().map_err(|r| r.add_err_proof(proof)) } + /// Runs a disambiguation probe in dry run mode, reusing the outcome of an + /// identical probe already run at this position. + /// + /// `dry_run` reverts the stream, position, buffered trivia and errors, and + /// leaves the tree builder untouched, so a probe depends only on the position + /// it starts from and on whether newlines are trivia there. The input never + /// changes during a parse, so an outcome stays valid. + /// + /// Error recovery is the one thing that would otherwise read more than the + /// input: `recover` searches the enclosing scope stack for a token to stop + /// at, so how far it consumes depends on where the probe was run from. A + /// probe therefore records its scope depth in `probe_scope_floors` and + /// `recover` searches no further out than that, which is what a probe + /// already means -- would this construct parse here, on its own? -- since a + /// probe's tree and errors are discarded either way. With recovery confined, + /// every outcome is a function of the key alone and can be reused, malformed + /// input included. + /// + /// Nesting is what makes caching worthwhile: each level of + /// `Wrap<::Point>` probes the positions inside it, and every + /// enclosing level probes them all again, so without this the same position + /// is re-parsed a multiple of times per level of depth. + pub(crate) fn probe(&mut self, kind: ProbeKind, f: F) -> bool + where + F: FnOnce(&mut Self) -> bool, + { + let key = (self.current_pos, self.is_newline_trivia, kind); + if let Some(&outcome) = self.probe_cache.get(&key) { + return outcome; + } + self.probe_scope_floors.push(self.parents.len()); + let outcome = self.dry_run(f); + self.probe_scope_floors.pop(); + self.probe_cache.insert(key, outcome); + outcome + } + /// Runs the parser in the dry run mode. /// /// Any changes to the parser state will be reverted. @@ -504,6 +567,9 @@ impl Parser { if self.recovery_mode == RecoveryMode::NoRecover { return (None, None); } + // Inside a probe, only the scopes the probe itself opened may stop the + // scan, so its outcome does not depend on the enclosing context. + let floor = self.probe_scope_floors.last().copied().unwrap_or(0); let mut unexpected = None; let mut match_scope_index = None; @@ -512,6 +578,7 @@ impl Parser { .parents .iter() .enumerate() + .skip(floor) .rev() .find(|(_i, scope)| scope.is_recovery_match(kind)) { diff --git a/crates/parser/src/parser/param.rs b/crates/parser/src/parser/param.rs index b5abcd3de2..83c8c430ab 100644 --- a/crates/parser/src/parser/param.rs +++ b/crates/parser/src/parser/param.rs @@ -5,7 +5,7 @@ use unwrap_infallible::UnwrapInfallible; use crate::{ExpectedKind, ParseError, SyntaxKind}; use super::{ - ErrProof, Parser, Recovery, define_scope, + ErrProof, Parser, ProbeKind, Recovery, define_scope, expr::{parse_const_generic_expr, parse_expr}, expr_atom::{BlockExprScope, LitExprScope}, parse_list, @@ -394,7 +394,7 @@ impl super::Parse for GenericArgScope { // Parse the type parse_type(parser, None)?; } else { - let is_const_call = parser.dry_run(|parser| { + let is_const_call = parser.probe(ProbeKind::ConstCall, |parser| { parser .parse(PathScope::default()) .is_ok_and(|()| parser.current_kind() == Some(SyntaxKind::LParen)) diff --git a/crates/parser/src/parser/path.rs b/crates/parser/src/parser/path.rs index 9d1e99576d..675b709c5a 100644 --- a/crates/parser/src/parser/path.rs +++ b/crates/parser/src/parser/path.rs @@ -1,7 +1,7 @@ use crate::{ParseError, SyntaxKind, TextRange, TextSize}; use super::{ - Parser, define_scope, + Parser, ProbeKind, define_scope, expr::{is_lshift, is_lt_eq}, param::{GenericArgListScope, TraitRefScope}, token_stream::TokenStream, @@ -39,16 +39,29 @@ impl super::Parse for PathSegmentScope { Some(kind) if is_path_segment(kind) => { parser.bump(); + if parser.current_kind_same_line() == Some(SyntaxKind::Lt) && is_lshift(parser) { + if lshift_opens_generic_args(parser) { + // Errors inside the arguments are reported as they are parsed. + let _ = parser.parse(GenericArgListScope::new(self.is_expr)); + } + return Ok(()); + } + let is_turbofish = parser.current_kind_same_line() == Some(SyntaxKind::Colon2) && parser.peek_two() == (Some(SyntaxKind::Colon2), Some(SyntaxKind::Lt)); if (is_turbofish || (parser.current_kind_same_line() == Some(SyntaxKind::Lt) - && !(is_lt_eq(parser) || is_lshift(parser)))) - && parser.dry_run(|parser| { - parser.bump_if(SyntaxKind::Colon2); - parser.parses_without_error(GenericArgListScope::new(self.is_expr)) - }) + && !is_lt_eq(parser))) + && parser.probe( + ProbeKind::GenericArgList { + is_expr: self.is_expr, + }, + |parser| { + parser.bump_if(SyntaxKind::Colon2); + parser.parses_without_error(GenericArgListScope::new(self.is_expr)) + }, + ) { if is_turbofish { parser.bump_trivias(); @@ -60,7 +73,7 @@ impl super::Parse for PathSegmentScope { } parser .parse(GenericArgListScope::new(self.is_expr)) - .expect("dry_run suggests this will succeed"); + .expect("the probe suggests this will succeed"); } Ok(()) } @@ -90,7 +103,13 @@ impl super::Parse for QualifiedTypeScope { )); } } - parser.bump_expected(SyntaxKind::AsKw); + if !parser.bump_if(SyntaxKind::AsKw) { + return Err(ParseError::expected( + &[SyntaxKind::AsKw], + None, + parser.end_of_prev_token, + )); + } parser.parse(TraitRefScope::default())?; if parser.bump_if(SyntaxKind::Gt) { Ok(()) @@ -104,16 +123,28 @@ impl super::Parse for QualifiedTypeScope { } } +/// Whether the `<<` at the current position opens generic arguments whose first +/// argument is a qualified path, as in `Wrapped<::Item>`, rather +/// than being a left shift. +/// +/// No shift operand continues with `>::`, so that prefix settles it, and a cast +/// such as `value << bits as u256 >> 1` stays a shift. Path segments and +/// method calls must agree on this, or `Wrapped<::Item>::new()` +/// parses while `receiver.take<::Item>(x)` does not. +pub(super) fn lshift_opens_generic_args(parser: &mut Parser) -> bool { + parser.probe(ProbeKind::LShiftOpensGenericArgs, |parser| { + parser.bump(); + parser.parses_without_error(QualifiedTypeScope::default()) + && parser.current_kind() == Some(SyntaxKind::Colon2) + }) +} + pub(super) fn is_qualified_type(parser: &mut Parser) -> bool { - parser - .dry_run(|parser| { - if !parser.bump_if(SyntaxKind::Lt) { - return None; - } - parse_type(parser, None).ok()?; - (parser.current_kind() == Some(SyntaxKind::AsKw)).then_some(()) - }) - .is_some() + parser.probe(ProbeKind::QualifiedType, |parser| { + parser.bump_if(SyntaxKind::Lt) + && parse_type(parser, None).is_ok() + && parser.current_kind() == Some(SyntaxKind::AsKw) + }) } pub(super) fn is_path_segment(kind: SyntaxKind) -> bool { diff --git a/crates/parser/test_files/error_recovery/exprs/qualified_generic_arg.fe b/crates/parser/test_files/error_recovery/exprs/qualified_generic_arg.fe new file mode 100644 index 0000000000..336e337696 --- /dev/null +++ b/crates/parser/test_files/error_recovery/exprs/qualified_generic_arg.fe @@ -0,0 +1 @@ +Wrapped<::Point { value: point } diff --git a/crates/parser/test_files/error_recovery/exprs/qualified_generic_arg.snap b/crates/parser/test_files/error_recovery/exprs/qualified_generic_arg.snap new file mode 100644 index 0000000000..c198d45b97 --- /dev/null +++ b/crates/parser/test_files/error_recovery/exprs/qualified_generic_arg.snap @@ -0,0 +1,48 @@ +--- +source: crates/parser/tests/error_recovery.rs +expression: node +input_file: test_files/error_recovery/exprs/qualified_generic_arg.fe +--- +Root@0..45 + RecordInitExpr@0..44 + Path@0..27 + PathSegment@0..27 + Ident@0..7 "Wrapped" + GenericArgList@7..27 + Lt@7..8 "<" + TypeGenericArg@8..27 + PathType@8..27 + Path@8..27 + PathSegment@8..20 + QualifiedType@8..20 + Lt@8..9 "<" + PathType@9..10 + Path@9..10 + PathSegment@9..10 + Ident@9..10 "T" + WhiteSpace@10..11 " " + AsKw@11..13 "as" + WhiteSpace@13..14 " " + TraitRef@14..19 + Path@14..19 + PathSegment@14..19 + Ident@14..19 "Model" + Gt@19..20 ">" + Colon2@20..22 "::" + PathSegment@22..27 + Ident@22..27 "Point" + WhiteSpace@27..28 " " + RecordFieldList@28..44 + LBrace@28..29 "{" + WhiteSpace@29..30 " " + RecordField@30..42 + Ident@30..35 "value" + Colon@35..36 ":" + WhiteSpace@36..37 " " + PathExpr@37..42 + Path@37..42 + PathSegment@37..42 + Ident@37..42 "point" + WhiteSpace@42..43 " " + RBrace@43..44 "}" + Newline@44..45 "\n" diff --git a/crates/parser/test_files/syntax_node/exprs/path_lshift.fe b/crates/parser/test_files/syntax_node/exprs/path_lshift.fe new file mode 100644 index 0000000000..7560f9492e --- /dev/null +++ b/crates/parser/test_files/syntax_node/exprs/path_lshift.fe @@ -0,0 +1,7 @@ +value << 2 +value<::BITS +value << bits as u256 >> 1 +value << bits as u256 >> (1) +M::BITS << bits as u8 >> 1 +value << ::BITS as u8 >> 1 diff --git a/crates/parser/test_files/syntax_node/exprs/path_lshift.snap b/crates/parser/test_files/syntax_node/exprs/path_lshift.snap new file mode 100644 index 0000000000..e07a7bab4b --- /dev/null +++ b/crates/parser/test_files/syntax_node/exprs/path_lshift.snap @@ -0,0 +1,214 @@ +--- +source: crates/parser/tests/syntax_node.rs +expression: node +input_file: test_files/syntax_node/exprs/path_lshift.fe +--- +Root@0..175 + BinExpr@0..10 + PathExpr@0..5 + Path@0..5 + PathSegment@0..5 + Ident@0..5 "value" + WhiteSpace@5..6 " " + LShift@6..8 + Lt@6..7 "<" + Lt@7..8 "<" + WhiteSpace@8..9 " " + LitExpr@9..10 + Lit@9..10 + Int@9..10 "2" + Newline@10..11 "\n" + BinExpr@11..24 + PathExpr@11..16 + Path@11..16 + PathSegment@11..16 + Ident@11..16 "value" + LShift@16..18 + Lt@16..17 "<" + Lt@17..18 "<" + PathExpr@18..24 + Path@18..24 + PathSegment@18..24 + Ident@18..24 "amount" + Newline@24..25 "\n" + BinExpr@25..52 + PathExpr@25..30 + Path@25..30 + PathSegment@25..30 + Ident@25..30 "value" + WhiteSpace@30..31 " " + LShift@31..33 + Lt@31..32 "<" + Lt@32..33 "<" + WhiteSpace@33..34 " " + PathExpr@34..52 + Path@34..52 + PathSegment@34..46 + QualifiedType@34..46 + Lt@34..35 "<" + PathType@35..36 + Path@35..36 + PathSegment@35..36 + Ident@35..36 "T" + WhiteSpace@36..37 " " + AsKw@37..39 "as" + WhiteSpace@39..40 " " + TraitRef@40..45 + Path@40..45 + PathSegment@40..45 + Ident@40..45 "Model" + Gt@45..46 ">" + Colon2@46..48 "::" + PathSegment@48..52 + Ident@48..52 "BITS" + Newline@52..53 "\n" + BinExpr@53..79 + BinExpr@53..74 + PathExpr@53..58 + Path@53..58 + PathSegment@53..58 + Ident@53..58 "value" + WhiteSpace@58..59 " " + LShift@59..61 + Lt@59..60 "<" + Lt@60..61 "<" + WhiteSpace@61..62 " " + CastExpr@62..74 + PathExpr@62..66 + Path@62..66 + PathSegment@62..66 + Ident@62..66 "bits" + WhiteSpace@66..67 " " + AsKw@67..69 "as" + WhiteSpace@69..70 " " + PathType@70..74 + Path@70..74 + PathSegment@70..74 + Ident@70..74 "u256" + WhiteSpace@74..75 " " + RShift@75..77 + Gt@75..76 ">" + Gt@76..77 ">" + WhiteSpace@77..78 " " + LitExpr@78..79 + Lit@78..79 + Int@78..79 "1" + Newline@79..80 "\n" + BinExpr@80..108 + BinExpr@80..101 + PathExpr@80..85 + Path@80..85 + PathSegment@80..85 + Ident@80..85 "value" + WhiteSpace@85..86 " " + LShift@86..88 + Lt@86..87 "<" + Lt@87..88 "<" + WhiteSpace@88..89 " " + CastExpr@89..101 + PathExpr@89..93 + Path@89..93 + PathSegment@89..93 + Ident@89..93 "bits" + WhiteSpace@93..94 " " + AsKw@94..96 "as" + WhiteSpace@96..97 " " + PathType@97..101 + Path@97..101 + PathSegment@97..101 + Ident@97..101 "u256" + WhiteSpace@101..102 " " + RShift@102..104 + Gt@102..103 ">" + Gt@103..104 ">" + WhiteSpace@104..105 " " + ParenExpr@105..108 + LParen@105..106 "(" + LitExpr@106..107 + Lit@106..107 + Int@106..107 "1" + RParen@107..108 ")" + Newline@108..109 "\n" + BinExpr@109..135 + BinExpr@109..130 + PathExpr@109..116 + Path@109..116 + PathSegment@109..110 + Ident@109..110 "M" + Colon2@110..112 "::" + PathSegment@112..116 + Ident@112..116 "BITS" + WhiteSpace@116..117 " " + LShift@117..119 + Lt@117..118 "<" + Lt@118..119 "<" + WhiteSpace@119..120 " " + CastExpr@120..130 + PathExpr@120..124 + Path@120..124 + PathSegment@120..124 + Ident@120..124 "bits" + WhiteSpace@124..125 " " + AsKw@125..127 "as" + WhiteSpace@127..128 " " + PathType@128..130 + Path@128..130 + PathSegment@128..130 + Ident@128..130 "u8" + WhiteSpace@130..131 " " + RShift@131..133 + Gt@131..132 ">" + Gt@132..133 ">" + WhiteSpace@133..134 " " + LitExpr@134..135 + Lit@134..135 + Int@134..135 "1" + Newline@135..136 "\n" + BinExpr@136..174 + BinExpr@136..169 + PathExpr@136..141 + Path@136..141 + PathSegment@136..141 + Ident@136..141 "value" + WhiteSpace@141..142 " " + LShift@142..144 + Lt@142..143 "<" + Lt@143..144 "<" + WhiteSpace@144..145 " " + CastExpr@145..169 + PathExpr@145..163 + Path@145..163 + PathSegment@145..157 + QualifiedType@145..157 + Lt@145..146 "<" + PathType@146..147 + Path@146..147 + PathSegment@146..147 + Ident@146..147 "T" + WhiteSpace@147..148 " " + AsKw@148..150 "as" + WhiteSpace@150..151 " " + TraitRef@151..156 + Path@151..156 + PathSegment@151..156 + Ident@151..156 "Model" + Gt@156..157 ">" + Colon2@157..159 "::" + PathSegment@159..163 + Ident@159..163 "BITS" + WhiteSpace@163..164 " " + AsKw@164..166 "as" + WhiteSpace@166..167 " " + PathType@167..169 + Path@167..169 + PathSegment@167..169 + Ident@167..169 "u8" + WhiteSpace@169..170 " " + RShift@170..172 + Gt@170..171 ">" + Gt@171..172 ">" + WhiteSpace@172..173 " " + LitExpr@173..174 + Lit@173..174 + Int@173..174 "1" + Newline@174..175 "\n" diff --git a/crates/parser/test_files/syntax_node/items/nested_qualified_generic_arg.fe b/crates/parser/test_files/syntax_node/items/nested_qualified_generic_arg.fe new file mode 100644 index 0000000000..8d45c06ba7 --- /dev/null +++ b/crates/parser/test_files/syntax_node/items/nested_qualified_generic_arg.fe @@ -0,0 +1,2 @@ +type A = Wrapped<::Point> +type B = Pair<::Point, ::Scalar> diff --git a/crates/parser/test_files/syntax_node/items/nested_qualified_generic_arg.snap b/crates/parser/test_files/syntax_node/items/nested_qualified_generic_arg.snap new file mode 100644 index 0000000000..2cec1177df --- /dev/null +++ b/crates/parser/test_files/syntax_node/items/nested_qualified_generic_arg.snap @@ -0,0 +1,104 @@ +--- +source: crates/parser/tests/syntax_node.rs +expression: node +input_file: test_files/syntax_node/items/nested_qualified_generic_arg.fe +--- +Root@0..95 + ItemList@0..94 + Item@0..37 + TypeAlias@0..37 + TypeKw@0..4 "type" + WhiteSpace@4..5 " " + Ident@5..6 "A" + WhiteSpace@6..7 " " + Eq@7..8 "=" + WhiteSpace@8..9 " " + PathType@9..37 + Path@9..37 + PathSegment@9..37 + Ident@9..16 "Wrapped" + GenericArgList@16..37 + Lt@16..17 "<" + TypeGenericArg@17..36 + PathType@17..36 + Path@17..36 + PathSegment@17..29 + QualifiedType@17..29 + Lt@17..18 "<" + PathType@18..19 + Path@18..19 + PathSegment@18..19 + Ident@18..19 "T" + WhiteSpace@19..20 " " + AsKw@20..22 "as" + WhiteSpace@22..23 " " + TraitRef@23..28 + Path@23..28 + PathSegment@23..28 + Ident@23..28 "Model" + Gt@28..29 ">" + Colon2@29..31 "::" + PathSegment@31..36 + Ident@31..36 "Point" + Gt@36..37 ">" + Newline@37..38 "\n" + Item@38..94 + TypeAlias@38..94 + TypeKw@38..42 "type" + WhiteSpace@42..43 " " + Ident@43..44 "B" + WhiteSpace@44..45 " " + Eq@45..46 "=" + WhiteSpace@46..47 " " + PathType@47..94 + Path@47..94 + PathSegment@47..94 + Ident@47..51 "Pair" + GenericArgList@51..94 + Lt@51..52 "<" + TypeGenericArg@52..71 + PathType@52..71 + Path@52..71 + PathSegment@52..64 + QualifiedType@52..64 + Lt@52..53 "<" + PathType@53..54 + Path@53..54 + PathSegment@53..54 + Ident@53..54 "T" + WhiteSpace@54..55 " " + AsKw@55..57 "as" + WhiteSpace@57..58 " " + TraitRef@58..63 + Path@58..63 + PathSegment@58..63 + Ident@58..63 "Model" + Gt@63..64 ">" + Colon2@64..66 "::" + PathSegment@66..71 + Ident@66..71 "Point" + Comma@71..72 "," + WhiteSpace@72..73 " " + TypeGenericArg@73..93 + PathType@73..93 + Path@73..93 + PathSegment@73..85 + QualifiedType@73..85 + Lt@73..74 "<" + PathType@74..75 + Path@74..75 + PathSegment@74..75 + Ident@74..75 "T" + WhiteSpace@75..76 " " + AsKw@76..78 "as" + WhiteSpace@78..79 " " + TraitRef@79..84 + Path@79..84 + PathSegment@79..84 + Ident@79..84 "Model" + Gt@84..85 ">" + Colon2@85..87 "::" + PathSegment@87..93 + Ident@87..93 "Scalar" + Gt@93..94 ">" + Newline@94..95 "\n" diff --git a/crates/parser/tests/nested_generic_args.rs b/crates/parser/tests/nested_generic_args.rs new file mode 100644 index 0000000000..96f36c6d01 --- /dev/null +++ b/crates/parser/tests/nested_generic_args.rs @@ -0,0 +1,66 @@ +use fe_parser::{RecoveryMode, SyntaxKind, parse_source_file, syntax_node::SyntaxNode}; + +fn nest(depth: usize, innermost: &str) -> String { + let mut ty = innermost.to_string(); + for _ in 0..depth { + ty = format!("Wrap<<{ty} as Model>::Point>"); + } + ty +} + +/// Each level of `Wrap<::Point>` probed the positions inside it to +/// tell generic arguments from a shift, and every enclosing level probed them +/// all again, so parsing cost grew by roughly 5x per level: depth 12 took about +/// 96 seconds. This depth is unreachable that way and immediate with probe +/// outcomes reused per position, so the test bounds the speculative work as much +/// as it checks the tree. +#[test] +fn deeply_nested_qualified_generic_args_parse_without_repeating_probes() { + const DEPTH: usize = 32; + + let source = format!("type A = {}\n", nest(DEPTH, "T")); + + let (green, errors) = parse_source_file(&source, RecoveryMode::new(false)); + assert!(errors.is_empty(), "{errors:#?}"); + + let cst = SyntaxNode::new_root(green); + assert_eq!(cst.text().to_string(), source); + assert_eq!( + cst.descendants() + .filter(|node| node.kind() == SyntaxKind::QualifiedType) + .count(), + DEPTH + ); +} + +/// Malformed nesting must be bounded too, which means reusing probe outcomes +/// that recovered. Excluding those instead, as an earlier revision did, left +/// each enclosing level re-parsing everything below it: depth 10 cost about +/// 13 ms and grew over twofold per level, putting this depth out of reach. +/// Reuse is sound only because `recover` is confined to the scopes the probe +/// itself opened, so how far it consumes cannot depend on the caller. The +/// confinement has no effect on timing, so this test pins the reuse; the suite +/// as a whole pins that confining recovery changed no tree or diagnostic. +#[test] +fn deeply_nested_malformed_generic_args_do_not_repeat_probes() { + const DEPTH: usize = 32; + + // A qualified type missing its `as`, innermost, and an argument list that + // never closes: both force recovery inside the probes at every level. + for innermost in ["Wrap<::Point>", "Wrap<::Point"] { + for recover in [false, true] { + let source = format!("type A = {}\n", nest(DEPTH, innermost)); + let (green, errors) = parse_source_file(&source, RecoveryMode::new(recover)); + assert!(!errors.is_empty(), "{innermost:?} recover={recover}"); + // Recovery keeps consuming, so the tree still spans the input; a + // `NoRecover` parse stops at the error and is not expected to. + if recover { + assert_eq!( + SyntaxNode::new_root(green).text().to_string(), + source, + "{innermost:?} recover={recover}", + ); + } + } + } +} diff --git a/crates/parser/tests/tree_sitter_parse.rs b/crates/parser/tests/tree_sitter_parse.rs index 69e90c1a3d..f946eef979 100644 --- a/crates/parser/tests/tree_sitter_parse.rs +++ b/crates/parser/tests/tree_sitter_parse.rs @@ -233,6 +233,90 @@ fn tree_sitter_parse_newline_lt_continuations() { } } +/// The compiler reads a `<<` as generic arguments when a qualified path follows, +/// in expression position as well as in type position, so the grammar has to +/// agree or editors flag valid code. A `<<` with anything else after it is still +/// a shift. +#[test] +fn tree_sitter_parse_qualified_first_generic_arg_in_expressions() { + let mut parser = new_parser(); + + const PRELUDE: &str = + "trait Model {\n type Point\n}\nstruct Wrapped {\n value: T,\n}\nstruct M {}\n"; + + let accepted = [ + ( + "associated_function_call", + "fn f(p: u256) -> Wrapped<::Point> {\n Wrapped<::Point>::new(p)\n}\n", + ), + ( + "record_literal", + "fn f(p: u256) -> Wrapped<::Point> {\n Wrapped<::Point> { value: p }\n}\n", + ), + ( + "method_call", + "fn f(w: Wrapped, p: u256) -> u256 {\n w.pick<::Point>(p)\n}\n", + ), + ]; + + for (name, body) in accepted { + let source = format!("{PRELUDE}{body}"); + let errors = parse_errors(&mut parser, &source); + assert!( + errors.is_empty(), + "unexpected parse errors for {name}:\n{}", + errors.join("\n"), + ); + } + + // `<<` followed by an operand rather than a qualified path is a shift, and a + // qualified operand whose `>` is a comparison rather than `>::` still is. + assert_eq!( + body_expression_kinds( + &mut parser, + "fn shift(value: u256, bits: u8) -> u256 {\n value << bits as u256 >> 1\n}\n", + ), + ["binary_expression"], + ); + assert_eq!( + body_expression_kinds( + &mut parser, + "fn compare(value: u8, limit: u8) -> bool {\n value << ::BITS > limit\n}\n", + ), + ["binary_expression"], + ); + + // Deciding a `<<` means looking ahead for `>::`, and the lookahead runs on to + // the end of the enclosing block before giving up. Text inside a string or a + // comment is not code, so a `>::` there leaves an ordinary shift alone. + let shift_then_text = [ + ( + "string_holding_gt_colon2", + "fn f(x: u256, y: u256) -> u256 {\n let a = x << y\n let s = \"a>::b\"\n a\n}\n", + ), + ( + "line_comment_holding_gt_colon2", + "fn f(x: u256, y: u256) -> u256 {\n let a = x << y\n // note a>::b\n a\n}\n", + ), + ( + "block_comment_holding_gt_colon2", + "fn f(x: u256, y: u256) -> u256 {\n let a = x << y\n /* note a>::b */\n a\n}\n", + ), + ( + "shift_assign_then_string", + "fn f(x: u256, y: u256) -> u256 {\n let mut a = x\n a <<= y\n let s = \"a>::b\"\n a\n}\n", + ), + ]; + for (name, source) in shift_then_text { + let errors = parse_errors(&mut parser, source); + assert!( + errors.is_empty(), + "unexpected parse errors for {name}:\n{}", + errors.join("\n"), + ); + } +} + #[test] fn tree_sitter_matches_line_start_star_policy() { let mut parser = new_parser(); diff --git a/crates/tree-sitter-fe/src/scanner.c b/crates/tree-sitter-fe/src/scanner.c index 555872fbb2..5602638e9d 100644 --- a/crates/tree-sitter-fe/src/scanner.c +++ b/crates/tree-sitter-fe/src/scanner.c @@ -179,6 +179,137 @@ static bool scan_automatic_semicolon(TSLexer *lexer) { } +// Advance past a string literal, starting at its opening quote, so that its +// contents are never read as code. An escape is `\` and the character after it, +// which keeps `"\\"` from swallowing the closing quote. +static void advance_over_string_literal(TSLexer *lexer) { + advance(lexer); // consume the opening '"' + while (!lexer->eof(lexer)) { + if (lexer->lookahead == '\\') { + advance(lexer); + if (lexer->eof(lexer)) return; + advance(lexer); + continue; + } + bool is_end = lexer->lookahead == '"'; + advance(lexer); + if (is_end) return; + } +} + +// Advance past a comment if one starts at the `/` the caller is looking at, so +// that its text is never read as code. Consumes the `/` either way, since a lone +// `/` is division and needs no further handling. +static void advance_over_comment(TSLexer *lexer) { + advance(lexer); // consume the '/' + if (lexer->lookahead == '/') { + while (!lexer->eof(lexer) && lexer->lookahead != '\n') advance(lexer); + return; + } + if (lexer->lookahead != '*') return; + advance(lexer); + int depth = 1; + while (depth > 0 && !lexer->eof(lexer)) { + if (lexer->lookahead == '*') { + advance(lexer); + if (!lexer->eof(lexer) && lexer->lookahead == '/') { + advance(lexer); + depth--; + } + continue; + } + if (lexer->lookahead == '/') { + advance(lexer); + if (!lexer->eof(lexer) && lexer->lookahead == '*') { + advance(lexer); + depth++; + } + continue; + } + advance(lexer); + } +} + +// Whether a `<<` opens generic arguments whose first argument is a qualified +// path, as in `Wrapped<::Point>`, rather than being a left shift. +// +// The caller has consumed the first `<` and marked the token end, so this only +// looks ahead. It scans the second `<` to its matching `>` and requires `::` to +// follow, the same prefix rule the compiler's parser uses: no shift operand +// continues with `>::`, so `value << ::BITS > limit` stays a shift +// because the `>` that closes its angle nesting is followed by ` limit`. +// +// Nesting depths mirror the single-`<` scan below, so a `)`, `]`, `}` or `;` +// that closes something never opened settles it as a shift. String literals and +// comments are skipped whole: the scan runs on to the end of the enclosing block +// before giving up, and a `>::` in a message or a comment there is text, not the +// close of a qualified path. +static bool scan_qualified_path_after_lshift(TSLexer *lexer) { + if (lexer->lookahead != '<') return false; + advance(lexer); // consume the second '<' + // `<<=` is a shift-assign; no type starts with `=`. + if (lexer->lookahead == '=') return false; + + int angle_depth = 1; + int paren_depth = 0; + int bracket_depth = 0; + int brace_depth = 0; + + while (!lexer->eof(lexer)) { + switch (lexer->lookahead) { + case '(': paren_depth++; advance(lexer); continue; + case ')': + if (paren_depth == 0) return false; + paren_depth--; + advance(lexer); + continue; + case '[': bracket_depth++; advance(lexer); continue; + case ']': + if (bracket_depth == 0) return false; + bracket_depth--; + advance(lexer); + continue; + case '{': brace_depth++; advance(lexer); continue; + case '}': + if (brace_depth == 0) return false; + brace_depth--; + advance(lexer); + continue; + case ';': + if (bracket_depth == 0 && brace_depth == 0) return false; + advance(lexer); + continue; + case '<': + if (paren_depth == 0 && bracket_depth == 0 && brace_depth == 0) { + angle_depth++; + } + advance(lexer); + continue; + case '>': + if (paren_depth != 0 || bracket_depth != 0 || brace_depth != 0) { + advance(lexer); + continue; + } + advance(lexer); + if (--angle_depth > 0) continue; + // The qualified path is closed; `::` after it is what distinguishes it + // from a shift. + while (lexer->lookahead == ' ' || lexer->lookahead == '\t') { + advance(lexer); + } + if (lexer->lookahead != ':') return false; + advance(lexer); + return lexer->lookahead == ':'; + case '"': advance_over_string_literal(lexer); continue; + case '/': advance_over_comment(lexer); continue; + default: + advance(lexer); + continue; + } + } + return false; +} + bool tree_sitter_fe_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { (void)payload; @@ -226,13 +357,23 @@ bool tree_sitter_fe_external_scanner_scan(void *payload, TSLexer *lexer, if (next == '<') { // Could be << (shift), <<= (shift-assign), or nested generics <::Item as ...>). - // Emit just the first '<' as GENERIC_OPEN. - if (valid_symbols[GENERIC_OPEN] && !valid_symbols[COMPARISON_LT]) { + if (valid_symbols[GENERIC_OPEN]) { lexer->mark_end(lexer); // mark end after first '<' - lexer->result_symbol = GENERIC_OPEN; - return true; + // In a type position COMPARISON_LT is not valid, so nothing else the + // '<<' could be: this is the start of a qualified path such as + // <::Item as ...>. + if (!valid_symbols[COMPARISON_LT]) { + lexer->result_symbol = GENERIC_OPEN; + return true; + } + // In an expression position both are valid, so apply the same rule + // the compiler's parser uses and require a qualified path after the + // '<<'. Without this, `Wrapped<::Point>::new(p)` lexes a + // shift and fails to parse even though the compiler accepts it. + if (scan_qualified_path_after_lshift(lexer)) { + lexer->result_symbol = GENERIC_OPEN; + return true; + } } // Otherwise let the internal lexer handle << / <<= return false; diff --git a/crates/uitest/fixtures/parser/nested_qualified_generic_arg_expr.fe b/crates/uitest/fixtures/parser/nested_qualified_generic_arg_expr.fe new file mode 100644 index 0000000000..5a3867c6ee --- /dev/null +++ b/crates/uitest/fixtures/parser/nested_qualified_generic_arg_expr.fe @@ -0,0 +1,32 @@ +trait Model { + type Point +} + +struct Wrapped { + value: T, +} + +impl Wrapped { + fn new(_ value: T) -> Self { + Wrapped { value } + } + + fn pick(self, _ value: U) -> U { + value + } +} + +fn wrap(_ point: M::Point) -> Wrapped<::Point> { + Wrapped<::Point>::new(point) +} + +fn rewrap(_ point: M::Point) -> Wrapped<::Point> { + Wrapped<::Point> { value: point } +} + +fn call_with_qualified_type_arg( + _ wrapped: Wrapped<::Point>, + _ point: M::Point, +) -> M::Point { + wrapped.pick<::Point>(point) +} diff --git a/crates/uitest/fixtures/parser/nested_qualified_generic_arg_expr.snap b/crates/uitest/fixtures/parser/nested_qualified_generic_arg_expr.snap new file mode 100644 index 0000000000..0735a7da1c --- /dev/null +++ b/crates/uitest/fixtures/parser/nested_qualified_generic_arg_expr.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/parser.rs +expression: diags +input_file: fixtures/parser/nested_qualified_generic_arg_expr.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/ambiguous_assoc_const_generic.fe b/crates/uitest/fixtures/ty_check/ambiguous_assoc_const_generic.fe new file mode 100644 index 0000000000..6beb931199 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/ambiguous_assoc_const_generic.fe @@ -0,0 +1,19 @@ +trait Gen { + const X: usize +} + +struct Foo {} + +impl Gen for Foo { + const X: usize = 1 +} + +impl Gen for Foo { + const X: usize = 2 +} + +// Both candidates are `Gen`, so the labels and the hint must name the type +// argument. +fn f() -> usize { + Foo::X +} diff --git a/crates/uitest/fixtures/ty_check/ambiguous_assoc_const_generic.snap b/crates/uitest/fixtures/ty_check/ambiguous_assoc_const_generic.snap new file mode 100644 index 0000000000..50c10bb225 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/ambiguous_assoc_const_generic.snap @@ -0,0 +1,18 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/ambiguous_assoc_const_generic.fe +--- +error[2-0018]: multiple trait candidates found + ┌─ ambiguous_assoc_const_generic.fe:18:10 + │ + 1 │ trait Gen { + │ --- + │ │ + │ candidate: `>::X` + │ candidate: `>::X` + · +18 │ Foo::X + │ ^ `X` is ambiguous + │ + = hint: specify the trait explicitly: `>::X` diff --git a/crates/uitest/fixtures/ty_check/assoc_type_diamond_equality_generic_receiver.fe b/crates/uitest/fixtures/ty_check/assoc_type_diamond_equality_generic_receiver.fe new file mode 100644 index 0000000000..2e790dc5ef --- /dev/null +++ b/crates/uitest/fixtures/ty_check/assoc_type_diamond_equality_generic_receiver.fe @@ -0,0 +1,15 @@ +pub trait Base { + type Item +} + +pub trait Left: Base {} + +pub trait Right: Base {} + +pub trait Both: Left + Right {} + +// The generic-receiver form of the diamond: `T::Item` reaches `Base::Item` +// through both supertraits, one carrying the equality. +pub fn take(_ value: T::Item) -> T::Item { + value +} diff --git a/crates/uitest/fixtures/ty_check/assoc_type_diamond_equality_generic_receiver.snap b/crates/uitest/fixtures/ty_check/assoc_type_diamond_equality_generic_receiver.snap new file mode 100644 index 0000000000..64f84759a4 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/assoc_type_diamond_equality_generic_receiver.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/assoc_type_diamond_equality_generic_receiver.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/assoc_type_not_from_other_param_bound.fe b/crates/uitest/fixtures/ty_check/assoc_type_not_from_other_param_bound.fe new file mode 100644 index 0000000000..fee33839f7 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/assoc_type_not_from_other_param_bound.fe @@ -0,0 +1,26 @@ +pub trait Arrow { + type Dom +} + +// Only `T` implements `Arrow`, so neither `Self::Dom` nor `U::Dom` may +// resolve through the bound on `T`. +pub trait Eval { + fn eval(_ value: Self::Dom) +} + +pub trait EvalWhere +where + T: Arrow, +{ + fn eval(_ value: Self::Dom) +} + +pub fn other_param(_ value: U::Dom) {} + +pub fn own_param(_ value: T::Dom) {} + +// `Self::Dom` comes from the supertrait alone. The bound on `T` must not +// offer a second candidate and make the reference ambiguous. +pub trait EvalSuper: Arrow { + fn eval(_ value: Self::Dom) +} diff --git a/crates/uitest/fixtures/ty_check/assoc_type_not_from_other_param_bound.snap b/crates/uitest/fixtures/ty_check/assoc_type_not_from_other_param_bound.snap new file mode 100644 index 0000000000..b407bc2093 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/assoc_type_not_from_other_param_bound.snap @@ -0,0 +1,22 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/assoc_type_not_from_other_param_bound.fe +--- +error[2-0002]: `Dom` is not found + ┌─ assoc_type_not_from_other_param_bound.fe:8:28 + │ +8 │ fn eval(_ value: Self::Dom) + │ ^^^ `Dom` is not found + +error[2-0002]: `Dom` is not found + ┌─ assoc_type_not_from_other_param_bound.fe:15:28 + │ +15 │ fn eval(_ value: Self::Dom) + │ ^^^ `Dom` is not found + +error[2-0002]: `Dom` is not found + ┌─ assoc_type_not_from_other_param_bound.fe:18:45 + │ +18 │ pub fn other_param(_ value: U::Dom) {} + │ ^^^ `Dom` is not found diff --git a/crates/uitest/fixtures/ty_check/impl_generic_trait_arg_wrong_return.fe b/crates/uitest/fixtures/ty_check/impl_generic_trait_arg_wrong_return.fe new file mode 100644 index 0000000000..348c407d03 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/impl_generic_trait_arg_wrong_return.fe @@ -0,0 +1,13 @@ +pub trait T { + type Out + fn f(_ a: A) -> Self::Out +} + +pub struct Wrap

{} + +impl> T for Wrap

{ + type Out = (

>::Out, A) + fn f(_ a: A) -> A { + a + } +} diff --git a/crates/uitest/fixtures/ty_check/impl_generic_trait_arg_wrong_return.snap b/crates/uitest/fixtures/ty_check/impl_generic_trait_arg_wrong_return.snap new file mode 100644 index 0000000000..5b05734ece --- /dev/null +++ b/crates/uitest/fixtures/ty_check/impl_generic_trait_arg_wrong_return.snap @@ -0,0 +1,13 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/impl_generic_trait_arg_wrong_return.fe +--- +error[6-0008]: method `f` has incompatible return type + ┌─ impl_generic_trait_arg_wrong_return.fe:10:8 + │ + 3 │ fn f(_ a: A) -> Self::Out + │ - trait requires this return type + · +10 │ fn f(_ a: A) -> A { + │ ^ expected `(P::Out, A)`, found `A` diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous.fe new file mode 100644 index 0000000000..a6ad9ea9f9 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous.fe @@ -0,0 +1,11 @@ +pub trait Left { + type Dom +} + +pub trait Right { + type Dom +} + +pub trait Both: Left + Right { + fn use_dom(_ value: Self::Dom) +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous.snap new file mode 100644 index 0000000000..939bc5c7ce --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous.snap @@ -0,0 +1,18 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_ambiguous.fe +--- +error[2-0009]: ambiguous associated type `Dom` + ┌─ supertrait_assoc_type_ambiguous.fe:10:31 + │ + 1 │ pub trait Left { + │ ---- candidate: `Left` + · + 5 │ pub trait Right { + │ ----- candidate: `Right` + · +10 │ fn use_dom(_ value: Self::Dom) + │ ^^^ associated type `Dom` is ambiguous + │ + = hint: specify the trait explicitly: `::Dom` diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous_generic.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous_generic.fe new file mode 100644 index 0000000000..d8eaa46851 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous_generic.fe @@ -0,0 +1,8 @@ +pub trait Gen { + type Out +} + +// Both supertraits are `Gen`, so the hint must name the type argument. +pub trait Both: Gen + Gen { + fn use_out(_ value: Self::Out) +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous_generic.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous_generic.snap new file mode 100644 index 0000000000..b0b1889ac4 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_ambiguous_generic.snap @@ -0,0 +1,18 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_ambiguous_generic.fe +--- +error[2-0009]: ambiguous associated type `Out` + ┌─ supertrait_assoc_type_ambiguous_generic.fe:7:31 + │ +1 │ pub trait Gen { + │ --- + │ │ + │ candidate: `Gen` + │ candidate: `Gen` + · +7 │ fn use_out(_ value: Self::Out) + │ ^^^ associated type `Out` is ambiguous + │ + = hint: specify the trait explicitly: `>::Out` diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_conflicting_equalities.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_conflicting_equalities.fe new file mode 100644 index 0000000000..78c12e0a7a --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_conflicting_equalities.fe @@ -0,0 +1,13 @@ +pub trait Base { + type Item +} + +pub trait Left: Base {} + +pub trait Right: Base {} + +// Two paths to one declaration that disagree on the equality must stay +// ambiguous rather than collapsing to whichever was seen first. +pub trait Both: Left + Right { + fn take(_ value: Self::Item) -> Self::Item +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_conflicting_equalities.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_conflicting_equalities.snap new file mode 100644 index 0000000000..34951d03ce --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_conflicting_equalities.snap @@ -0,0 +1,32 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_conflicting_equalities.fe +--- +error[2-0009]: ambiguous associated type `Item` + ┌─ supertrait_assoc_type_conflicting_equalities.fe:12:28 + │ + 1 │ pub trait Base { + │ ---- + │ │ + │ candidate: ::Item = bool + │ candidate: ::Item = u256 + · +12 │ fn take(_ value: Self::Item) -> Self::Item + │ ^^^^ associated type `Item` is ambiguous + │ + = hint: specify the trait explicitly: `::Item` + +error[2-0009]: ambiguous associated type `Item` + ┌─ supertrait_assoc_type_conflicting_equalities.fe:12:43 + │ + 1 │ pub trait Base { + │ ---- + │ │ + │ candidate: ::Item = bool + │ candidate: ::Item = u256 + · +12 │ fn take(_ value: Self::Item) -> Self::Item + │ ^^^^ associated type `Item` is ambiguous + │ + = hint: specify the trait explicitly: `::Item` diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_diamond_equality.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_diamond_equality.fe new file mode 100644 index 0000000000..a22d7c32b1 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_diamond_equality.fe @@ -0,0 +1,13 @@ +pub trait Base { + type Item +} + +pub trait Left: Base {} + +pub trait Right: Base {} + +// Both supertraits reach the same `Base::Item` declaration, one of them with an +// equality. Two paths to one declaration are not an ambiguity. +pub trait Both: Left + Right { + fn take(_ value: Self::Item) -> Self::Item +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_diamond_equality.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_diamond_equality.snap new file mode 100644 index 0000000000..fc23c8cda9 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_diamond_equality.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_diamond_equality.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_method_where_bound.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_method_where_bound.fe new file mode 100644 index 0000000000..45bd6397a2 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_method_where_bound.fe @@ -0,0 +1,16 @@ +pub trait Base { + type Item +} + +pub trait Extra { + type Item +} + +// `Base` is implied by the supertrait and `Extra` by the method's own where +// clause. Both declare `Item`, so `Self::Item` is ambiguous; picking the +// inherited one silently would type the method against the wrong item. +pub trait Sub: Base { + fn take(_ value: Self::Item) + where + Self: Extra +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_method_where_bound.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_method_where_bound.snap new file mode 100644 index 0000000000..a5022e731a --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_method_where_bound.snap @@ -0,0 +1,18 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_method_where_bound.fe +--- +error[2-0009]: ambiguous associated type `Item` + ┌─ supertrait_assoc_type_method_where_bound.fe:13:28 + │ + 1 │ pub trait Base { + │ ---- candidate: `Base` + · + 5 │ pub trait Extra { + │ ----- candidate: `Extra` + · +13 │ fn take(_ value: Self::Item) + │ ^^^^ associated type `Item` is ambiguous + │ + = hint: specify the trait explicitly: `::Item` diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_not_from_assoc_bound.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_not_from_assoc_bound.fe new file mode 100644 index 0000000000..d9b01feb3d --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_not_from_assoc_bound.fe @@ -0,0 +1,10 @@ +pub trait Arrow { + type Dom +} + +// `Self::Inner` implements `Arrow`, but `Self` does not, so `Self::Dom` +// must not resolve through the bound on `Inner`. +pub trait Holder { + type Inner: Arrow + fn take(_ value: Self::Dom) +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_not_from_assoc_bound.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_not_from_assoc_bound.snap new file mode 100644 index 0000000000..9a16c1b0c1 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_not_from_assoc_bound.snap @@ -0,0 +1,10 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_not_from_assoc_bound.fe +--- +error[2-0002]: `Dom` is not found + ┌─ supertrait_assoc_type_not_from_assoc_bound.fe:9:28 + │ +9 │ fn take(_ value: Self::Dom) + │ ^^^ `Dom` is not found diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_repeated_bound.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_repeated_bound.fe new file mode 100644 index 0000000000..9dbc3445ea --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_repeated_bound.fe @@ -0,0 +1,8 @@ +pub trait Base { + type Item +} + +// The same supertrait listed twice, once with an equality, is one candidate. +pub trait Sub: Base + Base { + fn take(_ value: Self::Item) +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_repeated_bound.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_repeated_bound.snap new file mode 100644 index 0000000000..d80f61ce76 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_repeated_bound.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_repeated_bound.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_via_self.fe b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_via_self.fe new file mode 100644 index 0000000000..0ce123f360 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_via_self.fe @@ -0,0 +1,7 @@ +pub trait Arrow { + type Dom +} + +pub trait Eval: Arrow { + fn eval(_ value: Self::Dom) -> Self::Dom +} diff --git a/crates/uitest/fixtures/ty_check/supertrait_assoc_type_via_self.snap b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_via_self.snap new file mode 100644 index 0000000000..44208967df --- /dev/null +++ b/crates/uitest/fixtures/ty_check/supertrait_assoc_type_via_self.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/supertrait_assoc_type_via_self.fe +--- + diff --git a/newsfragments/+assoc-type-other-param-bound.bugfix.md b/newsfragments/+assoc-type-other-param-bound.bugfix.md new file mode 100644 index 0000000000..610549d2ed --- /dev/null +++ b/newsfragments/+assoc-type-other-param-bound.bugfix.md @@ -0,0 +1 @@ +Report `U::Name` and `Self::Name` as not found when only the bound on a different type parameter declares `Name`. For example, in `fn f(_ x: U::Dom)` and in `trait Eval { fn eval(_ x: Self::Dom) }`, the name used to resolve silently to `T::Dom`. diff --git a/newsfragments/+finite-nested-layouts.bugfix.md b/newsfragments/+finite-nested-layouts.bugfix.md new file mode 100644 index 0000000000..3c88063483 --- /dev/null +++ b/newsfragments/+finite-nested-layouts.bugfix.md @@ -0,0 +1 @@ +Fix layout checking rejecting types that repeat a generic type through ordinary fields, such as a struct holding `Option` where `Argument` holds `Option`. Such layouts are finite. Types whose arguments grow, such as `Growing` containing `Growing<(T, T)>`, are still reported as expanding. diff --git a/newsfragments/+fmt-bare-qualified-arg.bugfix.md b/newsfragments/+fmt-bare-qualified-arg.bugfix.md new file mode 100644 index 0000000000..8a62c0866f --- /dev/null +++ b/newsfragments/+fmt-bare-qualified-arg.bugfix.md @@ -0,0 +1 @@ +`fe fmt` no longer joins a bare qualified first generic argument onto the opening `<`, which turned `Wrapped< >` into `Wrapped<>` and changed how it parsed. It now formats as `Wrapped< >`. diff --git a/newsfragments/+for-continue.bugfix.md b/newsfragments/+for-continue.bugfix.md new file mode 100644 index 0000000000..be53737f1a --- /dev/null +++ b/newsfragments/+for-continue.bugfix.md @@ -0,0 +1 @@ +Fix `continue` in `for` loops so the loop moves on to the next element. Previously `continue` jumped back without advancing, so the loop revisited the same element. diff --git a/newsfragments/+nested-generic-arg-probes.performance.md b/newsfragments/+nested-generic-arg-probes.performance.md new file mode 100644 index 0000000000..9509f7212c --- /dev/null +++ b/newsfragments/+nested-generic-arg-probes.performance.md @@ -0,0 +1 @@ +Reuse the outcome of a speculative parse at a position instead of repeating it. Disambiguating generic arguments from shifts and comparisons re-parsed the same nested syntax once per enclosing level, so deeply nested arguments such as `Wrap<::Point> as Model>::Point>` cost roughly five times more per level of depth. A 12-level type took about 96 seconds to parse and now takes milliseconds. Incomplete and malformed nesting, which is what an editor sees while the code is still being typed, is reused the same way. diff --git a/newsfragments/+nested-qualified-type-arg.bugfix.md b/newsfragments/+nested-qualified-type-arg.bugfix.md new file mode 100644 index 0000000000..c73cfb8e8f --- /dev/null +++ b/newsfragments/+nested-qualified-type-arg.bugfix.md @@ -0,0 +1 @@ +Fix parsing of generic arguments that start with a qualified path, such as `Wrapped<::Point>`. The `<<` is no longer mistaken for a left shift, so the argument now belongs to `Wrapped` in type position, in expression position, and in a method call such as `wrapped.pick<::Point>(value)`. diff --git a/newsfragments/+supertrait-assoc-type.bugfix.md b/newsfragments/+supertrait-assoc-type.bugfix.md new file mode 100644 index 0000000000..60a2af6fc0 --- /dev/null +++ b/newsfragments/+supertrait-assoc-type.bugfix.md @@ -0,0 +1 @@ +Resolve `Self::Name` inside a trait when `Name` is an associated type of one of its supertraits, such as `Self::Dom` in `trait Eval: Arrow`. Bounds in scope at the reference, such as a method's own `where Self: Extra`, are considered alongside the inherited ones, and the reference is reported as ambiguous when they name different associated types. Reaching a single declaration through several supertraits is not ambiguous, even when only some of them bind it with an equality. diff --git a/newsfragments/+tree-sitter-qualified-generic-expr.bugfix.md b/newsfragments/+tree-sitter-qualified-generic-expr.bugfix.md new file mode 100644 index 0000000000..2d3dfda2c2 --- /dev/null +++ b/newsfragments/+tree-sitter-qualified-generic-expr.bugfix.md @@ -0,0 +1 @@ +Teach the tree-sitter grammar the generic arguments that start with a qualified path, such as `Wrapped<::Point>::new(point)`. The compiler already accepted these in expression position while the grammar reported an error, so editors flagged valid code. A `<<` that is not followed by a qualified path is still a left shift.