diff --git a/spec/interpreter/nodes/extend_spec.cr b/spec/interpreter/nodes/extend_spec.cr new file mode 100644 index 0000000..b96368c --- /dev/null +++ b/spec/interpreter/nodes/extend_spec.cr @@ -0,0 +1,76 @@ +require "../../spec_helper.cr" +require "../../support/nodes.cr" +require "../../support/interpret.cr" + +EXT_MODULE_DEF = %q( + defmodule Foo + def foo + :extended + end + + def call_bar + bar + end + end +) + +TYPE_DEF = %q( + deftype Baz + extend Foo + + def bar + "bar called" + end + + end +) + +describe "Interpreter - Extend" do + # Modules may add static properties and methods to Types. + it_interprets EXT_MODULE_DEF + TYPE_DEF + %q( + Baz.foo + ), [val(:extended)] + + it_does_not_interpret %q(extend nil), /non-module/ + it_does_not_interpret %q(extend true), /non-module/ + it_does_not_interpret %q(extend false), /non-module/ + it_does_not_interpret %q(extend "hello"), /non-module/ + it_does_not_interpret %q(extend :hi), /non-module/ + it_does_not_interpret %q(extend [1, 2]), /non-module/ + it_does_not_interpret %q(extend {a: 1}), /non-module/ + + it "extends all ancestors of the extended module" do + itr = parse_and_interpret EXT_MODULE_DEF + %q( + defmodule JuniorFoo + include Foo + end + + defmodule FooTheThird + include JuniorFoo + end + + deftype Thing + extend FooTheThird + end + + Thing.foo + ) + + + itr.stack.pop.should eq(val(:extended)) + end + + it "does not add properties or methods to type instances" do + itr = interpret_with_mocked_output EXT_MODULE_DEF + %q( + deftype Bar + extend Foo + end + + %Bar{}.foo + ) + + itr.errput.to_s.downcase.should match(/no variable or method `foo`/) + end + + it_does_not_interpret EXT_MODULE_DEF + %q(defmodule Baz; extend Foo; end), /non-type/ +end diff --git a/spec/syntax/lexer_spec.cr b/spec/syntax/lexer_spec.cr index 9d54722..8328008 100644 --- a/spec/syntax/lexer_spec.cr +++ b/spec/syntax/lexer_spec.cr @@ -46,6 +46,7 @@ STATIC_TOKENS = { Token::Type::WHITESPACE => " ", Token::Type::REQUIRE => "require", Token::Type::INCLUDE => "include", + Token::Type::EXTEND => "extend", Token::Type::DEFMODULE => "defmodule", Token::Type::DEFTYPE => "deftype", Token::Type::DEFSTATIC => "defstatic", diff --git a/spec/syntax/parser_spec.cr b/spec/syntax/parser_spec.cr index d4554b7..b7c66b2 100644 --- a/spec/syntax/parser_spec.cr +++ b/spec/syntax/parser_spec.cr @@ -1205,7 +1205,34 @@ describe "Parser" do include Thing1, Thing2 ) + # Extend + # Extends accept any node as an argument, and are valid in any context. + it_parses %q(extend Thing), Extend.new(c("Thing")) + it_parses %q(extend Thing.Other), Extend.new(Call.new(c("Thing"), "Other")) + it_parses %q(extend dynamic), Extend.new(Call.new(nil, "dynamic")) + it_parses %q(extend 1 + 2), Extend.new(Call.new(l(1), "+", [l(2)], infix: true)) + it_parses %q(extend self), Extend.new(Self.new) + it_parses %q(extend ), Extend.new(i(Call.new(nil, "something"))) + it_parses %q( + deftype Thing + extend Other + end + ), TypeDef.new("Thing", e(Extend.new(c("Other")))) + # The argument for an extend must be on the same line as the keyword. + it_does_not_parse %q( + extend + Thing + ), /expected value for extend/ + # The argument is still allowed to span multiple lines + it_parses %q( + extend 1 + + 2 + ), Extend.new(Call.new(l(1), "+", [l(2)], infix: true)) + # Only one value is expected. Providing multiple values is invalid. + it_does_not_parse %q( + extend Thing1, Thing2 + ) # Require diff --git a/src/myst/interpreter/nodes/extend.cr b/src/myst/interpreter/nodes/extend.cr new file mode 100644 index 0000000..1538e4c --- /dev/null +++ b/src/myst/interpreter/nodes/extend.cr @@ -0,0 +1,21 @@ +module Myst + class Interpreter + def visit(node : Extend) + visit(node.path) + _module = stack.pop + unless _module.is_a?(TModule) + raise "Cannot extend non-module. Got #{_module}" + end + + slf = current_self + if slf.is_a?(TType) + slf.extend_module(_module) + else + raise "Cannot extend from non-type." + end + + # The result of an Extend is the module that was included. + stack.push(_module) + end + end +end diff --git a/src/myst/interpreter/util.cr b/src/myst/interpreter/util.cr index 258c3d5..5f8141b 100644 --- a/src/myst/interpreter/util.cr +++ b/src/myst/interpreter/util.cr @@ -63,12 +63,21 @@ module Myst def recursive_lookup(receiver, name) func = current_scope[name] if current_scope.has_key?(name) func ||= __scopeof(receiver)[name]? - func ||= __typeof(receiver).ancestors.each do |anc| - if found = __scopeof(anc)[name]? - break found + if receiver.is_a?(TType) + func ||= receiver.extended_ancestors.each do |anc| + if found = __scopeof(anc)[name]? + break found + end + end + else + func ||= __typeof(receiver).ancestors.each do |anc| + if found = __scopeof(anc)[name]? + break found + end end end + func end diff --git a/src/myst/interpreter/value.cr b/src/myst/interpreter/value.cr index b0d3b61..fe47d11 100644 --- a/src/myst/interpreter/value.cr +++ b/src/myst/interpreter/value.cr @@ -73,6 +73,7 @@ module Myst class TType < ContainerType property scope : Scope property instance_scope : Scope + property extended_modules = [] of TModule def initialize(@name : String, parent : Scope?=nil) @scope = Scope.new(parent) @@ -91,6 +92,17 @@ module Myst "Type" end + def extend_module(mod : TModule) + @extended_modules.unshift(mod) + end + + def extended_ancestors + @extended_modules.reduce(Set(TModule).new) do |acc, mod| + acc.add(mod) + acc.concat(mod.ancestors) + end.to_a + end + def_equals_and_hash name, scope, instance_scope end diff --git a/src/myst/syntax/ast.cr b/src/myst/syntax/ast.cr index f3917b8..cfb7ea3 100644 --- a/src/myst/syntax/ast.cr +++ b/src/myst/syntax/ast.cr @@ -821,6 +821,23 @@ module Myst def_equals_and_hash path end + # An extend expression. Extends allow Type instances to inherit + # static methods and properties from Modules. When an Extend is + # encountered, the module referenced by the path must already exist. + # + # 'extend' path + class Extend < Node + property path : Node + + def initialize(@path : Node); end + + def accept_children(visitor) + path.accept(visitor) + end + + def_equals_and_hash path + end + # Any flow control expression. These represent expressions that usurp the # normal flow of execution. A flow control expression may optionally carry # a value to be returned at the destination. diff --git a/src/myst/syntax/parser.cr b/src/myst/syntax/parser.cr index 284796f..28377fc 100644 --- a/src/myst/syntax/parser.cr +++ b/src/myst/syntax/parser.cr @@ -121,6 +121,8 @@ module Myst parse_anonymous_function when Token::Type::INCLUDE parse_include + when Token::Type::EXTEND + parse_extend when Token::Type::REQUIRE parse_require when Token::Type::RETURN, Token::Type::BREAK, Token::Type::NEXT, Token::Type::RAISE @@ -393,6 +395,16 @@ module Myst return Include.new(path).at(start.location).at_end(path) end + def parse_extend + start = expect(Token::Type::EXTEND) + skip_space + if current_token.type == Token::Type::NEWLINE + raise ParseError.new(current_location, "expected value for extend") + end + path = parse_expression + return Extend.new(path).at(start.location).at_end(path) + end + def parse_require start = expect(Token::Type::REQUIRE) skip_space diff --git a/src/myst/syntax/token.cr b/src/myst/syntax/token.cr index 569baaa..f006072 100644 --- a/src/myst/syntax/token.cr +++ b/src/myst/syntax/token.cr @@ -15,6 +15,7 @@ module Myst REQUIRE # require INCLUDE # include + EXTEND # extend DEFMODULE # defmodule DEFTYPE # deftype DEF # def @@ -105,7 +106,7 @@ module Myst end def self.keywords - [ REQUIRE, INCLUDE, + [ REQUIRE, INCLUDE, EXTEND, DEFMODULE, DEFTYPE, DEF, DEFSTATIC, FN, DO, END, WHEN, UNLESS, ELSE, WHILE, UNTIL, @@ -121,6 +122,7 @@ module Myst { "require" => REQUIRE, "include" => INCLUDE, + "extend" => EXTEND, "defmodule" => DEFMODULE, "deftype" => DEFTYPE, "def" => DEF,