Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

#2976. Add semantics tests #2998

Merged
merged 11 commits into from
Nov 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ main() {
E e2 = .staticMethod();
Expect.equals("v2", e2.value);

E e3 = E.values[1];
E e3 = .values[1];
Expect.equals(E.v2, e3);
}
29 changes: 29 additions & 0 deletions LanguageFeatures/Static-access-shorthand/grammar_A03_t01.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// @assertion We also add `.` to the tokens that an expression statement cannot
/// start with.
///
/// @description Checks that numerical literal like `.123` is still not an error
/// @author [email protected]

// SharedOptions=--enable-experiment=enum-shorthands

main() {
.123;

(.123);

var v1 = .123 + 1;

if (.123 > 0) {}

const half = .5;

final zero = 0 - .0;

.314e+1;

var pi = .214e+1 + 1;
}
121 changes: 121 additions & 0 deletions LanguageFeatures/Static-access-shorthand/semantics_A01_t01.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// @assertion Dart semantics, static and dynamic, do not follow the grammar
/// precisely. For example, a static member invocation expression of the form
/// `C.id<T1>(e2)` is treated as an atomic entity for type inference (and
/// runtime semantics). It’s not a combination of doing a `C.id` tear-off, then
/// a `<T1>` instantiation and then an `(e2)` invocation. The context type of
/// that entire expression is used throughout the inference, where
/// `(e1.id<T1>)(e2)` has `(e1.id<T1>)` in a position where it has no context
/// type.
///
/// Because of that, the specification of the static and runtime semantics of
/// the new constructs needs to address all the forms `.id`, `.id<typeArgs>`,
/// `.id(args)`, `.id<typeArgs>(args)`, `.new` or `.new(args)`.
eernstg marked this conversation as resolved.
Show resolved Hide resolved
/// ...
/// The general rule is that any of the expression forms above, starting with
/// `.id`, are treated exactly as if they were prefixed by a fresh identifier
/// `X` which denotes an accessible type alias for the greatest closure of the
/// context type scheme of the following primary and selector chain.
///
/// @description Checks that expressions of the form `.id`, `.id(args)` and
/// `.id<typeArgs>(args)` are treated as if they are prefixed by a fresh
/// identifier `X` which denotes an accessible type alias.
Copy link
Member

Choose a reason for hiding this comment

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

This is really hard to test! ;-)

The point could be that a type alias can be non-generic and still denote a parameterized type (typedef X = SomeType<With, Args>;), and this allows for the constructor invocations to have the actual type arguments, and it also allows for a static method invocation to ignore the same type arguments (because that's how we treat type aliases when used to access static members).

Apart from this, there would be no distinction between this description and a shorter one that simply says "the semantics of .id is T.id, where T is the greatest closure of the context type.

However, that works for invocations of static methods, but it is not the actual semantics of constructor invocations: For them we strip off the actual type arguments (so the greatest closure operation is ignored ... for both static member invocations and constructor invocations!), and then type inference is performed as if no actual type arguments were passed.

This means that it isn't hugely meaningful to test "as if prefixed by a type alias" at all.

What we need to test is that it works to call a static method even in the case where the context type has actual type arguments (where those actual type arguments must be ignored).

Next, we should test that it works to invoke a constructor in a case where the context type has actual type arguments (e.g., List<num> as context type should make .filled(10, 10) mean List<num>.filled(10, 10), and the type argument should not be int, as it would be if the context type were _).

Finally, it should work to invoke a constructor with a context type schema (say, List<Iterable<_>>), and we should then see that the actual arguments to the constructor invocation plays a role:

Object foo<X>(Map<int, X> map) => map;
var map = foo(.fromIterables([1], ['Hello']));

In this case we'd get int from the context type and String from the actual argument ['Hello'], and the result would then be a Map<int, String>.

Copy link
Member

Choose a reason for hiding this comment

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

This is really hard to test! ;-)

And the spec also doesn't use the "like a type alias" phrasing any more. Since yesterday.

What's important to test is that it:

  • Accesses the correct static member.
  • Does so, even if the name of that type is shadowed, or not even in scope.

so .filled with context type List<int> denotes (the constructor) List.filled. Even if List is not in scope.

The current spec also doesn't use "the greatest closure of the context type" any more. An occurrence of _ doesn't matter, it's either in a position where it's irrelevant, or the scheme is _ in which case it doesn't denote a static namespace.

To find the static namespace, it just looks at the context type scheme, and if it's of the form S? or FutureOr<S>, it recurses on the S, and otherwise it only recognizes types of the form C/C<typeArgs> where C is a type introduced by a class, mixin, enum or extension type declaration.

(It's written declaratively, "a type scheme denotes a static namespace N if and only if either the type scheme is a type introduced by a type declaration and N is the static namespace of that declaration, or the type scheme is either S? or FutureOr<S> and S denotes the static namespace N.".)

So if the context type scheme is a non-generic type or instantiated generic type scheme whose type is introduced by the declaration D, then .id denotes the static member named id or constructor with base name id in D.
And if it's a constructor, then it's a raw reference to that constructor, where type arguments to the class can be inferred from arguments or actual context type (not shorthand context, in the cases where those differ.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • Does so, even if the name of that type is shadowed, or not even in scope.
    so .filled with context type List<int> denotes (the constructor) List.filled. Even if List is not in scope.

How can we have a context type List<int> if List is not in scope or shadowed? It's just a compile-time error.

class C {
  int List() => 42;
  
  test() {
    List<int> l = List.filled(1, 1);
//  ^^^^  Error: List isn't a type
  }
}
class List<T> {}

main() {
  List<int> l = List<int>.filled(1, 1);
  //                      ^^^^^^ Error: Couldn't find constructor 'List.filled'.
}

Please, clarify.

Copy link
Member

@eernstg eernstg Nov 28, 2024

Choose a reason for hiding this comment

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

How can we have a context type List<int> if List is not in scope or shadowed?

You can declare a function like void foo<X>(List<X> xs) {} in another library 'lib.dart', and then call foo from a library that imports 'lib.dart' and imports 'dart:core' with a prefix such that List isn't in scope without the prefix. foo(.filled(1, 1)) should then work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you!

/// @author [email protected]

// SharedOptions=--enable-experiment=enum-shorthands

import '../../Utils/expect.dart';

class C<T> {
T t;
C(this.t);

static C<int> get id1 => C(1);
static C<X> id2<X>(X x) => C<X>(x);
}

typedef CAlias<T> = C<T>;
typedef CInt = C<int>;

mixin M<T> on C<T> {
static M<int> get id1 => MC(1);
static M<X> id2<X>(X x) => MC<X>(x);
}

class MC<T> = C<T> with M<T>;

typedef MAlias<T> = M<T>;
typedef MInt = M<int>;

enum E<T> {
e1(1), e2("2");
final T t;
const E(this.t);

static E<int> get id1 => E.e1;
static E<String> id2() => E.e2;
}

typedef EAlias<T> = E<T>;
typedef EInt = E<int>;

extension type ET<T>(T t) {
static ET<int> get id1 => ET(1);
static ET<X> id2<X>(X x) => ET<X>(x);
}

typedef ETAlias<T> = ET<T>;
typedef ETInt = ET<int>;

main() {
C<int> c1 = .id1;
Expect.equals(1, c1.t);

C<String> c2 = .id2("c2");
Expect.equals("c2", c2.t);

CAlias<int> c3 = .id1;
Expect.equals(1, c3.t);

CInt c4 = .id2<int>(4);
Expect.equals(4, c4.t);

M<int> m1 = .id1;
Expect.equals(1, m1.t);

M<String> m2 = .id2("m2");
Expect.equals("m2", m2.t);

MAlias<int> m3 = .id1;
Expect.equals(1, m3.t);

MInt m4 = .id2<int>(4);
Expect.equals(4, m4.t);

E<int> e1 = .id1;
Expect.equals(E.e0, e1);

E<String> e2 = .id2();
Expect.equals(E.e2, e2);

EInt e3 = .id1;
Expect.equals(1, e3.t);

EAlias<String> e4 = .id2();
Expect.equals("2", e4.t);

ET<int> et1 = .id1;
Expect.equals(1, et1.t);

ET<String> et2 = .id2("et2");
Expect.equals("et2", et2.t);

ETAlias<int> et3 = .id1;
Expect.equals(1, et3.t);

ETInt et4 = .id2<int>(4);
Expect.equals(4, et4.t);
eernstg marked this conversation as resolved.
Show resolved Hide resolved
}
82 changes: 82 additions & 0 deletions LanguageFeatures/Static-access-shorthand/semantics_A01_t02.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// @assertion Dart semantics, static and dynamic, do not follow the grammar
/// precisely. For example, a static member invocation expression of the form
/// `C.id<T1>(e2)` is treated as an atomic entity for type inference (and
/// runtime semantics). It’s not a combination of doing a `C.id` tear-off, then
/// a `<T1>` instantiation and then an `(e2)` invocation. The context type of
/// that entire expression is used throughout the inference, where
/// `(e1.id<T1>)(e2)` has `(e1.id<T1>)` in a position where it has no context
/// type.
///
/// Because of that, the specification of the static and runtime semantics of
/// the new constructs needs to address all the forms `.id`, `.id<typeArgs>`,
/// `.id(args)`, `.id<typeArgs>(args)`, `.new` or `.new(args)`.
/// ...
/// The general rule is that any of the expression forms above, starting with
/// `.id`, are treated exactly as if they were prefixed by a fresh identifier
/// `X` which denotes an accessible type alias for the greatest closure of the
/// context type scheme of the following primary and selector chain.
///
/// @description Checks that expressions of the form `.id`, `.id(args)` and
/// `.id<typeArgs>(args)` are treated as if they are prefixed by a fresh
/// identifier `X` which denotes an accessible type alias. Test unprefixed
/// import.
/// @author [email protected]

// SharedOptions=--enable-experiment=enum-shorthands

import '../../Utils/expect.dart';
import 'shorthand_lib.dart';

main() {
C<int> c1 = .id1;
Expect.equals(1, c1.t);

C<String> c2 = .id2("c2");
Expect.equals("c2", c2.t);

CAlias<int> c3 = .id1;
Expect.equals(1, c3.t);

CInt c4 = .id2<int>(4);
Expect.equals(4, c4.t);

M<int> m1 = .id1;
Expect.equals(1, m1.t);

M<String> m2 = .id2("m2");
Expect.equals("m2", m2.t);

MAlias<int> m3 = .id1;
Expect.equals(1, m3.t);

MInt m4 = .id2<int>(4);
Expect.equals(4, m4.t);

E<int> e1 = .id1;
Expect.equals(E.e0, e1);

E<String> e2 = .id2();
Expect.equals(E.e2, e2);

EInt e3 = .id1;
Expect.equals(1, e3.t);

EAlias<String> e4 = .id2();
Expect.equals("2", e4.t);

ET<int> et1 = .id1;
Expect.equals(1, et1.t);

ET<String> et2 = .id2("et2");
Expect.equals("et2", et2.t);

ETAlias<int> et3 = .id1;
Expect.equals(1, et3.t);

ETInt et4 = .id2<int>(4);
Expect.equals(4, et4.t);
}
82 changes: 82 additions & 0 deletions LanguageFeatures/Static-access-shorthand/semantics_A01_t03.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// @assertion Dart semantics, static and dynamic, do not follow the grammar
/// precisely. For example, a static member invocation expression of the form
/// `C.id<T1>(e2)` is treated as an atomic entity for type inference (and
/// runtime semantics). It’s not a combination of doing a `C.id` tear-off, then
/// a `<T1>` instantiation and then an `(e2)` invocation. The context type of
/// that entire expression is used throughout the inference, where
/// `(e1.id<T1>)(e2)` has `(e1.id<T1>)` in a position where it has no context
/// type.
///
/// Because of that, the specification of the static and runtime semantics of
/// the new constructs needs to address all the forms `.id`, `.id<typeArgs>`,
/// `.id(args)`, `.id<typeArgs>(args)`, `.new` or `.new(args)`.
/// ...
/// The general rule is that any of the expression forms above, starting with
/// `.id`, are treated exactly as if they were prefixed by a fresh identifier
/// `X` which denotes an accessible type alias for the greatest closure of the
/// context type scheme of the following primary and selector chain.
///
/// @description Checks that expressions of the form `.id`, `.id(args)` and
/// `.id<typeArgs>(args)` are treated as if they are prefixed by a fresh
/// identifier `X` which denotes an accessible type alias. Test prefixed
/// import.
/// @author [email protected]

// SharedOptions=--enable-experiment=enum-shorthands

import '../../Utils/expect.dart';
import 'shorthand_lib.dart' as p;

main() {
p.C<int> c1 = .id1;
Expect.equals(1, c1.t);

p.C<String> c2 = .id2("c2");
Expect.equals("c2", c2.t);

p.CAlias<int> c3 = .id1;
Expect.equals(1, c3.t);

p.CInt c4 = .id2<int>(4);
Expect.equals(4, c4.t);

p.M<int> m1 = .id1;
Expect.equals(1, m1.t);

p.M<String> m2 = .id2("m2");
Expect.equals("m2", m2.t);

p.MAlias<int> m3 = .id1;
Expect.equals(1, m3.t);

p.MInt m4 = .id2<int>(4);
Expect.equals(4, m4.t);

p.E<int> e1 = .id1;
Expect.equals(E.e0, e1);

p.E<String> e2 = .id2();
Expect.equals(E.e2, e2);

p.EInt e3 = .id1;
Expect.equals(1, e3.t);

p.EAlias<String> e4 = .id2();
Expect.equals("2", e4.t);

p.ET<int> et1 = .id1;
Expect.equals(1, et1.t);

p.ET<String> et2 = .id2("et2");
Expect.equals("et2", et2.t);

p.ETAlias<int> et3 = .id1;
Expect.equals(1, et3.t);

p.ETInt et4 = .id2<int>(4);
Expect.equals(4, et4.t);
}
Loading