Move all "class" methods to module methods

In the past I have gone back and forth a bit on the idea of using
class/static methods or not. With this commit I'm making a final
decision on this topic: Inko will not have class/static methods, at
least not until 1.0. Instead, Inko will use module methods. This means
that the following methods have been changed:

* HashMap.from_array  -> std::hash_map.from_array
* Trait.implement_for -> std::trait.implement
* Integer.from_string -> std::integer.parse
* Float.from_string   -> std::float.parse

To make this happen some compiler changes had to be made to optimise
`hash_map.from_array` and to use `trait.implement` instead of
`Trait.implement_for`.
parent 26f0023c
Pipeline #40700571 failed with stages
in 7 minutes and 48 seconds
......@@ -51,7 +51,7 @@ module Inkoc
def hash_map_literal?
receiver&.global? &&
receiver&.name == Config::HASH_MAP_CONST &&
receiver&.name == Config::HASH_MAP_LITERAL_RECEIVER &&
name == Config::FROM_ARRAY_MESSAGE &&
arguments[0]&.array_literal? &&
arguments[1]&.array_literal?
......
......@@ -13,6 +13,9 @@ module Inkoc
# The file extension of source files.
SOURCE_EXT = '.inko'
# The name of the root module for the standard library.
STD_MODULE = 'std'
# The name of the root module for the core library.
CORE_MODULE = 'core'
......@@ -26,6 +29,11 @@ module Inkoc
# module.
GLOBALS_MODULE = 'globals'
TRAIT_MODULE = 'trait'
INTERNAL_TRAIT_IMPORT = '_inkoc_std_trait'
HASH_MAP_LITERAL_RECEIVER = '_inkoc_hash_map_literal'
MARKER_MODULE = 'std::marker'
HASH_MAP_MODULE = 'std::hash_map'
......@@ -79,7 +87,7 @@ module Inkoc
TRY_BLOCK_NAME = '<try>'
ELSE_BLOCK_NAME = '<else>'
IMPL_NAME = '<impl>'
IMPLEMENT_TRAIT_MESSAGE = 'implement_for'
IMPLEMENT_TRAIT_MESSAGE = 'implement'
OBJECT_NAME_INSTANCE_ATTRIBUTE = '@_object_name'
INIT_MESSAGE = 'init'
......
# frozen_string_literal: true
module Inkoc
# rubocop: disable Metrics/ClassLength
class Parser
ParseError = Class.new(StandardError)
......@@ -118,8 +119,11 @@ module Inkoc
]
).freeze
attr_reader :trait_implementation
def initialize(input, file_path = Pathname.new('(eval)'))
@lexer = Lexer.new(input, file_path)
@trait_implementation = false
end
def location
......@@ -793,7 +797,7 @@ module Inkoc
end
location = start.location
receiver = AST::Global.new(Config::HASH_MAP_CONST, location)
receiver = AST::Global.new(Config::HASH_MAP_LITERAL_RECEIVER, location)
keys_array = new_array(keys, start)
vals_array = new_array(vals, start)
......@@ -1100,6 +1104,8 @@ module Inkoc
end
end
@trait_implementation = true unless impls.empty?
impls
end
......@@ -1173,6 +1179,8 @@ module Inkoc
if @lexer.next_type_is?(:for)
advance_and_expect!(:for)
@trait_implementation = true
object_name = type_name(advance_and_expect!(:constant))
body = block_body(advance_and_expect!(:curly_open))
......@@ -1558,4 +1566,5 @@ module Inkoc
AST::Send.new('new', receiver, [], values, start.location)
end
end
# rubocop: enable Metrics/ClassLength
end
......@@ -461,6 +461,8 @@ module Inkoc
def receiver_type_for_send_with_receiver(node, scope)
if node.name == Config::NEW_MESSAGE
define_type_instance(node.receiver, scope)
elsif node.hash_map_literal?
@state.module(Config::HASH_MAP_MODULE).type
else
define_type(node.receiver, scope)
end
......
......@@ -482,12 +482,13 @@ module Inkoc
def implement_trait(object, trait, body, location)
message = Config::IMPLEMENT_TRAIT_MESSAGE
method = trait.type.lookup_method(message).type
trait_mod = get_global(Config::INTERNAL_TRAIT_IMPORT, body, location)
method = trait_mod.type.lookup_method(message).type
send_object_message(
trait,
trait_mod,
message,
[object],
[object, trait],
[],
method,
method.return_type,
......@@ -516,12 +517,6 @@ module Inkoc
object
end
def trait_builtin(body, location)
top = get_toplevel(body, location)
get_attribute(top, Config::TRAIT_CONST, body, location)
end
def set_object_literal_name(object, name, body, location)
attr = Config::OBJECT_NAME_INSTANCE_ATTRIBUTE
name_reg = set_string(name, body, location)
......@@ -539,14 +534,13 @@ module Inkoc
end
def on_send(node, body)
receiver = receiver_for_send(node, body)
# HashMap literals need to be optimised before we process their
# arguments.
if node.hash_map_literal?
return on_hash_map_literal(receiver, node, body)
return on_hash_map_literal(node, body)
end
receiver = receiver_for_send(node, body)
args, kwargs = split_send_arguments(node.arguments, body)
send_object_message(
......@@ -578,7 +572,10 @@ module Inkoc
#
# While the example above uses a local variable `hash_map`, the generated
# code only uses registers.
def on_hash_map_literal(hash_map_global_reg, node, body)
def on_hash_map_literal(node, body)
hash_map_global_reg =
get_global(Config::HASH_MAP_CONST, body, node.location)
hash_map_type = hash_map_global_reg.type
new_method = hash_map_type.lookup_method(Config::NEW_MESSAGE).type
set_method = hash_map_type.lookup_method(Config::SET_INDEX_MESSAGE).type
......
......@@ -20,6 +20,7 @@ module Inkoc
@module.imports << import_bootstrap(loc) if @module.import_bootstrap?
@module.imports << import_globals(loc) if @module.import_globals?
@module.imports << import_prelude(loc) if @module.import_prelude?
@module.imports << import_trait(loc) if @module.import_trait_module?
end
# Generates the import statement for importing the bootstrap module.
......@@ -55,22 +56,41 @@ module Inkoc
#
# import core::bootstrap::(self as _)
def import_and_ignore(name, location)
std = identifier_for(Config::CORE_MODULE, location)
core = identifier_for(Config::CORE_MODULE, location)
bootstrap = identifier_for(name, location)
underscore = identifier_for('_', location)
symbol = AST::ImportSymbol
.new(AST::Self.new(location), underscore, location)
AST::Import.new([std, bootstrap], [symbol], location)
AST::Import.new([core, bootstrap], [symbol], location)
end
def import_everything_from(module_name, location)
std = identifier_for(Config::CORE_MODULE, location)
core = identifier_for(Config::CORE_MODULE, location)
prelude = identifier_for(module_name, location)
symbol = AST::GlobImport.new(location)
AST::Import.new([std, prelude], [symbol], location)
AST::Import.new([core, prelude], [symbol], location)
end
def import_trait(location)
import_std_module_as(
Config::TRAIT_MODULE,
Config::INTERNAL_TRAIT_IMPORT,
location
)
end
def import_std_module_as(name, symbol_name, location)
std = identifier_for(Config::STD_MODULE, location)
name_ident = identifier_for(name, location)
alias_name = identifier_for(symbol_name, location)
symbol = AST::ImportSymbol
.new(AST::Self.new(location), alias_name, location)
AST::Import.new([std, name_ident], [symbol], location)
end
end
end
......
......@@ -11,12 +11,17 @@ module Inkoc
def run(source)
parser = Parser.new(source, @module.file_path_as_string)
begin
[parser.parse]
rescue Parser::ParseError => error
@state.diagnostics.error(error.message, parser.location)
nil
end
ast =
begin
parser.parse
rescue Parser::ParseError => error
@state.diagnostics.error(error.message, parser.location)
return
end
@module.config[:import_trait_module] = parser.trait_implementation
[ast]
end
end
end
......
......@@ -75,6 +75,10 @@ module Inkoc
config.define_module?
end
def import_trait_module?
config.import_trait_module?
end
def source_code
location.file.path.read
end
......
......@@ -3,21 +3,20 @@
module Inkoc
module TIR
class ModuleConfig
VALID_KEYS = Set.new(
%w[import_prelude import_bootstrap import_globals define_module]
).freeze
DEFAULTS = {
import_prelude: true,
import_bootstrap: true,
import_globals: true,
define_module: true,
import_trait_module: false,
}.freeze
def initialize
@options = {
import_prelude: true,
import_bootstrap: true,
import_globals: true,
define_module: true
}
@options = DEFAULTS.dup
end
def valid_key?(key)
VALID_KEYS.include?(key)
DEFAULTS.key?(key.to_sym)
end
def []=(key, value)
......@@ -39,6 +38,10 @@ module Inkoc
def define_module?
@options[:define_module]
end
def import_trait_module?
@options[:import_trait_module]
end
end
end
end
......@@ -113,45 +113,6 @@ impl Numeric for Float {
}
impl Float {
## Parses a `Float` from a `String`.
##
## # Error handling
##
## This method will throw if the input `String` is not in the right format.
##
## # Format
##
## The input `String` is expected to be in base 10. Leading and/or trailing
## whitespace is considered to be invalid, and will result in an error being
## thrown.
##
## # Examples
##
## Parsing a `Float` from a `String`:
##
## try! Float.from_string('10.5') # => 10.5
##
## Parsing a `Float` with an exponent:
##
## try! Float.from_string('1.2e1') # => 12.0
def from_string(string: String) !! StandardError -> Float {
string == INFINITY_LABEL
.if_true {
return INFINITY
}
string == NEGATIVE_INFINITY_LABEL
.if_true {
return NEGATIVE_INFINITY
}
try {
_INKOC.string_to_float(string)
} else (error) {
throw StandardError.new(error as String)
}
}
## Returns `True` if `self` is not a number (NAN).
##
## # Examples
......@@ -355,3 +316,46 @@ let INFINITY = 1.0 / 0.0
## The negative infinity value.
let NEGATIVE_INFINITY = -1.0 / 0.0
## Parses a `Float` from a `String`.
##
## # Error handling
##
## This method will throw if the input `String` is not in the right format.
##
## # Format
##
## The input `String` is expected to be in base 10. Leading and/or trailing
## whitespace is considered to be invalid, and will result in an error being
## thrown.
##
## # Examples
##
## Parsing a `Float` from a `String`:
##
## import std::float
##
## try! float.parse('10.5') # => 10.5
##
## Parsing a `Float` with an exponent:
##
## import std::float
##
## try! float.parse('1.2e1') # => 12.0
def parse(string: String) !! StandardError -> Float {
string == INFINITY_LABEL
.if_true {
return INFINITY
}
string == NEGATIVE_INFINITY_LABEL
.if_true {
return NEGATIVE_INFINITY
}
try {
_INKOC.string_to_float(string)
} else (error) {
throw StandardError.new(error as String)
}
}
......@@ -355,53 +355,6 @@ object HashMap!(K: Hash + Equal, V) impl
Index!(K, V),
SetIndex!(K, V),
Length {
## Returns a `HashMap` using two arrays: one containing the keys and one
## containing the values.
##
## Using this method is semantically equivalent to creating a `HashMap` using
## `HashMap.new` and sending `[]=` to the `HashMap` for every key-value pair.
## In other words, this:
##
## %['name': 'Alice']
##
## Is the same as this:
##
## let mut map = HashMap.new
##
## map['name'] = 'Alice'
##
## # Compiler optimisation
##
## To remove the need for allocating two arrays for `HashMap` literals the
## compiler may decide to optimise `HashMap.from_array` into separate `[]=`
## message sends as illustrated above.
##
## # Examples
##
## Creating a `HashMap` from two arrays:
##
## HashMap.from_array(['name'], ['Alice']) # => %['name': 'Alice']
def from_array!(K: Hash + Equal, V)(keys: Array!(K), values: Array!(V)) -> HashMap!(K, V) {
let mut map = new
let max_index = values.length - 1
keys.length == values.length
.if_false {
process.panic('An equal number of keys and values must be specified')
}
keys.each_with_index do (key: K, index: Integer) {
index > max_index
.if_true {
return map
}
map[key] = *values[index]
}
map
}
## Creates a new, empty `HashMap`.
##
## The `hasher` argument can be used to provide an alternative `Hasher` to use
......@@ -635,3 +588,55 @@ object HashMap!(K: Hash + Equal, V) impl
@table.length
}
}
## Returns a `HashMap` using two arrays: one containing the keys and one
## containing the values.
##
## Using this method is semantically equivalent to creating a `HashMap` using
## `HashMap.new` and sending `[]=` to the `HashMap` for every key-value pair.
## In other words, this:
##
## %['name': 'Alice']
##
## Is the same as this:
##
## let mut map = HashMap.new
##
## map['name'] = 'Alice'
##
## # Compiler optimisation
##
## To remove the need for allocating two arrays for `HashMap` literals, the
## compiler may decide to optimise this method into separate `[]=` message sends
## as illustrated above.
##
## # Examples
##
## Creating a `HashMap` from two arrays:
##
## import std::hash_map
##
## hash_map.from_array(['name'], ['Alice']) # => %['name': 'Alice']
def from_array!(K: Hash + Equal, V)(
keys: Array!(K),
values: Array!(V)
) -> HashMap!(K, V) {
let mut map = HashMap.new
let max_index = values.length - 1
keys.length == values.length
.if_false {
process.panic('An equal number of keys and values must be specified')
}
keys.each_with_index do (key: K, index: Integer) {
index > max_index
.if_true {
return map
}
map[key] = *values[index]
}
map
}
......@@ -8,53 +8,6 @@ import std::numeric::Numeric
import std::operators::*
import std::successor::Successor
impl Integer {
## Parses an `Integer` from a `String` in a given base.
##
## # Error handling
##
## This method will panic if `radix` is not between `2` and `36`. If the input
## `String` is invalid, an error will be thrown instead.
##
## # Format
##
## The `String` is expected to start with an optional `+` or `-` sign,
## followed by one or more digits. Digits are a subset of the following
## ranges, depending on the value of the `radix` argument:
##
## * 0-9
## * a-z
## * A-Z
##
## Leading and/or trailing whitespace is considered to be invalid, and will
## result in an error being thrown.
##
## # Examples
##
## Parsing a `String` into a base 2 `Integer`:
##
## try! Integer.from_string('11', 2) # => 3
##
## Parsing a `String` into a base 8 `Integer`:
##
## try! Integer.from_string('0o52', 8) # => 42
##
## Parsing a `String` into a base 10 `Integer`:
##
## try! Integer.from_string('10') # => 10
##
## Parsing a `String` into a base 16 `Integer`:
##
## try! Integer.from_string('F', 16) # => 15
def from_string(string: String, radix = 10) !! StandardError -> Integer {
try {
_INKOC.string_to_integer(string, radix)
} else (error) {
throw StandardError.new(error as String)
}
}
}
impl ToInteger for Integer {
def to_integer -> Integer {
self
......@@ -218,3 +171,56 @@ impl Successor for Integer {
self + 1
}
}
## Parses an `Integer` from a `String` in a given base.
##
## # Error handling
##
## This method will panic if `radix` is not between `2` and `36`. If the input
## `String` is invalid, an error will be thrown instead.
##
## # Format
##
## The `String` is expected to start with an optional `+` or `-` sign, followed
## by one or more digits. Digits are a subset of the following ranges, depending
## on the value of the `radix` argument:
##
## * 0-9
## * a-z
## * A-Z
##
## Leading and/or trailing whitespace is considered to be invalid, and will
## result in an error being thrown.
##
## # Examples
##
## Parsing a `String` into a base 2 `Integer`:
##
## import std::integer
##
## try! integer.parse('11', 2) # => 3
##
## Parsing a `String` into a base 8 `Integer`:
##
## import std::integer
##
## try! integer.parse('0o52', 8) # => 42
##
## Parsing a `String` into a base 10 `Integer`:
##
## import std::integer
##
## try! integer.parse('10') # => 10
##
## Parsing a `String` into a base 16 `Integer`:
##
## import std::integer
##
## try! integer.parse('F', 16) # => 15
def parse(string: String, radix = 10) !! StandardError -> Integer {
try {
_INKOC.string_to_integer(string, radix)
} else (error) {
throw StandardError.new(error as String)
}
}
......@@ -4,7 +4,9 @@
#! UTF-8.
import std::conversion::(ToFloat, ToInteger, ToString)
import std::float
import std::hash::(Hasher, Hash)
import std::integer
import std::length::Length
import std::operators::(Equal, Add)
......@@ -147,7 +149,7 @@ impl ToInteger for String {
## '10'.to_integer # => 10
## 'a'.to_integer # => 0
def to_integer -> Integer {
try Integer.from_string(self, 10) else (_) 0
try integer.parse(self, 10) else (_) 0
}
}
......@@ -164,7 +166,7 @@ impl ToFloat for String {
## '10.5'.to_float # => 10.5
## 'a'.to_float # => 0.0
def to_float -> Float {
try Float.from_string(self) else (_) 0.0
try float.parse(self) else (_) 0.0
}
}
......
#! Methods for `Trait` that should be available before defining other types.
impl Trait {
## Implements `self` for the given object.
##
## Implementing a trait will result in all the trait's methods being copied
## over to the given object. Once copied the trait is tracked in an internal
## object so modules such as `std::mirror` can reflect upon this data during
## runtime.
##
## This method is not really meant for general use, instead it's called by the
## compiler whenever implementing a trait using the `impl` keyword.
##
## # Examples
##
## Manually implementing a trait, ignoring any required methods:
##
## trait MyTrait {
## def number -> Integer {
## 42
## }
## }
##
## object Person {}
##
## MyTrait.implement_for(Person) # => MyTrait
##
## Person.new.number # => 42
def implement_for(subject) -> Trait {
let mut traits =
_INKOC.set_attribute_to_object(subject, '@_implemented_traits')
## Implements a `Trait` for an `Object`.
##
## Implementing a trait will result in all the trait's methods being copied over
## to the given object. Once copied, the trait is tracked in an internal object
## so modules such as `std::mirror` can reflect upon this data during runtime.
##
## This method is not really meant for general use, instead it's called by the
## compiler whenever implementing a trait using the `impl` keyword.
##
## # Examples
##
## Manually implementing a trait, ignoring any required methods:
##
## # `trait` is a keyword, so we must define a custom alias for the module.
## import std::trait::(self as trait_mod)
##
## trait MyTrait {
## def number -> Integer {
## 42
## }
## }
##
## object Person {}
##
## trait_mod
## .implement(implement_for: Person, to_implement: MyTrait) # => MyTrait
##
## Person.new.number # => 42
def implement(implement_for: Object, to_implement: Trait) -> Trait {
# This will look up `@_implemented_traits` if it exists, or create it (then
# return it) if it does not.
let mut traits =
_INKOC.set_attribute_to_object(implement_for, '@_implemented_traits')
_INKOC.copy_blocks(subject, self)
_INKOC.set_attribute(traits, self, True)
_INKOC.copy_blocks(implement_for, to_implement)
self
}
# This marks the Trait as implemented by storing the Trait as an attribute,
# with its value set to True. We can not use other data structures such as
# `std::hash_map::HashMap`, since they may not yet exist when this method is
# used.
_INKOC.set_attribute(traits, to_implement, True)
to_implement
}
import std::float::(NAN, INFINITY, NEGATIVE_INFINITY)
import std::float::(self, NAN, INFINITY, NEGATIVE_INFINITY)
import std::format::DefaultFormatter
import std::hasher::DefaultHasher
import std::test
......@@ -823,51 +823,51 @@ test.group('std::float::Float.hash') do (g) {
}
}
test.group('std::float::Float.from_string') do (g) {
test.group('std::float.parse') do (g) {
g.test('Parsing a Float') {
assert.equal(try! Float.from_string('1.2'), 1.2)
assert.equal(try! float.parse('1.2'), 1.2)
}
g.test('Parsing a Float with leading whitespace') {
assert.panic {
try! Float.from_string(' 1.2')
try! float.parse(' 1.2')
}
}
g.test('Parsing a Float with trailing whitespace') {
assert.panic {
try! Float.from_string('1.2 ')
try! float.parse('1.2 ')
}
}
g.test('Parsing a Float with an exponentn') {
assert.equal(try! Float.from_string('1.2e1'), 12.0)
assert.equal(try! Float.from_string('1.2E1'), 12.0)
assert.equal(try! float.parse('1.2e1'), 12.0)
assert.equal(try! float.parse('1.2E1'), 12.0)
}