From d5e9937c31d07824d6246cfeb46cf713f539a83b Mon Sep 17 00:00:00 2001 From: Jon Egeland Date: Sun, 5 Nov 2017 19:12:24 -0500 Subject: [PATCH 1/2] [parser,interpreter] (#27) Allow operators as method names. The interpreter already treated operators as Calls on the receiving objects. This allows user-written code to define overloads for those operators on their own types. --- spec/interpreter/nodes/call_spec.cr | 50 +++++++++++++++++++++++++ spec/syntax/parser_spec.cr | 10 +++++ src/myst/interpreter/native_lib/list.cr | 5 +++ src/myst/syntax/parser.cr | 38 ++++++++++++++++--- src/myst/syntax/token.cr | 11 +++++- 5 files changed, 107 insertions(+), 7 deletions(-) diff --git a/spec/interpreter/nodes/call_spec.cr b/spec/interpreter/nodes/call_spec.cr index 04e659e..59e357c 100644 --- a/spec/interpreter/nodes/call_spec.cr +++ b/spec/interpreter/nodes/call_spec.cr @@ -66,6 +66,56 @@ describe "Interpreter - Call" do Foo.a? + Foo.b! ), [val(3)] + # Operators can be overloaded by defining a method with the operator as the + # name on an object. + # The match operator overload will have special semantics. These are TBD from + # https://github.com/myst-lang/myst/issues/11. + [ + "+", "-", "*", "/", "%", + "<", "<=", "!=", "==", ">=", ">" + ].each do |op| + it_interprets %Q( + deftype Foo + def a; @a; end + def initialize(a) + @a = a + end + + def #{op}(other : Foo) + %Foo{@a + other.a} + end + end + + f1 = %Foo{1} + f2 = %Foo{2} + f3 = f1 #{op} f2 + f3.a + ), [val(3)] + end + + # Access and access assignment can also be overloaded. + it_interprets %q( + deftype Foo + def initialize + @values = [1, 2, 3] + end + + def [](idx : Integer) + @values[idx] + end + + def []=(idx : Integer, value) + @values[idx] = value + end + + def values; @values; end + end + + f1 = %Foo{} + f1[2] = 5 + f1.values + ), [val([1, 2, 5])] + # When looking up a function, the current lexical scope should be checked for # overrides. However, parent lexical scopes should be ignored. diff --git a/spec/syntax/parser_spec.cr b/spec/syntax/parser_spec.cr index 60b9551..b37f4db 100644 --- a/spec/syntax/parser_spec.cr +++ b/spec/syntax/parser_spec.cr @@ -844,6 +844,16 @@ describe "Parser" do end ), Def.new("foo", [p("list", l([1, u("_")]), restriction: c("List")), p(nil, l(nil)), p("b", restriction: c("Integer"))]) + # Some operators are also allowed as method names for overloading. + [ + "+", "-", "*", "/", "%", "[]", "[]=", + "<", "<=", "!=", "==", ">=", ">" + ].each do |op| + it_parses %Q(def #{op}; end), Def.new(op) + it_parses %Q(def #{op}(); end), Def.new(op) + it_parses %Q(def #{op}(other); end), Def.new(op, [p("other")]) + it_parses %Q(def #{op}(a, b); end), Def.new(op, [p("a"), p("b")]) + end # Module definitions diff --git a/src/myst/interpreter/native_lib/list.cr b/src/myst/interpreter/native_lib/list.cr index 7dfebcd..11edc95 100644 --- a/src/myst/interpreter/native_lib/list.cr +++ b/src/myst/interpreter/native_lib/list.cr @@ -18,6 +18,10 @@ module Myst this.elements[index.value] end + NativeLib.method :list_access_assign, TList, index : TInteger, value : Value do + this.elements[index.value] = value + end + def init_list(root_scope : Scope) list_type = TType.new("List", root_scope) @@ -26,6 +30,7 @@ module Myst NativeLib.def_instance_method(list_type, :each, :list_each) NativeLib.def_instance_method(list_type, :+, :list_add) NativeLib.def_instance_method(list_type, :[], :list_access) + NativeLib.def_instance_method(list_type, :[]=, :list_access_assign) list_type end diff --git a/src/myst/syntax/parser.cr b/src/myst/syntax/parser.cr index f48c1ba..3607e96 100644 --- a/src/myst/syntax/parser.cr +++ b/src/myst/syntax/parser.cr @@ -136,12 +136,7 @@ module Myst start = expect(Token::Type::DEF, Token::Type::DEFSTATIC) static = (start.type == Token::Type::DEFSTATIC) skip_space - name = expect(Token::Type::IDENT).value - # If the name is unmodified, it can be followed by an `=` to create an - # assignment method. - if !modified_ident?(name) && accept(Token::Type::EQUAL) - name += "=" - end + name = parse_def_name method_def = Def.new(name, static: static).at(start.location) push_var_scope @@ -203,6 +198,37 @@ module Myst end end + def parse_def_name + case (token = current_token).type + when Token::Type::IDENT + expect(Token::Type::IDENT) + name = token.value + # If the name is unmodified, it can be followed by an `=` to create an + # assignment method. + if !modified_ident?(name) && accept(Token::Type::EQUAL) + name += "=" + end + name + when Token::Type::LBRACE + # An access overload is written as `[]`, so an RBRACE must also be + # given for the method name to be valid. + expect(Token::Type::LBRACE) + expect(Token::Type::RBRACE) + # An `=` can also be given to specify access assignment + if accept(Token::Type::EQUAL) + return "[]=" + else + return "[]" + end + when .overloadable_operator? + read_token + # Any overloadable operator is also allowed + token.value + else + raise ParseError.new("Invalid name for def: #{token.value}") + end + end + def parse_param(allow_splat=true) param = Param.new diff --git a/src/myst/syntax/token.cr b/src/myst/syntax/token.cr index f429dde..9ab9587 100644 --- a/src/myst/syntax/token.cr +++ b/src/myst/syntax/token.cr @@ -167,10 +167,15 @@ module Myst end def self.binary_operators - [ PLUS, MINUS, STAR, SLASH, EQUAL, MATCH, LESS, LESSEQUAL, + [ PLUS, MINUS, STAR, SLASH, MODULO, EQUAL, MATCH, LESS, LESSEQUAL, GREATEREQUAL, GREATER, NOTEQUAL, EQUALEQUAL, ANDAND, OROR] end + def self.overloadable_operators + [ PLUS, MINUS, STAR, SLASH, MODULO, MATCH, LESS, LESSEQUAL, + NOTEQUAL, EQUALEQUAL, GREATEREQUAL, GREATER] + end + def unary_operator? self.class.unary_operators.includes?(self) end @@ -179,6 +184,10 @@ module Myst self.class.binary_operators.includes?(self) end + def overloadable_operator? + self.class.overloadable_operators.includes?(self) + end + def operator? unary_operator? || binary_operator? end From 2744cee7f6985f025e3ca8361654cb1331dc2089 Mon Sep 17 00:00:00 2001 From: Jon Egeland Date: Sun, 5 Nov 2017 19:17:05 -0500 Subject: [PATCH 2/2] [spec] (#27) Assert that operator overloads work for Types and Modules. Operator overloading can also be used with `defstatic` or on a module to define operators for things other than instances. This could potentially be used for some type algebra tricks, though it doesn't seem overly useful outside of metaprogramming. --- spec/interpreter/nodes/call_spec.cr | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/interpreter/nodes/call_spec.cr b/spec/interpreter/nodes/call_spec.cr index 59e357c..9f0dc75 100644 --- a/spec/interpreter/nodes/call_spec.cr +++ b/spec/interpreter/nodes/call_spec.cr @@ -91,6 +91,30 @@ describe "Interpreter - Call" do f3 = f1 #{op} f2 f3.a ), [val(3)] + + # Operators can also be defined statically to do some type algebra. + it_interprets %Q( + deftype Foo + defstatic #{op}(other : Foo) + :called_op_on_type + end + end + + Foo #{op} Foo + ), [val(:called_op_on_type)] + + # Or on modules, for whatever that might be worth... (I guess this could + # define operators in a module that could be included? Not sure why it + # would be done outside of an `include`, though). + it_interprets %Q( + defmodule Foo + def #{op}(other) + :called_op_on_module + end + end + + Foo #{op} Foo + ), [val(:called_op_on_module)] end # Access and access assignment can also be overloaded.