From 93e19649aaab23e922a731c5bdafa072db741401 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Thu, 21 Jul 2022 16:16:18 -0400 Subject: [PATCH] Support for `as` operator and user-defined `As` impls. (#1476) --- common/fuzzing/carbon.proto | 1 + common/fuzzing/proto_to_carbon.cpp | 4 ++++ explorer/ast/expression.cpp | 2 ++ explorer/ast/expression.h | 17 +++++++++++++ explorer/data/prelude.carbon | 15 ++++++++++++ explorer/fuzzing/ast_to_proto.cpp | 2 ++ explorer/interpreter/action_stack.cpp | 9 +++++++ explorer/interpreter/action_stack.h | 3 +++ explorer/interpreter/builtins.cpp | 3 +++ explorer/interpreter/builtins.h | 5 ++-- explorer/interpreter/interpreter.cpp | 8 +++++++ explorer/interpreter/type_checker.cpp | 20 ++++++++++++++++ explorer/syntax/parser.ypp | 24 +++++++++++++++++-- explorer/testdata/as/as_same_type.carbon | 17 +++++++++++++ explorer/testdata/as/convert.carbon | 23 ++++++++++++++++++ .../as/fail_destination_not_type.carbon | 16 +++++++++++++ .../testdata/as/fail_no_conversion.carbon | 20 ++++++++++++++++ explorer/testdata/as/implicit_as.carbon | 23 ++++++++++++++++++ .../generic_function/generic_method.carbon | 4 ---- .../testdata/impl/generic_method_impl.carbon | 4 ---- 20 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 explorer/testdata/as/as_same_type.carbon create mode 100644 explorer/testdata/as/convert.carbon create mode 100644 explorer/testdata/as/fail_destination_not_type.carbon create mode 100644 explorer/testdata/as/fail_no_conversion.carbon create mode 100644 explorer/testdata/as/implicit_as.carbon diff --git a/common/fuzzing/carbon.proto b/common/fuzzing/carbon.proto index dbc7565a9de6..b6c20d8f51e4 100644 --- a/common/fuzzing/carbon.proto +++ b/common/fuzzing/carbon.proto @@ -53,6 +53,7 @@ message PrimitiveOperatorExpression { Sub = 10; Ptr = 11; Combine = 12; + As = 13; } optional Operator op = 1; repeated Expression arguments = 2; diff --git a/common/fuzzing/proto_to_carbon.cpp b/common/fuzzing/proto_to_carbon.cpp index fde2952e99e0..d3909bb90c73 100644 --- a/common/fuzzing/proto_to_carbon.cpp +++ b/common/fuzzing/proto_to_carbon.cpp @@ -106,6 +106,10 @@ static auto PrimitiveOperatorToCarbon( PrefixUnaryOperatorToCarbon("&", arg0, out); break; + case Fuzzing::PrimitiveOperatorExpression::As: + BinaryOperatorToCarbon(arg0, " as ", arg1, out); + break; + case Fuzzing::PrimitiveOperatorExpression::Deref: PrefixUnaryOperatorToCarbon("*", arg0, out); break; diff --git a/explorer/ast/expression.cpp b/explorer/ast/expression.cpp index d979fa62a19b..13e07e5420fd 100644 --- a/explorer/ast/expression.cpp +++ b/explorer/ast/expression.cpp @@ -61,6 +61,8 @@ auto ToString(Operator op) -> std::string_view { switch (op) { case Operator::Add: return "+"; + case Operator::As: + return "as"; case Operator::AddressOf: case Operator::Combine: return "&"; diff --git a/explorer/ast/expression.h b/explorer/ast/expression.h index f454948b3262..ed96add403ce 100644 --- a/explorer/ast/expression.h +++ b/explorer/ast/expression.h @@ -115,6 +115,7 @@ enum class Operator { Add, AddressOf, And, + As, Combine, Deref, Eq, @@ -506,9 +507,25 @@ class PrimitiveOperatorExpression : public Expression { return arguments_; } + // Set the rewritten form of this expression. Can only be called during type + // checking. + auto set_rewritten_form(const Expression* rewritten_form) -> void { + CARBON_CHECK(!rewritten_form_.has_value()) << "rewritten form set twice"; + rewritten_form_ = rewritten_form; + set_static_type(&rewritten_form->static_type()); + set_value_category(rewritten_form->value_category()); + } + // Get the rewritten form of this expression. A rewritten form is used when + // the expression is rewritten as a function call on an interface. A + // rewritten form is not used when providing built-in operator semantics. + auto rewritten_form() const -> std::optional> { + return rewritten_form_; + } + private: Operator op_; std::vector> arguments_; + std::optional> rewritten_form_; }; using ImplExpMap = std::map, Nonnull>; diff --git a/explorer/data/prelude.carbon b/explorer/data/prelude.carbon index a8c6055a3618..e06202efaa62 100644 --- a/explorer/data/prelude.carbon +++ b/explorer/data/prelude.carbon @@ -4,11 +4,26 @@ package Carbon api; +// Explicitly convert `Self` to `T`. +interface As(T:! Type) { + fn Convert[me: Self]() -> T; +} + // Implicitly convert `Self` to `T`. interface ImplicitAs(T:! Type) { fn Convert[me: Self]() -> T; } +// TODO: ImplicitAs(T) should extend As(T). +impl forall [T:! Type, U:! ImplicitAs(T)] U as As(T) { + fn Convert[me: Self]() -> T { return me.Convert(); } +} + +// Every type implicitly converts to itself. +impl forall [T:! Type] T as ImplicitAs(T) { + fn Convert[me: Self]() -> T { return me; } +} + // TODO: Simplify this once we have variadics. // TODO: Should these be final? impl forall [U1:! Type, T1:! ImplicitAs(U1)] diff --git a/explorer/fuzzing/ast_to_proto.cpp b/explorer/fuzzing/ast_to_proto.cpp index 2aa7ea52c999..9a8f332c32e1 100644 --- a/explorer/fuzzing/ast_to_proto.cpp +++ b/explorer/fuzzing/ast_to_proto.cpp @@ -37,6 +37,8 @@ static auto OperatorToProtoEnum(const Operator op) switch (op) { case Operator::AddressOf: return Fuzzing::PrimitiveOperatorExpression::AddressOf; + case Operator::As: + return Fuzzing::PrimitiveOperatorExpression::As; case Operator::Deref: return Fuzzing::PrimitiveOperatorExpression::Deref; case Operator::Neg: diff --git a/explorer/interpreter/action_stack.cpp b/explorer/interpreter/action_stack.cpp index 1dd9814ab540..7f24d7ecb181 100644 --- a/explorer/interpreter/action_stack.cpp +++ b/explorer/interpreter/action_stack.cpp @@ -176,6 +176,15 @@ auto ActionStack::Spawn(std::unique_ptr child, RuntimeScope scope) return Success(); } +auto ActionStack::ReplaceWith(std::unique_ptr replacement) + -> ErrorOr { + std::unique_ptr old = todo_.Pop(); + CARBON_CHECK(replacement->kind() == old->kind()) + << "ReplaceWith can't change action kind"; + todo_.Push(std::move(replacement)); + return Success(); +} + auto ActionStack::RunAgain() -> ErrorOr { Action& action = *todo_.Top(); action.set_pos(action.pos() + 1); diff --git a/explorer/interpreter/action_stack.h b/explorer/interpreter/action_stack.h index 877434cc8404..57a37acb52af 100644 --- a/explorer/interpreter/action_stack.h +++ b/explorer/interpreter/action_stack.h @@ -87,6 +87,9 @@ class ActionStack { auto Spawn(std::unique_ptr child) -> ErrorOr; auto Spawn(std::unique_ptr child, RuntimeScope scope) -> ErrorOr; + // Replace the current action with another action of the same kind and run it + // next. + auto ReplaceWith(std::unique_ptr child) -> ErrorOr; // Start a new recursive action. auto BeginRecursiveAction() { diff --git a/explorer/interpreter/builtins.cpp b/explorer/interpreter/builtins.cpp index 034e2a6a48d7..5f2b0d909db2 100644 --- a/explorer/interpreter/builtins.cpp +++ b/explorer/interpreter/builtins.cpp @@ -15,6 +15,9 @@ void Builtins::Register(Nonnull decl) { if (interface->name() == GetName(Builtin::ImplicitAs)) { builtins_[static_cast(Builtin::ImplicitAs)] = interface; } + if (interface->name() == GetName(Builtin::As)) { + builtins_[static_cast(Builtin::As)] = interface; + } } } diff --git a/explorer/interpreter/builtins.h b/explorer/interpreter/builtins.h index 54d0b943c0b0..235fc7b80475 100644 --- a/explorer/interpreter/builtins.h +++ b/explorer/interpreter/builtins.h @@ -20,9 +20,10 @@ class Builtins { public: explicit Builtins() {} - enum class Builtin { ImplicitAs, Last = ImplicitAs }; + enum class Builtin { ImplicitAs, As, Last = As }; // TODO: In C++20, replace with `using enum Builtin;`. static constexpr Builtin ImplicitAs = Builtin::ImplicitAs; + static constexpr Builtin As = Builtin::As; // Register a declaration that might be a builtin. void Register(Nonnull decl); @@ -38,7 +39,7 @@ class Builtins { private: static constexpr int NumBuiltins = static_cast(Builtin::Last) + 1; - static constexpr const char* BuiltinNames[NumBuiltins] = {"ImplicitAs"}; + static constexpr const char* BuiltinNames[NumBuiltins] = {"ImplicitAs", "As"}; std::optional> builtins_[NumBuiltins] = {}; }; diff --git a/explorer/interpreter/interpreter.cpp b/explorer/interpreter/interpreter.cpp index c89474cdd717..a11330fdfdae 100644 --- a/explorer/interpreter/interpreter.cpp +++ b/explorer/interpreter/interpreter.cpp @@ -208,6 +208,8 @@ auto Interpreter::EvalPrim(Operator op, Nonnull static_type, return arena_->New(cast(*args[0]).address()); case Operator::Combine: return &cast(static_type)->constraint_type(); + case Operator::As: + return Convert(args[0], args[1], source_loc); } } @@ -420,6 +422,9 @@ auto Interpreter::StepLvalue() -> ErrorOr { } case ExpressionKind::PrimitiveOperatorExpression: { const auto& op = cast(exp); + if (auto rewrite = op.rewritten_form()) { + return todo_.ReplaceWith(std::make_unique(*rewrite)); + } if (op.op() != Operator::Deref) { CARBON_FATAL() << "Can't treat primitive operator expression as lvalue: " << exp; @@ -1079,6 +1084,9 @@ auto Interpreter::StepExp() -> ErrorOr { arena_->New(cast(exp).value())); case ExpressionKind::PrimitiveOperatorExpression: { const auto& op = cast(exp); + if (auto rewrite = op.rewritten_form()) { + return todo_.ReplaceWith(std::make_unique(*rewrite)); + } if (act.pos() != static_cast(op.arguments().size())) { // { {v :: op(vs,[],e,es) :: C, E, F} :: S, H} // -> { {e :: op(vs,v,[],es) :: C, E, F} :: S, H} diff --git a/explorer/interpreter/type_checker.cpp b/explorer/interpreter/type_checker.cpp index ebcf4648a5a1..60d6f833e91c 100644 --- a/explorer/interpreter/type_checker.cpp +++ b/explorer/interpreter/type_checker.cpp @@ -2052,6 +2052,26 @@ auto TypeChecker::TypeCheckExp(Nonnull e, op.set_value_category(ValueCategory::Let); return Success(); } + case Operator::As: { + CARBON_ASSIGN_OR_RETURN( + Nonnull type, + InterpExp(op.arguments()[1], arena_, trace_stream_)); + CARBON_RETURN_IF_ERROR( + ExpectIsConcreteType(op.arguments()[1]->source_loc(), type)); + ErrorOr> converted = + BuildBuiltinMethodCall(impl_scope, op.arguments()[0], + BuiltinInterfaceName{Builtins::As, type}, + BuiltinMethodCall{"Convert"}); + if (!converted.ok()) { + // We couldn't find a matching `impl`. + return CompilationError(e->source_loc()) + << "type error in `as`: `" << *ts[0] + << "` is not explicitly convertible to `" << *type << "`:\n" + << converted.error().message(); + } + op.set_rewritten_form(*converted); + return Success(); + } } break; } diff --git a/explorer/syntax/parser.ypp b/explorer/syntax/parser.ypp index 81d3782354ee..2587ec8401cb 100644 --- a/explorer/syntax/parser.ypp +++ b/explorer/syntax/parser.ypp @@ -130,6 +130,8 @@ %type > additive_operand %type > additive_lhs %type > additive_expression +%type > as_operand +%type > as_expression %type > unimpl_expression %type > value_expression %type > comparison_operand @@ -144,6 +146,7 @@ %type > where_clause %type >> where_clause_list %type > where_expression +%type > type_or_where_expression %type > statement_expression %type > if_expression %type > expression @@ -466,6 +469,18 @@ additive_expression: std::vector>({$1, $3})); } ; +as_operand: + minus_expression +| ref_deref_expression +; +as_expression: + as_operand AS as_operand + { + $$ = arena->New( + context.source_loc(), Operator::As, + std::vector>{$1, $3}); + } +; unimpl_expression: // ref_deref_expression excluded due to precedence diamond. ref_deref_expression UNIMPL_EXAMPLE ref_deref_expression @@ -477,6 +492,7 @@ unimpl_expression: value_expression: // ref_deref_expression excluded due to precedence diamond. additive_expression +| as_expression | combine_expression | fn_type_expression | unimpl_expression @@ -560,6 +576,10 @@ where_expression: $$ = arena->New(context.source_loc(), self, $3); } ; +type_or_where_expression: + type_expression +| where_expression +; statement_expression: ref_deref_expression | predicate_expression @@ -998,7 +1018,7 @@ declaration: $$ = arena->New(context.source_loc(), $2, $3, self, $5); } -| impl_kind IMPL impl_deduced_params impl_type AS expression LEFT_CURLY_BRACE impl_body RIGHT_CURLY_BRACE +| impl_kind IMPL impl_deduced_params impl_type AS type_or_where_expression LEFT_CURLY_BRACE impl_body RIGHT_CURLY_BRACE { ErrorOr impl = ImplDeclaration::Create( arena, context.source_loc(), $1, $4, $6, $3, $8); @@ -1021,7 +1041,7 @@ impl_kind: impl_type: // Self { $$ = arena->New(context.source_loc(), "Self"); } -| expression +| type_expression ; declaration_list: // Empty diff --git a/explorer/testdata/as/as_same_type.carbon b/explorer/testdata/as/as_same_type.carbon new file mode 100644 index 000000000000..1da2ed4b38aa --- /dev/null +++ b/explorer/testdata/as/as_same_type.carbon @@ -0,0 +1,17 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// RUN: %{explorer} %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s +// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes %s +// AUTOUPDATE: %{explorer} %s +// CHECK: result: 5 + +package ExplorerTest api; + +fn Main() -> i32 { + let n: i32 = 5; + return n as i32; +} diff --git a/explorer/testdata/as/convert.carbon b/explorer/testdata/as/convert.carbon new file mode 100644 index 000000000000..0e71efeca677 --- /dev/null +++ b/explorer/testdata/as/convert.carbon @@ -0,0 +1,23 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// RUN: %{explorer} %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s +// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes %s +// AUTOUPDATE: %{explorer} %s +// CHECK: result: 5 + +package ExplorerTest api; + +class A { var n: i32; } + +external impl A as As(i32) { + fn Convert[me: Self]() -> i32 { return me.n; } +} + +fn Main() -> i32 { + var a: A = {.n = 5}; + return a as i32; +} diff --git a/explorer/testdata/as/fail_destination_not_type.carbon b/explorer/testdata/as/fail_destination_not_type.carbon new file mode 100644 index 000000000000..45190fdf46b5 --- /dev/null +++ b/explorer/testdata/as/fail_destination_not_type.carbon @@ -0,0 +1,16 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// RUN: %{not} %{explorer} %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s +// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes %s +// AUTOUPDATE: %{explorer} %s + +package ExplorerTest api; + +fn Main() -> i32 { + // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/as/fail_destination_not_type.carbon:[[@LINE+1]]: Expected a type, but got 7 + return 4 as 7; +} diff --git a/explorer/testdata/as/fail_no_conversion.carbon b/explorer/testdata/as/fail_no_conversion.carbon new file mode 100644 index 000000000000..3a4ad3318e5e --- /dev/null +++ b/explorer/testdata/as/fail_no_conversion.carbon @@ -0,0 +1,20 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// RUN: %{not} %{explorer} %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s +// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes %s +// AUTOUPDATE: %{explorer} %s + +package ExplorerTest api; + +class A { var n: i32; } + +fn Main() -> i32 { + var a: A = {.n = 5}; + // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/as/fail_no_conversion.carbon:[[@LINE+2]]: type error in `as`: `class A` is not explicitly convertible to `i32`: + // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/as/fail_no_conversion.carbon:[[@LINE+1]]: could not find implementation of interface As(T = i32) for class A + return a as i32; +} diff --git a/explorer/testdata/as/implicit_as.carbon b/explorer/testdata/as/implicit_as.carbon new file mode 100644 index 000000000000..d2cd3b5aec69 --- /dev/null +++ b/explorer/testdata/as/implicit_as.carbon @@ -0,0 +1,23 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// RUN: %{explorer} %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s +// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \ +// RUN: %{FileCheck} --match-full-lines --allow-unused-prefixes %s +// AUTOUPDATE: %{explorer} %s +// CHECK: result: 5 + +package ExplorerTest api; + +class A { var n: i32; } + +external impl A as ImplicitAs(i32) { + fn Convert[me: Self]() -> i32 { return me.n; } +} + +fn Main() -> i32 { + var a: A = {.n = 5}; + return a as i32; +} diff --git a/explorer/testdata/generic_function/generic_method.carbon b/explorer/testdata/generic_function/generic_method.carbon index 9733f9340321..42e90001cb3e 100644 --- a/explorer/testdata/generic_function/generic_method.carbon +++ b/explorer/testdata/generic_function/generic_method.carbon @@ -30,10 +30,6 @@ class Integer { var int: i32; } -impl Integer as ImplicitAs(Integer) { - fn Convert[me: Self]() -> Integer { return me; } -} - fn Main() -> i32 { var i: Integer = {.int = 1}; var c: Cell(Integer) = Cell(Integer).Create(i); diff --git a/explorer/testdata/impl/generic_method_impl.carbon b/explorer/testdata/impl/generic_method_impl.carbon index 6533f9b67704..01d600509927 100644 --- a/explorer/testdata/impl/generic_method_impl.carbon +++ b/explorer/testdata/impl/generic_method_impl.carbon @@ -33,10 +33,6 @@ class Integer { var int: i32; } -impl Integer as ImplicitAs(Integer) { - fn Convert[me: Self]() -> Integer { return me; } -} - fn Main() -> i32 { var i: Integer = {.int = 1}; var c: Cell(Integer) = Cell(Integer).Create(i); // c contains 1