Skip to content
Snippets Groups Projects
Unverified Commit a3fc88d7 authored by Yorick Peterse's avatar Yorick Peterse
Browse files

WIP: implement Inko's compiler in Inko

parent 4154a36d
No related branches found
No related tags found
No related merge requests found
Showing
with 1330 additions and 3 deletions
# The Inko bytecode compiler.
#
# The types and methods of modules in the `std::compiler` namespace are not
# part of the public API at this time, meaning they can change at any time.
import std::byte_array::ToByteArray
import std::compiler::ast::body::Body
import std::compiler::diagnostics::Diagnostics
import std::compiler::module_name::ModuleName
import std::compiler::pass::desugar_object::DesugarObject
import std::compiler::pass::extract_imports::ExtractImports
import std::compiler::pass::hoist_imports::HoistImports
import std::compiler::pass::insert_implicit_imports::InsertImplicitImports
import std::compiler::pass::parse_source::(ReadSource, ParseSource)
import std::compiler::state::State
import std::fs::path::ToPath
import std::process::(self, Process)
# A type holding the information needed to compile a module stored on disk.
object CompileRequest {
# The name of the module to compile.
@name: ModuleName
# The state to use for the compiler.
@state: State
# The process to notify when compilation finishes.
@notify: Process
def init(name: ModuleName, state: State, notify: Process) {
@name = name
@state = state
@notify = notify
}
def name -> ModuleName {
@name
}
def state -> State {
@state
}
def notify -> Process {
@notify
}
}
# A type used for compiling Inko source code.
object Compiler {
# The state available to all compiler processes.
@state: State
# The diagnostics produced by this compiler.
@diagnostics: Diagnostics
def init(state: State) {
@state = state
@diagnostics = Diagnostics.new
}
# Compiles the main module.
#
# The main module is the module run directly by the user, and always has a
# fixed name.
#
# The return value is `True` if any errors were produced.
def compile_main_module(path: ToPath) -> Boolean {
let name = ModuleName.new(Array.new('main'))
@state.module_pending(name)
compile_file(name: name, path: path)
}
# Compiles a module using a file path derived from the module name.
#
# The return value is `True` if any errors were produced.
def compile_module(name: ModuleName) -> Boolean {
let module_path = name.source_path
@state.config.source_directories.each do (directory) {
let source_file = directory.join(module_path)
source_file.file?.if_true {
return compile_file(name: name, path: source_file)
}
}
@diagnostics.read_source_error(
message: 'The source file could not be found',
path: module_path
)
module_finished(name)
}
# Compiles a module located at the given file path.
#
# The return value is `True` if any errors were produced.
def compile_file(name: ModuleName, path: ToPath) -> Boolean {
let source = ReadSource.new(@diagnostics).run(path)
compile_source(name: name, source: source, path: path)
}
# Compiles the source code of a module.
#
# The return value is `True` if any errors were produced.
def compile_source(
name: ModuleName,
source: ToByteArray,
path: ToPath
) -> Boolean {
let mut ast = ParseSource.new(@diagnostics).run(input: source, path: path)
@diagnostics.errors?.if_true {
return module_finished(name)
}
ast = DesugarObject.new.run(ast)
ast = HoistImports.new.run(ast)
ast = InsertImplicitImports.new(name).run(ast)
compile_dependencies(ast)
@state.errors?.if_true {
return module_finished(name)
}
# TODO: run other passes
module_finished(name)
}
# Compiles all the modules imported in the AST.
def compile_dependencies(ast: Body) {
# We can not both mark and spawn processes at the same time, as messages
# sent to us from the child processes may conflict with the messages sent
# from the coordinator.
#
# To work around this, we first mark all the modules, then spawn the
# processes to compile them.
let to_compile = ExtractImports
.new
.run(ast)
.iter
.select do (name) { @state.module_pending(name) }
.to_array
let mut pending = to_compile.length
to_compile.each do (name) {
compile_dependency(name)
}
{ pending.positive? }.while_true {
process.receive
pending -= 1
}
}
# Schedules the compilation of a module that was imported.
def compile_dependency(name: ModuleName) {
let child = process.spawn {
let request = process.receive as CompileRequest
process.defer {
request.notify.send(1)
}
Compiler.new(request.state).compile_module(request.name)
}
let request =
CompileRequest.new(name: name, state: @state, notify: process.current)
child.send(request)
}
def module_finished(name: ModuleName) -> Boolean {
@state.module_finished(name: name, diagnostics: @diagnostics)
}
}
Loading
Loading
@@ -2,6 +2,7 @@
import std::compiler::ast::body::Body
import std::compiler::ast::node::Node
import std::compiler::ast::type_parameter::TypeParameter
import std::compiler::config::(INIT_METHOD, NEW_METHOD)
import std::compiler::source_location::SourceLocation
 
# An argument as defined in a closure, lambda, or method..
Loading
Loading
@@ -56,12 +57,12 @@ object Argument {
@value_type
}
 
## Returns `True` if the argument is mutable.
# Returns `True` if the argument is mutable.
def mutable? -> Boolean {
@mutable
}
 
## Returns `True` if the argument is a rest argument.
# Returns `True` if the argument is a rest argument.
def rest? -> Boolean {
@rest
}
Loading
Loading
@@ -315,12 +316,25 @@ object MethodDefinition {
def static_method? -> Boolean {
@static_method
}
# Returns `True` if this method is an instance method.
def instance_method? -> Boolean {
@static_method.not
}
}
 
impl Node for MethodDefinition {
def location -> SourceLocation {
@location
}
def static_new_method? -> Boolean {
static_method?.and { name == NEW_METHOD }
}
def init_method? -> Boolean {
instance_method?.and { name == INIT_METHOD }
}
}
 
# A required method created using the `def` keyword.
Loading
Loading
# AST types for import statements.
import std::compiler::ast::node::Node
import std::compiler::ast::variables::Identifier
import std::compiler::module_name::ModuleName
import std::compiler::source_location::SourceLocation
 
# The alias to import a symbol under.
Loading
Loading
@@ -100,14 +101,23 @@ object Import {
@symbols
}
 
## Returns `True` if all symbols should be imported from the module.
# Returns `True` if all symbols should be imported from the module.
def import_all? -> Boolean {
@import_all
}
# Returns a `ModuleName` for the module that is imported.
def module_name -> ModuleName {
ModuleName.new(@path.iter.map do (ident) { ident.name }.to_array)
}
}
 
impl Node for Import {
def location -> SourceLocation {
@location
}
def import? -> Boolean {
True
}
}
Loading
Loading
@@ -6,4 +6,24 @@ import std::operators::Equal
trait Node: Equal {
# Returns the location of the AST node.
def location -> SourceLocation
# Returns `True` if the current node is an object definition.
def object_definition? -> Boolean {
False
}
# Returns `True` if the current node defines the static method "new".
def static_new_method? -> Boolean {
False
}
# Returns `True` if the current node fines the instance method "init".
def init_method? -> Boolean {
False
}
# Returns `True` if the current node is an `import` expression.
def import? -> Boolean {
False
}
}
Loading
Loading
@@ -51,6 +51,10 @@ impl Node for ObjectDefinition {
def location -> SourceLocation {
@location
}
def object_definition? -> Boolean {
True
}
}
 
# The definition of an attribute in an object.
Loading
Loading
# Types for requesting the compilation of Inko source code.
import std::byte_array::(ByteArray, ToByteArray)
import std::compiler::diagnostics::Diagnostics
import std::compiler::errors::READ_SOURCE_ERROR
import std::compiler::source_location::SourceLocation
import std::fs::file
import std::fs::path::(ToPath, Path)
import std::string_buffer::StringBuffer
# A request to compile source code.
trait CompileRequest {
# The path of the file that is being compiled.
def path -> Path
# Reads the source code and returns it as a `ByteArray`.
#
# The `diagnostics` argument can be used to add diagnostics when reading
# fails.
def read_source(diagnostics: Diagnostics) -> ByteArray
}
# A request to compile a pre-defined chunk of source code.
object CompileSource {
# The source code to compile.
@source: ByteArray
# The (fake) path of the source code to compile.
@path: Path
def init(source: ToByteArray, path: ToPath) {
@source = source.to_byte_array
@path = path.to_path
}
}
impl CompileRequest for CompileSource {
def path -> Path {
@path
}
def read_source(diagnostics: Diagnostics) -> ByteArray {
@source
}
}
# A request to compile a file.
object CompileFile {
# The path of the source code to compile.
@path: Path
def init(path: ToPath) {
@path = path.to_path
}
}
impl CompileRequest for CompileFile {
def path -> Path {
@path
}
def read_source(diagnostics: Diagnostics) -> ByteArray {
let bytes = ByteArray.new
let handle = try {
file.read_only(path).read_bytes(bytes)
} else (err) {
let loc = SourceLocation.new(file: path, line_range: 1..1, column: 1)
let msg = StringBuffer
.new('Failed to read ', loc.file.to_string, ': ', err.message)
diagnostics.error(id: READ_SOURCE_ERROR, message: msg, location: loc)
return bytes
}
bytes
}
}
# Configuration options for a compiler.
import std::env
import std::fs::dir
import std::fs::path::(ToPath, Path)
import std::inko::INKO_VERSION
import std::os
# The name used for initialiser methods, after they have been allocated.
let INIT_METHOD = 'init'
# The name used for the built-in factory method for creating new object
# instances.
let NEW_METHOD = 'new'
# The name used for the method that just allocates a new method, without calling
# the initialiser method.
let ALLOCATE_METHOD = 'allocate'
# The name of the cache directory (relative to a cache root directory) to store
# bytecode files in.
let CACHE_DIRECTORY_NAME = 'inko'
# The default directory to use for finding source files.
let DEFAULT_SOURCE_DIRECTORY =
os.windows?.if(true: { 'C:\\inko\\lib' }, false: { '/usr/lib/inko' })
# The file extension of Inko source files.
let SOURCE_EXTENSION = '.inko'
# A type for storing compiler settings, such as the source directories to search
# through.
object Config {
# The source directories to search through when importing a module.
@source_directories: Array!(Path)
# The target directory to store compiled bytecode files in.
@target_directory: Path
def init {
@source_directories = Array.new(default_source_directory)
@target_directory = default_target_directory
}
def source_directories -> Array!(Path) {
@source_directories
}
def add_source_directory(path: ToPath) -> Path {
@source_directories.push(path.to_path)
}
def target_directory -> Path {
@target_directory
}
def target_directory=(path: ToPath) -> Path {
@target_directory = path.to_path
}
def default_source_directory -> Path {
let inkoc_lib = env['INKOC_LIB']
inkoc_lib.if_true {
return Path.new(inkoc_lib!)
}
Path.new(DEFAULT_SOURCE_DIRECTORY).join(INKO_VERSION)
}
def default_target_directory -> Path {
base_cache_directory.join(CACHE_DIRECTORY_NAME).join(INKO_VERSION)
}
def base_cache_directory -> Path {
let cache_home = os.windows?.if(
true: { env['LOCALAPPDATA'] },
false: { env['XDG_CACHE_HOME'] }
)
cache_home.length.positive?.if_true {
return Path.new(cache_home!)
}
let home = env.home_directory
let base = home.if(true: { home! }, false: { env.temporary_directory })
base.join('.cache')
}
def create_target_directory -> Nil {
try dir.create(path: target_directory, recursive: True) else Nil
}
}
# Coordination of parallel module compiltation.
import std::compiler::diagnostics::Diagnostics
import std::compiler::module_name::ModuleName
import std::compiler::server::(Server, Query)
# A module is in the process of being compiled.
let PENDING = 0
# A module has been compiled successfully.
let FINISHED = 1
# A type used for registering the modules being compiled,
object Coordinator {
# The compilation status of every module that is being or has been imported.
@module_status: Map!(ModuleName, Integer)
# All diagnostics produced by the compiler processes.
@diagnostics: Diagnostics
def init {
@module_status = Map.new
@diagnostics = Diagnostics.new
}
def diagnostics -> Diagnostics {
@diagnostics
}
def module_status -> Map!(ModuleName, Integer) {
@module_status
}
}
impl Server for Coordinator {}
# A query for marking a module as currently being compiled.
object ModulePending {
# The name of the module that a process wants to compile.
@name: ModuleName
def init(name: ModuleName) {
@name = name
}
}
impl Query!(Boolean) for ModulePending {
def run(server: Coordinator) -> Boolean {
server.diagnostics.errors?.if_true {
return False
}
server.module_status[@name].if_true {
return False
}
server.module_status[@name] = PENDING
True
}
}
# A query that marks a module as being compiled.
object ModuleFinished {
# The name of the module that has been compiled.
@name: ModuleName
# Any diagnostics that have been produced while compiling the module.
@diagnostics: Diagnostics
def init(name: ModuleName, diagnostics: Diagnostics) {
@name = name
@diagnostics = diagnostics
}
}
impl Query!(Boolean) for ModuleFinished {
def run(server: Coordinator) -> Boolean {
server.diagnostics.append(@diagnostics)
server.diagnostics.errors?
}
}
# A query for shutting down the coordinator.
object Stop {}
impl Query!(Boolean) for Stop {
def stop? -> Boolean {
True
}
def run(server: Coordinator) -> Boolean {
True
}
}
# A query that checks if any errors have been produced thus far.
object HasErrors {}
impl Query!(Boolean) for HasErrors {
def run(server: Coordinator) -> Boolean {
server.diagnostics.errors?
}
}
# A query that prints the diagnostics that have been produced thus far.
object DisplayDiagnostics {}
impl Query!(Boolean) for DisplayDiagnostics {
def run(server: Coordinator) -> Boolean {
# TODO: formatter object
server.diagnostics.iter.each do (diagnostic) {
_INKOC.stdout_write(diagnostic.level + ': ' + diagnostic.message + "\n")
}
server.diagnostics.errors?
}
}
# Diagnostics produced by the compiler, such as warnings and errors.
import std::compiler::errors::*
import std::compiler::source_location::SourceLocation
import std::conversion::ToString
import std::fs::path::Path
import std::iterator::Iterator
import std::length::Length
import std::string_buffer::StringBuffer
# The level identifier to use for errors.
let ERROR = 'error'
# The level identifier to use for warnings.
let WARNING = 'warning'
# A single warning or error produced by the compiler.
object Diagnostic {
# The identifier associated with this diagnostic.
#
# Error identifiers can be used to more easily look up additional information
# about a diagnostic.
@id: String
# The level of the diagnostic.
@level: String
# The message of the diagnostic.
#
# Messages are unique to the diagnostics. For looking up more details about a
# diagnostic, one should use the identifier.
@message: String
# The source location that triggered the creation of this diagnostic.
@location: SourceLocation
def init(
id: String,
level: String,
message: String,
location: SourceLocation
) {
@id = id
@level = level
@message = message
@location = location
}
# Returns the identifier of this diagnostic.
def id -> String {
@id
}
# Returns the level of this diagnostic.
def level -> String {
@level
}
# Returns the message of this diagnostic.
def message -> String {
@message
}
# Returns the location this diagnostic was triggered on.
def location -> SourceLocation {
@location
}
}
# A collection of Diagnostic objects produced by the compiler.
object Diagnostics {
# The collection of diagnostics produced so far.
@values: Array!(Diagnostic)
# A boolean indicating that one or more errors have been produced.
@errors: Boolean
def init {
@values = Array.new
@errors = False
}
# Returns `True` if there are one or more error diagnostics.
def errors? -> Boolean {
@errors
}
# Returns `True` if any diagnostics have been recorded.
def any? -> Boolean {
@values.length.positive?
}
# Appends the `Diagnostics` to `self`.
def append(other: Diagnostics) -> Self {
other.iter.each do (diag) {
(diag.level == ERROR).if_true {
@errors = True
}
@values.push(diag)
}
self
}
# Returns an `Iterator` over all the values in this collection.
def iter -> Iterator!(Diagnostic) {
@values.iter
}
# Records a new error diagnostic.
def error(
id: String,
message: ToString,
location: SourceLocation
) -> Diagnostic {
let diag = Diagnostic
.new(id: id, level: ERROR, message: message.to_string, location: location)
@errors = True
@values.push(diag)
}
# Records a new warning diagnostic.
def warning(
id: String,
message: ToString,
location: SourceLocation
) -> Diagnostic {
let diag = Diagnostic.new(
id: id,
level: WARNING,
message: message.to_string,
location: location
)
@values.push(diag)
}
# Adds an error for a failure to read source code from a file.
def read_source_error(message: String, path: Path) -> Diagnostic {
let loc = SourceLocation.new(file: path, line_range: 1..1, column: 1)
let msg = StringBuffer.new('Failed to read ', path.to_string, ': ', message)
error(id: READ_SOURCE_ERROR, message: msg, location: loc)
}
# Adds an error for a syntax/parse error.
def parse_error(message: String, location: SourceLocation) -> Diagnostic {
let msg = StringBuffer
.new('Failed to parse ', location.file.to_string, ': ', message)
error(id: PARSE_SOURCE_ERROR, message: msg, location: location)
}
}
impl Length for Diagnostics {
def length -> Integer {
@values.length
}
}
# Error codes/identifiers produced by the compiler.
# The source code of a file could not be read.
#
# This error is produced when a file does not exist, the user does not have the
# right permissions to read a file, or another error preventing the compiler
# from reading the source file.
let READ_SOURCE_ERROR = 'E001'
# Inko source code could not be parsed into an AST.
#
# This error is produced when the source code to parse is invalid.
let PARSE_SOURCE_ERROR = 'E002'
# Types and methods for module names.
import std::compiler::config::SOURCE_EXTENSION
import std::fs::path::(Path, SEPARATOR)
import std::hash::*
import std::operators::Equal
import std::string_buffer::StringBuffer
# The name of a module.
object ModuleName {
# The components of the module name.
#
# For example, the module name `std::foo` would translate to
# `Array.new('std', 'foo')`.
@components: Array!(String)
def init(components: Array!(String)) {
@components = components
}
# The components that make up the name of a module
def components -> Array!(String) {
@components
}
# Returns the relative path to the module's source code.
def source_path -> Path {
let buffer = StringBuffer.new
@components.each_with_index do (component, index) {
index.positive?.if_true {
buffer.push(SEPARATOR)
}
buffer.push(component)
}
buffer.push(SOURCE_EXTENSION)
Path.new(buffer.to_string)
}
}
impl Equal for ModuleName {
def ==(other: Self) -> Boolean {
@components == other.components
}
}
impl Hash for ModuleName {
def hash(hasher: Hasher) {
@components.each do (value) {
value.hash(hasher)
}
}
}
Loading
Loading
@@ -301,6 +301,9 @@ object Parser {
let throw_type = try optional_throw_type
let return_type = try optional_return_type
 
# TODO: remove once we're using the self-hosting compiler.
try! trait_bounds
(peek_token.type == 'curly_open').if_false {
return RequiredMethodDefinition.new(
name: name,
Loading
Loading
# Compiler pass for desugaring an AST.
import std::compiler::ast::blocks::MethodDefinition
import std::compiler::ast::body::Body
import std::compiler::ast::control_flow::Try
import std::compiler::ast::node::Node
import std::compiler::ast::objects::ObjectDefinition
import std::compiler::ast::send::Send
import std::compiler::ast::variables::(DefineVariable, Identifier, SelfObject)
import std::compiler::config::(ALLOCATE_METHOD, INIT_METHOD, NEW_METHOD)
# The name of the variable to use for storing object instances.
let INSTANCE_LOCAL = 'instance'
# Compiler pass for desugaring object definitions.
#
# When an object defines its own "init" method, this pass generates a
# corresponding "new" method with the same signature. For example, this:
#
# object Person {
# @name: String
#
# def init(name: String) {
# @name = name
# }
# }
#
# Is desugared into this:
#
# object Person {
# @name: String
#
# def init(name: String) {
# @name = name
# }
#
# static def new(name: String) -> Person {
# let instance = allocate
#
# instance.init(name)
#
# instance
# }
# }
object DesugarObject {
# Runs the pass, returning the input `Body`.
def run(body: Body) -> Body {
body.children.each do (child) {
child.object_definition?.if_true {
desugar_object(child as ObjectDefinition)
}
}
body
}
# Desugars the given object, if necessary.
def desugar_object(node: ObjectDefinition) {
new_method?(node).if_true {
return
}
let init_method = init_method(node)
init_method.nil?.if_true {
return
}
node.body.children.push(new_from_init(init_method!))
}
# Returns a "new" method based on the provided "init" method's definition.
def new_from_init(init_method: MethodDefinition) -> MethodDefinition {
let location = init_method.location
# The arguments to pass to init()
let init_args =
init_method
.arguments
.iter
.map do (arg) { Identifier.new(name: arg.name, location: location) }
.to_array
# let instance = allocate
let allocate_instance = DefineVariable.new(
name: Identifier.new(name: INSTANCE_LOCAL, location: location),
value_type: Nil,
value: Send.new(
message: ALLOCATE_METHOD,
receiver: SelfObject.new(location),
arguments: Array.new,
type_arguments: Array.new,
location: location
),
mutable: False,
location: location
)
# instance.init(...), or try instance.init(...) in case "init" throws an
# error.
let mut init_instance: Node = Send.new(
message: INIT_METHOD,
receiver: Identifier.new(name: INSTANCE_LOCAL, location: location),
arguments: init_args,
type_arguments: Array.new,
location: location
)
init_method.throw_type.if_true {
init_instance = Try.new(
expression: init_instance,
error_variable: Nil,
else_body: Body.new(children: Array.new, location: location),
location: location
)
}
# Since the compiler does not modify the arguments in a way that would make
# init/new incompatible, we do not need to deep copy the arguments. Since
# the types are the same for both methods, type inference/checking will also
# work just fine.
MethodDefinition.new(
name: NEW_METHOD,
type_parameters: init_method.type_parameters,
arguments: init_method.arguments,
throw_type: init_method.throw_type,
return_type: init_method.return_type,
static_method: True,
body: Body.new(
children: Array.new!(Node)(
allocate_instance,
init_instance,
Identifier.new(name: INSTANCE_LOCAL, location: location)
),
location: location
),
location: location
)
}
def new_method?(node: ObjectDefinition) -> Boolean {
node.body.children.iter.any? do (node) { node.static_new_method? }
}
def init_method(node: ObjectDefinition) -> ?MethodDefinition {
node.body.children.iter.find do (node) {
node.init_method?
} as ?MethodDefinition
}
}
# Compiler pass for extracting modules imported by a module.
import std::compiler::ast::body::Body
import std::compiler::ast::imports::Import
import std::compiler::module_name::ModuleName
# Compiler pass for extracting modules imported by a module.
object ExtractImports {
def run(body: Body) -> Array!(ModuleName) {
body
.children
.iter
.select do (node) { node.import? }
.map do (node) { (node as Import).module_name }
.to_array
}
}
# Compiler pass for hosting imports.
import std::compiler::ast::body::Body
# Compiler pass for hosting imports to the start of a module.
#
# While `import` expressions can only appear at the top-level of a module, they
# can follow non-import code. For example, the following is valid Inko code:
#
# import std::stdio::stdout
#
# stdout.print('foo')
#
# import std::stdio::stderr
#
# This compiler pass hoists all imports to the start of a module, retaining the
# order in which the `import` expressions appeared in the module. This means the
# above example would be turned into the following:
#
# import std::stdio::stdout
# import std::stdio::stderr
#
# stdout.print('foo')
#
# Hosting imports is done so we can process (e.g. type check) dependencies
# first.
object HoistImports {
# Runs the pass on the given `Body` node.
#
# The return value is a new `Body` with all the imports hoisted.
def run(body: Body) -> Body {
let pair = body.children.iter.partition do (node) { node.import? }
Body.new(children: pair.first.append(pair.second), location: body.location)
}
}
# Compiler pass for inserting implicit imports.
import std::compiler::ast::body::Body
import std::compiler::ast::imports::(Import, ImportAlias, ImportSymbol)
import std::compiler::ast::variables::Identifier
import std::compiler::module_name::ModuleName
import std::compiler::source_location::SourceLocation
# The name of the core::bootstrap module.
let CORE_BOOTSTRAP = ModuleName.new(Array.new('core', 'bootstrap'))
# The name of the core::modules module.
let CORE_GLOBALS = ModuleName.new(Array.new('core', 'globals'))
# The name of the std::prelude module.
let CORE_PRELUDE = ModuleName.new(Array.new('core', 'prelude'))
# Compiler pass for inserting implicit imports.
#
# Some imports are inserted implicit, such as the importing of
# `core::bootstrap`. This pass inserts these imports where needed.
object InsertImplicitImports {
# The name of the module that we are processing.
@module_name: ModuleName
def init(module_name: ModuleName) {
@module_name = module_name
}
# Runs the pass, returning a `Body` that includes the implicit import
# expressions.
def run(body: Body) -> Body {
let nodes = Array.new
import_bootstrap(nodes: nodes, location: body.location)
import_globals(nodes: nodes, location: body.location)
import_prelude(nodes: nodes, location: body.location)
nodes.empty?.if_true {
return body
}
Body.new(children: nodes.append(body.children), location: body.location)
}
def import_bootstrap(nodes: Array!(Import), location: SourceLocation) {
(@module_name == CORE_BOOTSTRAP).if_true {
return
}
let path =
import_path_for_module(module: CORE_BOOTSTRAP, location: location)
let symbols = Array.new(
ImportSymbol.new(
name: 'self',
location: location,
alias: ImportAlias.new(name: '_', location: location)
)
)
let node = Import
.new(path: path, symbols: symbols, import_all: False, location: location)
nodes.push(node)
}
def import_globals(nodes: Array!(Import), location: SourceLocation) {
(@module_name == CORE_BOOTSTRAP)
.or { @module_name == CORE_GLOBALS }
.if_true {
return
}
nodes.push(import_all(module: CORE_GLOBALS, location: location))
}
def import_prelude(nodes: Array!(Import), location: SourceLocation) {
(@module_name == CORE_BOOTSTRAP)
.or { @module_name == CORE_GLOBALS }
.or { @module_name == CORE_PRELUDE }
.if_true {
return
}
nodes.push(import_all(module: CORE_PRELUDE, location: location))
}
def import_path_for_module(
module: ModuleName,
location: SourceLocation
) -> Array!(Identifier) {
module
.components
.iter
.map do (name) { Identifier.new(name: name, location: location) }
.to_array
}
def import_all(module: ModuleName, location: SourceLocation) -> Import {
Import.new(
path: import_path_for_module(module: module, location: location),
symbols: Array.new,
import_all: True,
location: location
)
}
}
# Compiler passes for reading and parsing source code.
import std::byte_array::(ByteArray, ToByteArray)
import std::compiler::ast::body::Body
import std::compiler::diagnostics::Diagnostics
import std::compiler::errors::(READ_SOURCE_ERROR, PARSE_SOURCE_ERROR)
import std::compiler::parser::Parser
import std::compiler::source_location::SourceLocation
import std::fs::file
import std::fs::path::ToPath
import std::string_buffer::StringBuffer
# Compiler pass for reading source code from a file.
object ReadSource {
# A collection of diagnostics produced while reading the source code.
@diagnostics: Diagnostics
def init(diagnostics: Diagnostics) {
@diagnostics = diagnostics
}
# Runs the pass and reads the given path into a `ByteArray`.
def run(path: ToPath) -> ByteArray {
let bytes = ByteArray.new
let path_conv = path.to_path
try {
file.read_only(path_conv).read_bytes(bytes)
} else (err) {
@diagnostics.read_source_error(message: err.message, path: path_conv)
return bytes
}
bytes
}
}
# Compiler pass for parsing source code into an AST.
object ParseSource {
# A collection of diagnostics produced while parsing the source code.
@diagnostics: Diagnostics
def init(diagnostics: Diagnostics) {
@diagnostics = diagnostics
}
# Runs the pass using the given `String` or `ByteArray` as the input source
# code.
def run(input: ToByteArray, path: ToPath) -> Body {
try {
Parser.new(input: input, file: path).parse
} else (err) {
@diagnostics.parse_error(message: err.message, location: err.location)
Body.new(
children: Array.new,
location: SourceLocation.new(file: path, line_range: 1..1, column: 1)
)
}
}
}
# Types for compiler servers that can receive and respond to queries.
import std::process::(self, Process)
# A server that responds to queries sent by a process.
#
# Queries are objects that implement the `Query` trait. These objects can be
# used for anything from simply shutting down a server to obtaining data and
# sending it back to the process that scheduled a query.
#
# All queries executed are expected to produce some value to send back to the
# process that scheduled a query.
trait Server {
# Starts the server, blocking the current process until the server is stopped.
def run {
# We don't care nor know what the return type of a query is, so we use
# Dynamic here.
let message = process.receive as Message!(Dynamic)
let query = message.query
let reply = query.run(self)
message.sender.send(reply)
query.stop?.if_true {
return
}
run
}
}
# A query to execute on a `Server`.
#
# When a query is executed its `run` method is called and passed the server that
# is executing the query. It's then up to the `run` method to decide what needs
# to be done.
trait Query!(T) {
# A boolean that indicates if the server should stop after running this query.
def stop? -> Boolean {
False
}
# Schedules the query on a server and waits for a response.
#
# For running a query on the server side one should use the `run` method.
def schedule(server: Process) -> T {
server.send(Message.new(query: self, sender: process.current))
process.receive as T
}
# Runs the query on the server side.
#
# The return value of this method is sent back to the process that scheduled
# the query.
def run(server: Server) -> T
}
# A message sent to a `Server`
#
# The `Message` type acts as a wrapper around a query and the process that
# scheduled the query. By using a separate type we remove the need for every
# query to define an attribute and method for obtaining the sender of a query.
object Message!(T) {
# The query to execute.
@query: Query!(T)
# The process that sent the query.
@sender: Process
def init(query: Query!(T), sender: Process) {
@query = query
@sender = sender
}
# Returns the query to execute.
def query -> Query!(T) {
@query
}
# Returns the process that sent the query.
def sender -> Process {
@sender
}
}
# State available to all compiler processes.
import std::compiler::config::Config
import std::compiler::coordinator::(
Coordinator, DisplayDiagnostics, HasErrors, ModuleFinished, ModulePending,
Stop as StopCoordinator
)
import std::compiler::diagnostics::Diagnostics
import std::compiler::module_name::ModuleName
import std::compiler::typedb::database::(Database, Stop as StopDatabase)
import std::process::(self, Process)
# A type containing state that has to be available to all compiler processes.
object State {
# The process used for coordinating the compilation of modules.
@coordinator: Process
# The process that stores all type information.
@type_database: Process
# The configuration settings of the compiler, such as the source directories
# to search.
@config: Config
def init(config = Config.new) {
@coordinator = process.spawn { Coordinator.new.run }
@type_database = process.spawn { Database.new.run }
@config = config
}
def stop {
StopDatabase.new.schedule(@type_database)
StopCoordinator.new.schedule(@coordinator)
}
def coordinator -> Process {
@coordinator
}
def type_database -> Process {
@type_database
}
def config -> Config {
@config
}
# Marks a module as pending for compilation.
#
# The returned `Boolean` is `True` if we marked the module and can compile it.
# If it is `False`, the module is being or has been compiled.
def module_pending(name: ModuleName) -> Boolean {
ModulePending.new(name).schedule(@coordinator)
}
# Marks a module as finished.
#
# The returned `Boolean` is `True` if any errors were produced by any of the
# compiler processes.
def module_finished(name: ModuleName, diagnostics: Diagnostics) -> Boolean {
ModuleFinished
.new(name: name, diagnostics: diagnostics)
.schedule(@coordinator)
}
# Returns `True` if any errors have been produced.
def errors? -> Boolean {
HasErrors.new.schedule(@coordinator)
}
# Displays all diagnostics produced thus far.
#
# The returned `Boolean` is `True` if any errors were produced.
def display_diagnostics -> Boolean {
DisplayDiagnostics.new.schedule(@coordinator)
}
}
# Data structures for mapping names to symbols.
import std::compiler::typedb::ids::TypeId
import std::index::*
# An identifier, such as a method name, along with its type and other data.
object Symbol {
# The name of the symbol.
@name: String
# The type ID of the symbol.
@type_id: TypeId
# If the symbol is mutable or not.
@mutable: Boolean
def init(name: String, type_id: TypeId, mutable: Boolean) {
@name = name
@type_id = type_id
@mutable = mutable
}
}
# A type used for mapping names (such as method names) to symbols.
object SymbolTable {
# The symbols in the same order as they were defined in.
@symbols: Array!(Symbol)
# The names of the symbols and their corresponding Symbol objects.
@mapping: Map!(String, Symbol)
def init {
@symbols = Array.new
@mapping = Map.new
}
}
impl Index!(String, Symbol) for SymbolTable {
def [](index: String) -> ?Symbol {
@mapping[index]
}
}
impl SetIndex!(String, Symbol) for SymbolTable {
def []=(index: String, value: Symbol) -> Symbol {
@symbols.push(value)
@mapping[index] = value
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment