Skip to content

Commit

Permalink
Merge branch 'main' of github.com:hylo-lang/hylo into portable-build-…
Browse files Browse the repository at this point in the history
…tool
  • Loading branch information
dabrahams committed Oct 11, 2023
2 parents 15344fa + 7b73654 commit 3c5d1e4
Show file tree
Hide file tree
Showing 85 changed files with 2,524 additions and 492 deletions.
7 changes: 0 additions & 7 deletions .github/workflows/doc-extraction.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ jobs:
steps:
- uses: actions/checkout@v3

- uses: actions/cache@v3
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Setup swift
uses: swift-actions/setup-swift@v1
with:
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/markdown-link-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: Check Markdown links

on: [pull_request, push]

jobs:
markdown-link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: gaurav-nelson/github-action-markdown-link-check@v1
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ We greatly appreciate it if you can isolate the problem and provide a minimal re

## Contributing code

Please get familiar with our [CONVENTIONS.md](project conventions).
Please get familiar with our [project conventions](CONVENTIONS.md). See
[CompilerArchitecture.md] for an overview on the design of the compiler.
We use the standard GitHub workflow to merge code contributions:

1. Fork this repository.
Expand Down Expand Up @@ -65,3 +66,4 @@ Do not hesitate to reach out if you are lost in the process.
[typos-action]: https://github.com/marketplace/actions/typos-action
[install typos]: https://github.com/crate-ci/typos#install
[typos false positives documentation]: https://github.com/crate-ci/typos#false-positives
[CompilerArchitecture.md]: Docs/CompilerArchitecture.md
100 changes: 100 additions & 0 deletions Docs/CompilerArchitecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Compiler Architecture

## Project Overview

The hylo compiler is written in [Swift] and uses [LLVM] as it's code generation
backend. It conforms to the the standard swift project layout:
* [Package.swift] the manifest file used by SPM
* [Sources] all the source code for the compiler
* [Tests] all the test code

Then there are some extra directories specific to the hylo project:
* [Tools] Scripts to aid in development
* [Examples] Some real world hylo programs
* [Library] The hylo standard (and core) library

## Stages of compilation

The hylo compiler goes through the standard stages of compilation:

1. Tokenisation: Transforms hylo source code (Strings) to stream of distinct
tokens
1. Parsing: Creates an [abstract syntax tree] from the token stream
1. Type-checking: Inspects the abstract syntax tree for type errors
1. IR-lowering: Generates the [intermediate representation] from the abstract
syntax tree
1. LLVM IR generation: Convert hylo IR into [LLVM] IR
1. Machine Code Generation: This is completely handled by [LLVM]

These top-level stages of the compiler are laid out in [Driver] where you
can see the outline of the compilation phases with their entry points.
Depending on the flags passed to the compiler, the compiler can exit early at
some of these stages.

### Interesting parts

Most of the compiler does what you'd expect from the compiltion stages above
but some are worth a deeper look:

#### Abstract syntax tree

The abstract syntax tree is made up of an **append-only** array that produces
`NodeID` objects as indices into the array. The `NodeID` allows nodes to refer
to other nodes using their `NodeID`. `NodeID` is generic over node types and
allows us to constrain which nodes are allowed as leaves of other nodes.

The use of `NodeID` types as indices into an array allows us to define the
existence of a node, by it's `NodeID`, without providing access to the node.
For access you still need the array. This is in contrast to traditional
references that provide existence AND access without allowing separation.

The use of `NodeID` types are ubiquitous and is often aliased to `.ID` of a
new type (e.g., `FunctionDecl.ID`).

#### Program Protocol

After the AST is created the compiler creates property maps that associate
properties to the nodes of the AST. Currently there are two distinct phases of
property creation for these property maps. The first is creating the
connections between scopes and nodes, stored in the `ScopedProgram` struct. The
second is where the majority of the type-checking happens, associating a type
for each expression, declaration etc. Each of these stages is composed of the
previous stage:

[AST] < [ScopedProgram] < [TypedProgram] < [IR/Program]

A successfully created `TypedProgram` means the hylo program is well typed.

#### Hylo IR

The Hylo IR is composed of instructions defined in the [Instruction] module.
The [Emitter] is the component responsible for creating the `IR` an inserting
it into the [IR/Module], module-by-module and creating an [IR/Program].

The hylo IR is only valid after it has gone through some mandatory passes
defined in `Module+*` files of [IR/Analysis]. After these passes the IR should
be valid and executable by a *theortical* hylo VM. Some [more passes] may be
necessary dependent on the target.

[Swift]: https://en.wikipedia.org/wiki/Swift_(programming_language)
[LLVM]: https://en.wikipedia.org/wiki/LLVM
[SPM]: https://www.swift.org/package-manager/
[intermediate representation]: https://en.wikipedia.org/wiki/Intermediate_representation
[abstract syntax tree]: https://en.wikipedia.org/wiki/Abstract_syntax_tree

[Driver]: ../Sources/Driver/Driver.swift
[Package.swift]: ../Package.swift
[Sources]: ../Sources
[Tests]: ../Tests
[Tools]: ../Tools
[Examples]: ../Examples
[Library]: ../Library
[AST]: ../Sources/Core/AST/AST.swift
[ScopedProgram]: ../Sources/Core/ScopedProgram.swift
[TypedProgram]: ../Sources/FrontEnd/TypedProgram.swift
[Instruction]: ../Sources/IR/Operands/Instruction/
[Emitter]: ../Sources/IR/Emitter.swift
[IR/Module]: ../Sources/IR/Module.swift
[IR/Program]: ../Sources/IR/Program.swift
[IR/Analysis]: ../Sources/IR/Analysis/
[more passes]: ../Sources/IR/Analysis/Module+Depolymorphize.swift
62 changes: 53 additions & 9 deletions Library/Hylo/Array.hylo
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public type Array<Element: Movable & Deinitializable>: Deinitializable {
/// The header of the buffer indicates the number of elements contained in the array.
var storage: DynamicBuffer<Int, Element>

/// Creates a new, empty array.
/// Creates an empty array.
public init() {
&storage = .new()
}
Expand All @@ -15,22 +15,27 @@ public type Array<Element: Movable & Deinitializable>: Deinitializable {
public fun deinit() sink {
var i = 0
while i < count() {
&pointer_to_element(at: i).unsafe_pointee().deinit()
&pointer_to_element[at: i].unsafe_pointee().deinit()
&i += 1
}
}

/// The number of elements in the array.
/// Returns the number of elements in `self`.
public fun count() -> Int {
if storage.capacity() == 0 { 0 } else { storage.header.copy() }
}

/// Returns `true` if `self` is empty.
public fun is_empty() -> Bool {
count() == 0
}

/// The number of elements that can be stored in the array before new storage must be allocated.
public fun capacity() -> Int {
return storage.capacity()
}

/// Reserves enough space to store `n` elements
/// Reserves enough space to store `n` elements in `self`.
public fun reserve_capacity(_ n: Int) inout {
if n < capacity() { return }

Expand Down Expand Up @@ -60,31 +65,70 @@ public type Array<Element: Movable & Deinitializable>: Deinitializable {
&storage = new_storage
}

/// Projects a pointer to the start of the array's contiguous storage.
///
/// The projected pointer is valid only for the duration of the projection and can be advanced up
/// to `count()`. It may be null if `self` is empty.
public property contiguous_storage: Pointer<Element> {
yield if capacity() == 0 { .null() } else { .new(pointer_to_element[at: 0]) }
}

/// Calls `action` with a pointer to the start of the array's mutable contiguous storage.
///
/// The projected pointer is valid only for the duration of the projection and can be advanced up
/// to `count()`. It may be null if `self` is empty.
public fun with_mutable_contiguous_storage<E, T>(
_ action: inout [E](PointerToMutable<Element>) inout -> T
) inout -> T {
if capacity() == 0 { &action(.null()) } else { &action(pointer_to_element[at: 0]) }
}

/// Adds a new element at the end of the array.
public fun append(_ source: sink Element) inout {
&reserve_capacity(count() + 1)
pointer_to_element(at: count()).unsafe_initialize_pointee(source)
pointer_to_element[at: count()].unsafe_initialize_pointee(source)
&storage.header += 1
}

/// Exchanges the values at the given positions in `self`.
public fun swap_at(_ i: Int, _ j: Int) inout {
// precondition(i >= 0 && i < count())
// precondition(j >= 0 && j < count())
if i == j { return }
swap(&pointer_to_element[at: i].unsafe[], &pointer_to_element[at: j].unsafe[])
}

/// Reverses the elements of `self` in place.
///
/// - Complexity: O(n), where n is the number of elements in `self`.
public fun reverse() inout {
var i = count() - 1
var j = 0
while i > j {
swap_at(i, j)
&i -= 1
&j += 1
}
}

/// Accesses the element at `position`.
///
/// - Requires: `position` is in the range `0 ..< count()`.
public subscript(_ position: Int): Element {
let {
// precondition(position >= 0 && position < count())
pointer_to_element(at: position).unsafe[]
pointer_to_element[at: position].unsafe[]
}
inout {
// precondition(position >= 0 && position < count())
pointer_to_element(at: position).unsafe[]
pointer_to_element[at: position].unsafe[]
}
}

/// Returns the address of the element at `position`.
/// Projects the address of the element at `position`.
///
/// - Requires: `position` is in the range `0 ..< capacity()`.
fun pointer_to_element(at position: Int) -> PointerToMutable<Element> {
subscript pointer_to_element(at position: Int): PointerToMutable<Element> {
storage.first_element_address().advance(by: position)
}

Expand Down
8 changes: 7 additions & 1 deletion Library/Hylo/Core/Bitcast.hylo
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ public subscript unsafe_bitcast<T, U>(_ value: T): U {
yield p.unsafe[]
}
inout {
sink let p: PointerToMutable<U> = PointerToMutable(type_punning: pointerToMutable[&value])
sink let p: PointerToMutable<U> = PointerToMutable(type_punning: mutable_pointer[to: &value])
yield &p.unsafe[]
}
}

/// Returns `value` with its memory representation reinterpreted as a value of type `U`.
public fun unsafe_bitcast<T, U: Movable>(consuming value: T) -> U {
sink let p: PointerToMutable<U> = PointerToMutable(type_punning: mutable_pointer[to: &value])
return p.unsafe_pointee()
}
10 changes: 10 additions & 0 deletions Library/Hylo/Core/Bool.hylo
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ public type Bool {
Bool(value: Builtin.icmp_eq_i1(value, Builtin.zeroinitializer_i1()))
}

/// Returns the logical conjunction of `self` and `other`.
public fun infix&& (_ rhs: Bool) -> Bool {
Bool(value: Builtin.and_i1(self.value, rhs.value))
}

/// Returns the logical disjunction of `self` and `other`.
public fun infix|| (_ rhs: Bool) -> Bool {
Bool(value: Builtin.or_i1(self.value, rhs.value))
}

/// Returns `true` if `self` is equal to `other`. Otherwise, returns `false`.
public fun infix== (_ other: Self) -> Bool {
Bool(value: Builtin.icmp_eq_i1(value, other.value))
Expand Down
25 changes: 25 additions & 0 deletions Library/Hylo/Core/Collection.hylo
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// A collection of elements accessible via an indexed subscript.
public trait Collection {

/// The type of the elements contained in `Self`.
type Element

/// The type of the positions in `Self`.
type Index: SemiRegular

/// Returns the position of the first element in `self`, or `end_index()` if `self` is empty.
fun start_index() -> Index

/// Returns the "past the end" position in `self`, that is, the position immediately after the
/// last element in `self`.
fun end_index() -> Index

/// Returns the position immediately after `i`.
///
/// - Requires: `i != end_index()`.
fun index(after i: Index) -> Index

/// Accesses the elment at position `i`.
subscript(_ i: Index): Element { let }

}
28 changes: 28 additions & 0 deletions Library/Hylo/Core/CollectionOfOne.hylo
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// A collection containing a single element.
public type CollectionOfOne<Element: Movable & Deinitializable>: Deinitializable {

public typealias Index = Bool

/// The element contained in `self`.
var contents: Element

/// Creates a collection containing just `contents`.
public init(_ contents: sink Element) {
&self.contents = contents
}

public fun start_index() -> Bool { false }

public fun end_index() -> Bool { true }

public fun index(after i: Bool) -> Bool { true }

public subscript(_ position: Bool): Element {
let {
// TODO: uncomment when #1046 is implemented
// precondition(!position, "index is out of bounds")
yield contents
}
}

}
32 changes: 0 additions & 32 deletions Library/Hylo/Core/Int8.hylo

This file was deleted.

18 changes: 18 additions & 0 deletions Library/Hylo/Core/Movable.hylo
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,21 @@ public trait Movable {
}

}

public extension Movable {

/// Exchanges the value of `self` with that of `other`.
public fun exchange(with other: inout Self) inout {
sink let x = self
&self = other
&other = x
}

}

// TODO: Remove in favor of `Movable.exchange(with:)` when #1064 is fixed
public fun swap<T: Movable>(_ a: inout T, _ b: inout T) {
sink let x = a
&a = b
&b = x
}
Loading

0 comments on commit 3c5d1e4

Please sign in to comment.