Skip to content

Commit

Permalink
Add syntax support for static methods
Browse files Browse the repository at this point in the history
This allows one to define a static method using the "static" keyword
like so:

    object Something {
      static def new -> Something {
        ...
      }
    }

While Inko already has module methods, static methods are useful when
acting as factory methods: methods that just return an instance of the
type they are defined on. For a while I wanted to use module methods for
this, but this can lead to unpleasant APIs. For example, the std::time
module offers a few module methods like so:

    import std::time

    time.now
    time.utc

I'm not a fan of this setup, as it means having to import both the
module (std::time) and the appropriate types (e.g. SystemTime) in
certain cases. Static methods allow you to write the following instead:

    import std::time::SystemTime

    SystemTime.now
    SystemTime.utc

The compiler currently does not perform any validation to make sure
static methods are called in the right context. This is the result of
the Ruby compiler being too much of a hack to implement this properly.
Instead, we only add syntax support for now and will add validation for
this in the self hosting compiler.
  • Loading branch information
Yorick Peterse committed Jul 22, 2019
1 parent 48dfebe commit f86eb0b
Show file tree
Hide file tree
Showing 15 changed files with 84 additions and 65 deletions.
3 changes: 3 additions & 0 deletions compiler/lib/inkoc/ast/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class Method
attr_reader :name, :arguments, :type_parameters, :returns, :throws,
:method_bounds, :body, :location

attr_accessor :static

# name - The name of the method.
# args - The arguments of the method.
# returns - The return type of the method.
Expand Down Expand Up @@ -39,6 +41,7 @@ def initialize(
@location = loc
@required = required
@type = nil
@static = false
end

def required?
Expand Down
3 changes: 2 additions & 1 deletion compiler/lib/inkoc/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class Lexer
'impl' => :impl,
'for' => :for,
'lambda' => :lambda,
'where' => :where
'where' => :where,
'static' => :static
}.freeze

SPECIALS = Set.new(
Expand Down
10 changes: 10 additions & 0 deletions compiler/lib/inkoc/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ def value(start)
when :bracket_open then array(start)
when :hash_open then hash(start)
when :define then def_method(start)
when :static then def_static_method(start)
when :do, :lambda then block(start, start.type)
when :let then let_define(start)
when :return then return_value(start)
Expand Down Expand Up @@ -840,6 +841,14 @@ def def_method(start)
)
end

def def_static_method(start)
def_start = advance_and_expect!(:define)

def_method(def_start).tap do |method|
method.static = true
end
end

# Parses a list of argument definitions.
# rubocop: disable Metrics/CyclomaticComplexity
def def_arguments
Expand Down Expand Up @@ -1093,6 +1102,7 @@ def object_body(start)
node =
case token.type
when :define then def_method(token)
when :static then def_static_method(token)
when :attribute then def_attribute(token)
when :documentation then documentation(token)
else
Expand Down
2 changes: 2 additions & 0 deletions compiler/lib/inkoc/pass/desugar_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def on_object(node)
default_new(node.location)
end

method.static = true

node.body.expressions.push(method)
process_node(node.body)
end
Expand Down
4 changes: 2 additions & 2 deletions runtime/src/core/bootstrap.inko
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ _INKOC.set_object_name(Block, 'Block')
# building blocks of Inko, such as "Object.new" and the bits necessary to allow
# creating of modules.
impl Object {
def new -> Self {
static def new -> Self {
let obj = _INKOC.set_object(False, self)

obj.init
Expand Down Expand Up @@ -71,7 +71,7 @@ object Module {
## The file path of the module.
@path: String

def new(name: String, path: String) -> Self {
static def new(name: String, path: String) -> Self {
# Modules are always global objects, so we create them as such right away.
let obj = _INKOC.set_object(True, self)

Expand Down
2 changes: 1 addition & 1 deletion runtime/src/std/array.inko
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl Array!(T) {
## Creating an Array with values:
##
## Array.new(10, 20, 30) # => [10, 20, 30]
def new!(V)(*values: V) -> Array!(V) {
static def new!(V)(*values: V) -> Array!(V) {
values
}

Expand Down
2 changes: 1 addition & 1 deletion runtime/src/std/byte_array.inko
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl ByteArray {
## import std::byte_array::ByteArray
##
## ByteArray.new([10, 20, 30])
def new(bytes: Array!(Integer) = []) -> Self {
static def new(bytes: Array!(Integer) = []) -> Self {
_INKOC.byte_array_from_array(bytes) as ByteArray
}

Expand Down
99 changes: 49 additions & 50 deletions runtime/src/std/compiler/keywords.inko
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,58 @@ import std::byte_array::ByteArray

## The associated values for the first and second bytes in a keyword.
let ASSOCIATED_VALUES = [
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 5, 11, 20,
13, 0, 13, 20, 6, 2, 20, 20, 0, 2,
20, 2, 20, 20, 0, 5, 2, 10, 20, 3,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 0, 0, 22,
0, 0, 0, 22, 7, 4, 22, 22, 6, 3,
22, 15, 22, 22, 0, 0, 2, 10, 22, 3,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22
]

## All available keywords, as ByteArray instances.
##
## The first three empty values are necessary and produced by gperf. Removing
## these may produce unexpected results.
let WORD_LIST = [
''.to_byte_array,
''.to_byte_array,
''.to_byte_array,
'let'.to_byte_array,
'else'.to_byte_array,
'try'.to_byte_array,
'return'.to_byte_array,
'trait'.to_byte_array,
'impl'.to_byte_array,
'self'.to_byte_array,
'import'.to_byte_array,
'lambda'.to_byte_array,
'as'.to_byte_array,
'throw'.to_byte_array,
'where'.to_byte_array,
'mut'.to_byte_array,
'def'.to_byte_array,
'do'.to_byte_array,
'for'.to_byte_array,
'object'.to_byte_array
''.to_byte_array,
''.to_byte_array,
'as'.to_byte_array,
'def'.to_byte_array,
'self'.to_byte_array,
'try'.to_byte_array,
'return'.to_byte_array,
'trait'.to_byte_array,
'static'.to_byte_array,
'let'.to_byte_array,
'else'.to_byte_array,
'impl'.to_byte_array,
'lambda'.to_byte_array,
'import'.to_byte_array,
'throw'.to_byte_array,
'where'.to_byte_array,
'mut'.to_byte_array,
'do'.to_byte_array,
'for'.to_byte_array,
''.to_byte_array,
''.to_byte_array,
'object'.to_byte_array
]

## Input values with a length outside of this `Range` definitely can not be a
Expand All @@ -71,7 +70,7 @@ let WORD_LENGTH = 2..6

## The maximum possible hash value. If a hash value is greater, the input is
## definitely not a keyword.
let MAX_HASH_VALUE = 19
let MAX_HASH_VALUE = 21

## Returns `True` if the given `ByteArray` is a valid Inko keyword.
def keyword?(bytes: ByteArray) -> Boolean {
Expand Down
6 changes: 3 additions & 3 deletions runtime/src/std/ffi.inko
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ let NULL_ADDRESS = 0x0
## A pointer to a value in C.
object Pointer {
## Creates a pointer using the given memory address.
def new(address: Integer) -> Self {
static def new(address: Integer) -> Self {
_INKOC.pointer_from_address(Pointer, address)
}

Expand Down Expand Up @@ -170,7 +170,7 @@ object Library {
##
## The names in this `Array` can either be the library name with extension,
## such as `'libc.so.6'`, or absolute paths such as `'/usr/lib/libc.so.6'`.
def new(names: Array!(String)) -> Self {
static def new(names: Array!(String)) -> Self {
_INKOC.library_open(Library, names)
}

Expand Down Expand Up @@ -199,7 +199,7 @@ object Function {
@returns: Type

## Attaches a C function to Inko.
def new(
static def new(
library: Library,
name: String,
arguments: Array!(Type),
Expand Down
6 changes: 3 additions & 3 deletions runtime/src/std/fs/file.inko
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ object ReadOnlyFile {
## import std::fs::file::ReadOnlyFile
##
## let handle = try! ReadOnlyFile.new('/dev/null')
def new(path: ToPath) !! IOError -> Self {
static def new(path: ToPath) !! IOError -> Self {
let conv_path = path.to_path
let instance =
try raw.open(type: self, path: conv_path.to_string, mode: READ_ONLY)
Expand Down Expand Up @@ -99,7 +99,7 @@ object WriteOnlyFile {
## import std::fs::file::WriteOnlyFile
##
## let handle = try! WriteOnlyFile.new('/dev/null')
def new(path: ToPath, append = False) !! IOError -> Self {
static def new(path: ToPath, append = False) !! IOError -> Self {
let mode = raw.mode_for_write(append)
let conv_path = path.to_path
let instance =
Expand Down Expand Up @@ -172,7 +172,7 @@ object ReadWriteFile {
## import std::fs::file::ReadWriteFile
##
## let handle = try! ReadWriteFile.new('/dev/null')
def new(path: ToPath, append = False) !! IOError -> Self {
static def new(path: ToPath, append = False) !! IOError -> Self {
let mode = raw.mode_for_read_write(append)
let conv_path = path.to_path
let instance =
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/std/hash_map.inko
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ object DefaultHasher {
## # Panics
##
## This method will panic if any of the provided keys are below zero.
def new(key0: Integer, key1: Integer) -> Self {
static def new(key0: Integer, key1: Integer) -> Self {
_INKOC.hasher_new(DefaultHasher, key0, key1)
}
}
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/std/net/socket.inko
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ object Socket {
## import std::net::socket::(IPV4, DGRAM, Socket)
##
## try! Socket.new(domain: IPV4, kind: DGRAM)
def new(domain: Integer, kind: Integer) !! IoError -> Self {
static def new(domain: Integer, kind: Integer) !! IoError -> Self {
try {
_INKOC.socket_create(Socket, domain, kind)
} else (error) {
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/std/net/unix.inko
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ object Socket {
## import std::net::unix::(DGRAM, Socket)
##
## try! Socket.new(DGRAM)
def new(kind: Integer) !! IoError -> Socket {
static def new(kind: Integer) !! IoError -> Socket {
try {
_INKOC.socket_create(Socket, UNIX, kind)
} else (error) {
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/std/nil.inko
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Nil {
## Sending `new` to a `Nil` produces a new `Nil`:
##
## Nil.new # => Nil
def new -> Self {
static def new -> Self {
self
}
}
Expand Down
4 changes: 4 additions & 0 deletions runtime/tests/test/std/compiler/test_keywords.inko
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ test.group('std::compiler::keywords.keyword?') do (g) {
assert.true(keywords.keyword?('object'.to_byte_array))
}

g.test('Checking if "static" is a keyword') {
assert.true(keywords.keyword?('static'.to_byte_array))
}

g.test('Checking if an invalid identifier is a keyword') {
assert.false(keywords.keyword?('foo'.to_byte_array))
assert.false(keywords.keyword?('bar'.to_byte_array))
Expand Down

0 comments on commit f86eb0b

Please sign in to comment.