Skip to content

Instantly share code, notes, and snippets.

@dabrahams
Last active February 28, 2025 19:31
Show Gist options
  • Save dabrahams/15ba20350e5c20b3a77b28d32c729893 to your computer and use it in GitHub Desktop.
Save dabrahams/15ba20350e5c20b3a77b28d32c729893 to your computer and use it in GitHub Desktop.
//
// Un-lowered Hylo definitions, mapped to Swift. These are here to
// to help explain the lowered forms that follow.
//
/// All types
protocol _Type {
static var size: Int { get }
static var alignment: Int { get }
}
extension _Type {
static var size: Int { MemoryLayout<Self>.size }
static var alignment: Int { MemoryLayout<Self>.alignment }
}
/// Types that can be destroyed.
protocol Deinitializable: _Type {
static func deinitialize(_: consuming Self)
}
extension Deinitializable {
static func deinitialize(_: consuming Self) {}
}
/// Types that can be constructed without arguments
protocol New: _Type {
static func new() -> Self
}
/// Types that can be moved in memory.
protocol Movable: _Type {
static func move(_ x: consuming Self) -> Self
}
extension Movable {
static func move(_ x: consuming Self) -> Self { x }
}
/// Types having an infinite chain of associated types, that can produce
/// an instance of the next type in the chain.
protocol P: Movable, New {
associatedtype A: P
func f() -> A
}
/// A generic P that stores an instance of its type parameter.
struct X<T: New&Movable&Deinitializable>: P, Deinitializable {
typealias A = X<Self>
let magic = 12345
let t: T
static func new() -> Self { .init(t: T.new()) }
func f() -> A {
assert(magic == 12345)
return A.new()
}
}
/// A generic P whose associated A type is its type parameter.
struct Y<U: P>: P, Deinitializable {
let magic = 54321
typealias A = U
static func new() -> Self { .init() }
func f() -> A {
assert(magic == 54321)
return A.new()
}
}
//
// Note: Non-requirement-satisfying operations on generic types are
// not shown because they can always be mapped to generic functions.
//
/// Demo generic function 1.
func g<V: Movable&New&Deinitializable>(_: V) {
let a = X<V>.new()
defer { type(of: a).deinitialize(a) }
let b = h(a)
let ignored = h(b)
type(of: b).deinitialize(b)
type(of: ignored).deinitialize(ignored)
}
/// Demo generic function 2.
func h<S: P>(_ b: S) -> S {
let c = Y<S>.new()
defer { type(of: c).deinitialize(c) }
let r = c.f()
return type(of:r).move(r)
}
//
// Utilities for expressing lowerings
//
typealias _Let = UnsafeRawPointer
typealias _Inout = UnsafeMutableRawPointer
typealias _Set = UnsafeMutableRawPointer
typealias _Sink = UnsafeMutableRawPointer
/// Converts a let to a sink when its lifetime has ended.
func sink(_ p: _Let) -> _Sink { .init(mutating: p) }
/// Temporarily allocates memory with the given `size` and
/// `alignment`, and invokes `body` on that memory, returning the
/// result.
func alloca<R>(size: Int, alignment: Int, body: (UnsafeMutableRawPointer)->R) -> R {
var storage = [UInt8](repeating: 0, count: size + alignment)
return storage.withUnsafeMutableBufferPointer {
let offset = alignment - Int((UInt(bitPattern: $0.baseAddress)) % UInt(alignment))
return body($0.baseAddress! + offset)
}
}
//
// Metadata
//
// The key to avoiding the need for a distinct witness table for every
// generic type is to pass a bundle of witnesses—the conformances of
// generic parameters and *their* corresponding metadata—to the
// lowerings of all protocol requirements. For concision, and to allow
// for future expansion, we call these bundles metadata.
//
// In concrete type conformances this metadata can be empty.
//
/// Type erasure for metadata
///
/// Metadata never escapes and downcasts are always forced so we could
/// equally use unsafe pointers.
protocol AnyMetadata {}
//
// Lowerings
//
// Witness tables are expressed here as existential containers of
// types that have no storage. Thus, every conformance corresponds to
// a *singleton* witness table type. They never escape so they could
// equally well be unsafe pointers.
/// Witnesss table for the _Type trait.
protocol _TypeWitness {
/// The metadata format for the conforming (possibly generic) type.
associatedtype Metadata: AnyMetadata
// Non-type-erased entry points. We implement these for specific conformances.
// For convenience and without loss of generality, we have size and
// alignment return Int directly rather than via _Set parameters.
func size(metadata: Metadata)->Int
func alignment(metadata: Metadata)->Int
// Type-erased entry points. Default implementations below
// force-downcast and dispatch to the non-type-erased entry points
// above. This scheme is merely here to help me write witnesses
// correctly manually.
func _size(metadata: AnyMetadata)->Int
func _alignment(metadata: AnyMetadata)->Int
}
/// Forwarding for type-erasure
extension _TypeWitness {
func _size(metadata: AnyMetadata)->Int { size(metadata: metadata as! Metadata) }
func _alignment(metadata: AnyMetadata)->Int { alignment(metadata: metadata as! Metadata) }
}
protocol DeinitializableWitness: _TypeWitness {
func deinitialize(metadata: Metadata, sinking _: _Sink)
func _deinitialize(metadata: AnyMetadata, sinking _: _Sink)
}
extension DeinitializableWitness {
func _deinitialize(metadata: AnyMetadata, sinking x: _Sink) {
deinitialize(metadata: metadata as! Metadata, sinking: x)
}
}
protocol MovableWitness: _TypeWitness {
func move(metadata: Metadata, from source: _Sink, to target: _Set)
func _move(metadata: AnyMetadata, from source: _Sink, to target: _Set)
}
extension MovableWitness {
func _move(metadata: AnyMetadata, from source: _Sink, to target: _Set) {
move(metadata: metadata as! Metadata, from: source, to: target)
}
// For this witness we have a default implementation for the
// non-type-erased implementation.
func move(metadata: Metadata, from source: _Sink, to target: _Set) {
target.copyMemory(from: source, byteCount: size(metadata: metadata))
}
}
protocol NewWitness: _TypeWitness {
func new(metadata: Metadata, into target: _Set)
func _new(metadata: AnyMetadata, into target: _Set)
}
extension NewWitness {
func _new(metadata: AnyMetadata, into target: _Set) {
new(metadata: metadata as! Metadata, into: target)
}
}
protocol PWitness: MovableWitness, NewWitness {
func f(metadata: Metadata, _self: _Let, result: _Set)
func _f(metadata: AnyMetadata, _self: _Let, result: _Set)
}
extension PWitness {
func _f(metadata: AnyMetadata, _self: _Let, result: _Set) {
f(metadata: metadata as! Metadata, _self: _self, result: result)
}
}
//
// Technically, we should have distinct witnesses for Deinitializable
// and P. We could achieve it by factoring _TypeWitness conformance
// into a X_is_Type protocol with default implementations, then have
// X_is_P and X_is_Deinitializable conform to that. Taking a shortcut,
// though, and bundling these conformances into X_is_P.
//
/// Singleton witness type for all Xs.
struct X_is_P: _TypeWitness {
// We have just one generic parameter; We use a tuple with an empty
// 2nd element just so we can get labeled member access.
struct Metadata: AnyMetadata {
let wt: any NewWitness&MovableWitness&DeinitializableWitness
let mt: any AnyMetadata
}
func size(metadata: Metadata)->Int {
metadata.wt._size(metadata: metadata.mt) + MemoryLayout<Int>.size
}
func alignment(metadata: Metadata)->Int {
max(metadata.wt._alignment(metadata: metadata.mt), MemoryLayout<Int>.alignment)
}
}
extension X_is_P: DeinitializableWitness {
func deinitialize(metadata: Metadata, sinking x: _Sink) {
assert(x.assumingMemoryBound(to: Int.self).pointee == 12345)
metadata.wt._deinitialize(metadata: metadata.mt, sinking: x.advanced(by: MemoryLayout<Int>.size))
}
}
extension X_is_P: MovableWitness {
func move(metadata: Metadata, from source: _Sink, to target: _Set) {
assert(source.assumingMemoryBound(to: Int.self).pointee == 12345)
// move the initial Int.
target.assumingMemoryBound(to: Int.self).moveInitialize(from: source.assumingMemoryBound(to: Int.self), count: 1)
metadata.wt._move(metadata: metadata.mt, from: source.advanced(by: MemoryLayout<Int>.size), to: target.advanced(by: MemoryLayout<Int>.size))
}
}
extension X_is_P: NewWitness {
func new(metadata: Metadata, into target: _Set) {
target.assumingMemoryBound(to: Int.self).initialize(to: 12345)
metadata.wt._new(metadata: metadata.mt, into: target.advanced(by: MemoryLayout<Int>.size))
assert(target.assumingMemoryBound(to: Int.self).pointee == 12345)
}
}
extension X_is_P : PWitness {
func f(metadata: Metadata, _self: _Let, result: _Set) {
assert(_self.assumingMemoryBound(to: Int.self).pointee == 12345)
metadata.wt._new(metadata: metadata.mt, into: result)
}
}
struct Y_is_P: _TypeWitness {
struct Metadata: AnyMetadata {
let wu: any PWitness
let mu: any AnyMetadata
}
func size(metadata: Metadata)->Int { MemoryLayout<Int>.size }
func alignment(metadata: Metadata)->Int { MemoryLayout<Int>.alignment }
}
extension Y_is_P: DeinitializableWitness {
func deinitialize(metadata: Metadata, sinking x: _Sink) {
assert(x.assumingMemoryBound(to: Int.self).pointee == 54321)
}
}
extension Y_is_P: MovableWitness {
func move(metadata: Metadata, from source: _Sink, to target: _Set) {
assert(source.assumingMemoryBound(to: Int.self).pointee == 54321)
target.assumingMemoryBound(to: Int.self).moveInitialize(from: source.assumingMemoryBound(to: Int.self), count: 1)
}
}
extension Y_is_P: NewWitness {
func new(metadata: Metadata, into target: _Set) {
target.assumingMemoryBound(to: Int.self).initialize(to: 54321)
}
}
extension Y_is_P : PWitness {
func f(metadata: Metadata, _self: _Let, result: _Set) {
assert(_self.assumingMemoryBound(to: Int.self).pointee == 54321)
metadata.wu._new(metadata: metadata.mu, into: result)
}
}
func gLowered(wv: any MovableWitness&NewWitness&DeinitializableWitness, mv: AnyMetadata, _: _Let) {
let mx = X_is_P.Metadata(wt: wv, mt: mv)
let wx = X_is_P()
alloca(size: wx.size(metadata: mx), alignment: wx.alignment(metadata: mx)) { a in
wx.new(metadata: mx, into: a)
alloca(size: wx.size(metadata: mx), alignment: wx.alignment(metadata: mx)) { b in
hLowered(ws: wx, ms: mx, b: a, result: b)
let wxx = X_is_P()
let mxx = X_is_P.Metadata(wt: wx, mt: mx)
alloca(size: wxx.size(metadata: mxx), alignment: wxx.alignment(metadata: mxx)) { ignored in
hLowered(ws: wxx, ms: mxx, b: b, result: ignored)
wxx.deinitialize(metadata: mxx, sinking: ignored)
}
wx.deinitialize(metadata: mx, sinking: b)
}
wx.deinitialize(metadata: mx, sinking: a)
}
}
func hLowered(ws: any PWitness, ms: AnyMetadata, b: _Let, result: _Set) {
let my = Y_is_P.Metadata(wu: ws, mu: ms)
let wy = Y_is_P()
alloca(size: wy.size(metadata: my), alignment: wy.alignment(metadata: my)) { c in
wy.new(metadata: my, into: c)
// We'd actually return directly into our result, but for
// illustration purposes, we'll use a move
//
// wy.f(metadata: my, _self: c, result: result)
alloca(size: ws._size(metadata: ms), alignment: ws._alignment(metadata: ms)) { r in
wy.f(metadata: my, _self: c, result: r)
ws._move(metadata: ms, from: r, to: result)
}
wy.deinitialize(metadata: my, sinking: c)
}
}
//---------- Demo --------
var trackedCount = 0
final class Tracked: Movable, New, Deinitializable {
let magic = 0xBADF00D
let id: Int
init() {
id = trackedCount
trackedCount += 1
print("Tracked.init, \(id)")
}
deinit {
assert(magic == 0xBADF00D)
print("Tracked.deinit, \(id)")
}
static func new() -> Self { .init() }
static func move(_ x: consuming Tracked) -> Tracked {
assert(x.magic == 0xBADF00D)
print("Tracked.move, \(x.id)")
return x
}
}
struct TrackedWitness: MovableWitness, NewWitness, DeinitializableWitness {
struct Metadata: AnyMetadata {}
func new(metadata: Metadata, into target: _Set) {
target.withMemoryRebound(to: Tracked.self, capacity: 1) { $0.initialize(to: .init())}
}
func deinitialize(metadata: Metadata, sinking target: _Sink) {
_ = target.withMemoryRebound(to: Tracked.self, capacity: 1) { $0.deinitialize(count: 1) }
}
func size(metadata: Metadata) -> Int {
MemoryLayout<Tracked>.size
}
func alignment(metadata: Metadata) -> Int {
MemoryLayout<Tracked>.alignment
}
func move(metadata: Metadata, from source: _Sink, to target: _Set) {
assert(source.assumingMemoryBound(to: Tracked.self).pointee.magic == 0xBADF00D)
print("Tracked move, \(source.assumingMemoryBound(to: Tracked.self).pointee.id)")
target.assumingMemoryBound(to: Tracked.self).moveInitialize(from: source.assumingMemoryBound(to: Tracked.self), count: 1)
}
}
func test() {
let t = Tracked()
g(t)
withUnsafePointer(to: t) {
gLowered(wv: TrackedWitness(), mv: TrackedWitness.Metadata(), $0)
}
}
test()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment