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

[Lexer, Parser, Interpreter, Spec] Add extend keyword closes #56 #77

Merged
merged 1 commit into from
Dec 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions spec/interpreter/nodes/extend_spec.cr
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions spec/syntax/lexer_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions spec/syntax/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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 <something>), 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

Expand Down
21 changes: 21 additions & 0 deletions src/myst/interpreter/nodes/extend.cr
Original file line number Diff line number Diff line change
@@ -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
15 changes: 12 additions & 3 deletions src/myst/interpreter/util.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions src/myst/interpreter/value.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down
17 changes: 17 additions & 0 deletions src/myst/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions src/myst/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/myst/syntax/token.cr
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Myst

REQUIRE # require
INCLUDE # include
EXTEND # extend
DEFMODULE # defmodule
DEFTYPE # deftype
DEF # def
Expand Down Expand Up @@ -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,
Expand All @@ -121,6 +122,7 @@ module Myst
{
"require" => REQUIRE,
"include" => INCLUDE,
"extend" => EXTEND,
"defmodule" => DEFMODULE,
"deftype" => DEFTYPE,
"def" => DEF,
Expand Down