Add extensibility support for arithmetic operators. (#1751)

Add new prelude interfaces `Negate`, `AddWith`, `SubWith`, and `MulWith`, and
use them to implement non-builtin arithmetic operations.
This commit is contained in:
Richard Smith
2022-07-27 15:09:27 -07:00
committed by GitHub
parent 34a18b5e9e
commit 15ec5113aa
20 changed files with 401 additions and 57 deletions
+41
View File
@@ -49,6 +49,47 @@ impl forall [U1:! Type, U2:! Type, U3:! Type,
}
}
interface Negate {
// TODO: = Self
let Result:! Type;
fn Op[me: Self]() -> Result;
}
interface AddWith(U:! Type) {
// TODO: = Self
let Result:! Type;
fn Op[me: Self](other: U) -> Result;
}
// TODO: constraint Add { ... }
interface SubWith(U:! Type) {
// TODO: = Self
let Result:! Type;
fn Op[me: Self](other: U) -> Result;
}
// TODO: constraint Sub { ... }
interface MulWith(U:! Type) {
// TODO: = Self
let Result:! Type;
fn Op[me: Self](other: U) -> Result;
}
// TODO: constraint Mul { ... }
// Note, these impls use the builtin addition for i32.
external impl i32 as Negate where .Result == i32 {
fn Op[me: i32]() -> i32 { return -me; }
}
external impl i32 as AddWith(i32) where .Result == i32 {
fn Op[me: i32](other: i32) -> i32 { return me + other; }
}
external impl i32 as SubWith(i32) where .Result == i32 {
fn Op[me: i32](other: i32) -> i32 { return me - other; }
}
external impl i32 as MulWith(i32) where .Result == i32 {
fn Op[me: i32](other: i32) -> i32 { return me * other; }
}
// Note that Print is experimental, and not part of an accepted proposal, but
// is included here for printing state in tests.
// TODO: Remove Print special casing once we have variadics or overloads.
+11 -5
View File
@@ -12,11 +12,17 @@ namespace Carbon {
void Builtins::Register(Nonnull<const Declaration*> decl) {
if (auto* interface = dyn_cast<InterfaceDeclaration>(decl)) {
if (interface->name() == GetName(Builtin::ImplicitAs)) {
builtins_[static_cast<int>(Builtin::ImplicitAs)] = interface;
}
if (interface->name() == GetName(Builtin::As)) {
builtins_[static_cast<int>(Builtin::As)] = interface;
static std::map<std::string, int>* builtin_indexes = [] {
std::map<std::string, int> builtin_indexes;
for (int index = 0; index <= static_cast<int>(Builtin::Last); ++index) {
builtin_indexes.emplace(BuiltinNames[index], index);
}
return new auto(std::move(builtin_indexes));
}();
auto it = builtin_indexes->find(interface->name());
if (it != builtin_indexes->end()) {
builtins_[it->second] = interface;
}
}
}
+20 -3
View File
@@ -20,10 +20,26 @@ class Builtins {
public:
explicit Builtins() {}
enum class Builtin { ImplicitAs, As, Last = As };
enum class Builtin {
// Conversions.
As,
ImplicitAs,
// Arithmetic.
Negate,
AddWith,
SubWith,
MulWith,
Last = MulWith
};
// TODO: In C++20, replace with `using enum Builtin;`.
static constexpr Builtin ImplicitAs = Builtin::ImplicitAs;
static constexpr Builtin As = Builtin::As;
static constexpr Builtin ImplicitAs = Builtin::ImplicitAs;
static constexpr Builtin Negate = Builtin::Negate;
static constexpr Builtin AddWith = Builtin::AddWith;
static constexpr Builtin SubWith = Builtin::SubWith;
static constexpr Builtin MulWith = Builtin::MulWith;
// Register a declaration that might be a builtin.
void Register(Nonnull<const Declaration*> decl);
@@ -39,7 +55,8 @@ class Builtins {
private:
static constexpr int NumBuiltins = static_cast<int>(Builtin::Last) + 1;
static constexpr const char* BuiltinNames[NumBuiltins] = {"ImplicitAs", "As"};
static constexpr const char* BuiltinNames[NumBuiltins] = {
"As", "ImplicitAs", "Negate", "AddWith", "SubWith", "MulWith"};
std::optional<Nonnull<const Declaration*>> builtins_[NumBuiltins] = {};
};
+58 -40
View File
@@ -127,14 +127,20 @@ static void SetValue(Nonnull<Pattern*> pattern, Nonnull<const Value*> value) {
}
}
auto TypeChecker::IsSameType(Nonnull<const Value*> type1,
Nonnull<const Value*> type2,
const ImplScope& impl_scope) const -> bool {
SingleStepEqualityContext equality_ctx(this, &impl_scope);
return TypeEqual(type1, type2, &equality_ctx);
}
auto TypeChecker::ExpectExactType(SourceLocation source_loc,
const std::string& context,
Nonnull<const Value*> expected,
Nonnull<const Value*> actual,
const ImplScope& impl_scope) const
-> ErrorOr<Success> {
SingleStepEqualityContext equality_ctx(this, &impl_scope);
if (!TypeEqual(expected, actual, &equality_ctx)) {
if (!IsSameType(expected, actual, impl_scope)) {
return CompilationError(source_loc) << "type error in " << context << "\n"
<< "expected: " << *expected << "\n"
<< "actual: " << *actual;
@@ -380,8 +386,7 @@ auto TypeChecker::IsImplicitlyConvertible(
// conversions.
CARBON_CHECK(IsConcreteType(source));
CARBON_CHECK(IsConcreteType(destination));
SingleStepEqualityContext equality_ctx(this, &impl_scope);
if (TypeEqual(source, destination, &equality_ctx)) {
if (IsSameType(source, destination, impl_scope)) {
return true;
}
@@ -1938,44 +1943,58 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
CARBON_RETURN_IF_ERROR(TypeCheckExp(argument, impl_scope));
ts.push_back(&argument->static_type());
}
auto handle_binary_arithmetic =
[&](Builtins::Builtin builtin) -> ErrorOr<Success> {
// Handle a built-in operator first.
if (isa<IntType>(ts[0]) && isa<IntType>(ts[1]) &&
IsSameType(ts[0], ts[1], impl_scope)) {
op.set_static_type(ts[0]);
op.set_value_category(ValueCategory::Let);
return Success();
}
// Now try an overloaded operator.
ErrorOr<Nonnull<Expression*>> result = BuildBuiltinMethodCall(
impl_scope, op.arguments()[0], BuiltinInterfaceName{builtin, ts[1]},
BuiltinMethodCall{"Op", {op.arguments()[1]}});
if (!result.ok()) {
// We couldn't find a matching `impl`.
return CompilationError(e->source_loc())
<< "type error in `" << ToString(op.op()) << "`:\n"
<< result.error().message();
}
op.set_rewritten_form(*result);
return Success();
};
switch (op.op()) {
case Operator::Neg:
CARBON_RETURN_IF_ERROR(ExpectExactType(e->source_loc(), "negation",
arena_->New<IntType>(), ts[0],
impl_scope));
op.set_static_type(arena_->New<IntType>());
op.set_value_category(ValueCategory::Let);
case Operator::Neg: {
// Handle a built-in negation first.
if (isa<IntType>(ts[0])) {
op.set_static_type(arena_->New<IntType>());
op.set_value_category(ValueCategory::Let);
return Success();
}
// Now try an overloaded negation.
ErrorOr<Nonnull<Expression*>> result = BuildBuiltinMethodCall(
impl_scope, op.arguments()[0],
BuiltinInterfaceName{Builtins::Negate}, BuiltinMethodCall{"Op"});
if (!result.ok()) {
// We couldn't find a matching `impl`.
return CompilationError(e->source_loc())
<< "type error in `" << ToString(op.op()) << "`:\n"
<< result.error().message();
}
op.set_rewritten_form(*result);
return Success();
}
case Operator::Add:
CARBON_RETURN_IF_ERROR(ExpectExactType(e->source_loc(), "addition(1)",
arena_->New<IntType>(), ts[0],
impl_scope));
CARBON_RETURN_IF_ERROR(ExpectExactType(e->source_loc(), "addition(2)",
arena_->New<IntType>(), ts[1],
impl_scope));
op.set_static_type(arena_->New<IntType>());
op.set_value_category(ValueCategory::Let);
return Success();
return handle_binary_arithmetic(Builtins::AddWith);
case Operator::Sub:
CARBON_RETURN_IF_ERROR(
ExpectExactType(e->source_loc(), "subtraction(1)",
arena_->New<IntType>(), ts[0], impl_scope));
CARBON_RETURN_IF_ERROR(
ExpectExactType(e->source_loc(), "subtraction(2)",
arena_->New<IntType>(), ts[1], impl_scope));
op.set_static_type(arena_->New<IntType>());
op.set_value_category(ValueCategory::Let);
return Success();
return handle_binary_arithmetic(Builtins::SubWith);
case Operator::Mul:
CARBON_RETURN_IF_ERROR(
ExpectExactType(e->source_loc(), "multiplication(1)",
arena_->New<IntType>(), ts[0], impl_scope));
CARBON_RETURN_IF_ERROR(
ExpectExactType(e->source_loc(), "multiplication(2)",
arena_->New<IntType>(), ts[1], impl_scope));
op.set_static_type(arena_->New<IntType>());
op.set_value_category(ValueCategory::Let);
return Success();
return handle_binary_arithmetic(Builtins::MulWith);
case Operator::And:
CARBON_RETURN_IF_ERROR(ExpectExactType(e->source_loc(), "&&(1)",
arena_->New<BoolType>(), ts[0],
@@ -2812,9 +2831,8 @@ auto TypeChecker::TypeCheckStmt(Nonnull<Statement*> s,
// TODO: Consider using `ExpectExactType` here.
CARBON_CHECK(IsConcreteType(&return_term.static_type()));
CARBON_CHECK(IsConcreteType(&ret.value_node().static_type()));
SingleStepEqualityContext equality_ctx(this, &impl_scope);
if (!TypeEqual(&return_term.static_type(),
&ret.value_node().static_type(), &equality_ctx)) {
if (!IsSameType(&return_term.static_type(),
&ret.value_node().static_type(), impl_scope)) {
return CompilationError(ret.value_node().base().source_loc())
<< "type of returned var `" << ret.value_node().static_type()
<< "` does not match return type `"
+7
View File
@@ -328,6 +328,13 @@ class TypeChecker {
Nonnull<const Value*> destination)
-> ErrorOr<Nonnull<Expression*>>;
// Determine whether `type1` and `type2` are considered to be the same type
// in the given scope. This is true if they're structurally identical or if
// there is an equality relation in scope that specifies that they are the
// same.
auto IsSameType(Nonnull<const Value*> type1, Nonnull<const Value*> type2,
const ImplScope& impl_scope) const -> bool;
// Check whether `actual` is implicitly convertible to `expected`
// and halt with a fatal compilation error if it is not.
//
+4 -3
View File
@@ -10,9 +10,10 @@
package ExplorerTest api;
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/function/fail_invalid_fnty.carbon:[[@LINE+3]]: type error in negation
// CHECK: expected: i32
// CHECK: actual: Bool
// TODO: We should type-check the prelude before the main input so that we can
// use it within the types of top-level declarations.
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/function/fail_invalid_fnty.carbon:[[@LINE+2]]: type error in `-`:
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/function/fail_invalid_fnty.carbon:[[@LINE+1]]: missing declaration for builtin `Negate`
fn f(g: __Fn(-true) -> true) {
}
+2 -3
View File
@@ -11,9 +11,8 @@
package ExplorerTest api;
fn id[T:! Type](x: T) -> T {
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/generic_function/fail_not_addable.carbon:[[@LINE+3]]: type error in addition(1)
// CHECK: expected: i32
// CHECK: actual: T:! Type
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/generic_function/fail_not_addable.carbon:[[@LINE+2]]: type error in `+`:
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/generic_function/fail_not_addable.carbon:[[@LINE+1]]: could not find implementation of interface AddWith(U = i32) for T:! Type
return x + 0;
}
@@ -11,7 +11,7 @@
package ExplorerTest api;
interface AddWith(T:! Type) {
interface MyAddWith(T:! Type) {
fn Op[me: Self](b: T) -> Self;
}
@@ -21,13 +21,13 @@ class Point {
}
// Allowed: `Self` means `Point` after `as`
impl Point as AddWith(Self) {
impl Point as MyAddWith(Self) {
fn Op[me: Point](b: Point) -> Point {
return {.x = me.x + b.x, .y = me.y + b.y};
}
}
fn DoAddGeneric[T:! Type, U:! AddWith(T)](a: U, b: T) -> U {
fn DoAddGeneric[T:! Type, U:! MyAddWith(T)](a: U, b: T) -> U {
return a.Op(b);
}
+24
View File
@@ -0,0 +1,24 @@
// 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: 6
package ExplorerTest api;
class A { var n: i32; }
external impl A as AddWith(i32) where .Result == A {
fn Op[me: Self](rhs: i32) -> A { return {.n = me.n + rhs}; }
}
fn Main() -> i32 {
var a: A = {.n = 5};
a = a + 1;
return a.n;
}
+19
View File
@@ -0,0 +1,19 @@
// 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: 3
package ExplorerTest api;
// TODO: T:! Add
fn DoAdd[T:! AddWith(.Self) where .Result == .Self](x: T, y: T) -> T { return x + y; }
fn Main() -> i32 {
return DoAdd(1, 2);
}
+21
View File
@@ -0,0 +1,21 @@
// 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 {}
fn Main() -> i32 {
var a: A = {};
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_add.carbon:[[@LINE+2]]: type error in `+`:
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_add.carbon:[[@LINE+1]]: could not find implementation of interface AddWith(U = i32) for class A
a + 1;
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
// 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 {}
fn Main() -> i32 {
var a: A = {};
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_mul.carbon:[[@LINE+2]]: type error in `*`:
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_mul.carbon:[[@LINE+1]]: could not find implementation of interface MulWith(U = i32) for class A
a * 1;
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
// 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 {}
fn Main() -> i32 {
var a: A = {};
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_negate.carbon:[[@LINE+2]]: type error in `-`:
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_negate.carbon:[[@LINE+1]]: could not find implementation of interface Negate for class A
-a;
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
// 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 {}
fn Main() -> i32 {
var a: A = {};
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_sub.carbon:[[@LINE+2]]: type error in `-`:
// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/operators/fail_no_sub.carbon:[[@LINE+1]]: could not find implementation of interface SubWith(U = i32) for class A
a - 1;
return 0;
}
+24
View File
@@ -0,0 +1,24 @@
// 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: 10
package ExplorerTest api;
class A { var n: i32; }
external impl A as MulWith(i32) where .Result == A {
fn Op[me: Self](rhs: i32) -> A { return {.n = me.n * rhs}; }
}
fn Main() -> i32 {
var a: A = {.n = 5};
a = a * 2;
return a.n;
}
+19
View File
@@ -0,0 +1,19 @@
// 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: 6
package ExplorerTest api;
// TODO: T:! Mul
fn DoMul[T:! MulWith(.Self) where .Result == .Self](x: T, y: T) -> T { return x * y; }
fn Main() -> i32 {
return DoMul(2, 3);
}
+24
View File
@@ -0,0 +1,24 @@
// 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 Negate where .Result == A {
fn Op[me: Self]() -> A { return {.n = -me.n}; }
}
fn Main() -> i32 {
var a: A = {.n = 5};
a = -a;
return a.n;
}
+18
View File
@@ -0,0 +1,18 @@
// 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: -3
package ExplorerTest api;
fn DoNegate[T:! Negate where .Result == .Self](x: T) -> T { return -x; }
fn Main() -> i32 {
return DoNegate(3);
}
+24
View File
@@ -0,0 +1,24 @@
// 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: 4
package ExplorerTest api;
class A { var n: i32; }
external impl A as SubWith(i32) where .Result == A {
fn Op[me: Self](rhs: i32) -> A { return {.n = me.n - rhs}; }
}
fn Main() -> i32 {
var a: A = {.n = 5};
a = a - 1;
return a.n;
}
+19
View File
@@ -0,0 +1,19 @@
// 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: 1
package ExplorerTest api;
// TODO: T:! Sub
fn DoSub[T:! SubWith(.Self) where .Result == .Self](x: T, y: T) -> T { return x - y; }
fn Main() -> i32 {
return DoSub(3, 2);
}