Prototype my new universe types idea

I had an interesting idea tonight for how to a fully coherent cumulative
hierarchy of universes in an algebraic way. The idea is to have a smallness
*relation* and a shrinking operator that takes a small type and turns it into a
code.

This is algebraic, but even better, it can be interpreted into MLTT + the usual
more complicated cumulative universes with semilattice and successor. The
interpretation sends the smallness relation to the *image* of the universe
decoding map; this image exists in the algebraic sense because the decoding map
is injective.

I have added a notion of general size to factor the code appropriately. The
idea is as follows:

There is a type

Universe

that classifies small universe levels. This type is "very large", not because
of its size but because it won't be fibrant.

A "size" is either

small(U: Universe)

or it is

moderate().

For each size S, there is a type

S.codes

that classifes the S-small types.

For each code

X : S.codes

there is a type

X.decode.

--------------------------------------------

There is an inequality relation on sizes. Every small(U) is strictly lower than
moderate().

Add tests for size inequalities, fix revealed bugs

+1 -1
Sources/PterodactylKernel/Control/AsyncThunk.swift
···
import Foundation
-
actor AsyncThunk<Value> {
+
public actor AsyncThunk<Value> {
private var storage: Value?
private var thunk: (() async -> Value)?
+4 -2
Sources/PterodactylKernel/Core Types/FieldDict.swift
···
import Foundation
-
struct FieldDict<Value> {
+
// TODO: This might be rewritten. The goal is to provide a dictionary that I can easily construct and traverse in order. Maybe ordered dictionaries from swift-collections?
+
public struct FieldDict<Value> {
public let elements: [(String, Value)]
+
var keys: [String] { elements.map(\.0) }
init(_ dict: [(String, Value)]) {
self.elements = dict
···
subscript(key: String) -> Value? {
elements.first { $0.0 == key }?.1
}
-
+
func prefix(until: String) -> Self {
var prefix: [(String, Value)] = []
for (key, value) in elements {
+36
Sources/PterodactylKernel/Core Types/Size.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
/// Pterodactyl has two sizes of small things: little and moderate. Beyond these, we have things that are not types but rather “type schemes”.
+
public enum Size<Universe> {
+
/// A “little” thing is something classified by a universe. Universes form a countable hierarchy.
+
case little(universe: Universe)
+
+
/// A “moderate” type is something classified by `Type`.
+
case moderate
+
}
+
+
+
extension Size: Sendable where Universe: Sendable {}
+
+
extension Size: Equatable where Universe == () {
+
public static func == (lhs: Size<Universe>, rhs: Size<Universe>) -> Bool {
+
switch (lhs, rhs) {
+
case (.little, .little): true
+
case (.moderate, .moderate): true
+
default: false
+
}
+
}
+
}
+
+
extension Size {
+
func map<U>(_ f: (Universe) -> U) -> Size<U> {
+
switch self {
+
case let .little(universe: universe): .little(universe: f(universe))
+
case .moderate: .moderate
+
}
+
}
+
}
+28 -7
Sources/PterodactylKernel/Core Types/Term.swift
···
//
// SPDX-License-Identifier: MPL-2.0
-
typealias Name = String
+
public typealias Name = String
-
indirect enum Term: Sendable {
+
public indirect enum Term: Sendable {
+
+
/// A local variable, designated by a De Bruijn index.
case local(index: Int)
+
+
/// A global variable, designated by a `Name`.
case global(name: Name)
+
+
/// A (dependent) function.
case fun(dom: AsyncThunk<Type_>, boundName: String?, body: Term)
+
+
/// A (dependent) record.
case record(boundName: String?, fields: FieldDict<FieldImpl>)
+
+
/// A negative elimination-form (``Frame``) applied to a term.
case cut(term: Term, frame: Frame)
+
+
/// The canonical code of a small type.
+
case shrink(size: Size<Term>, type: Type_)
+
+
case universeZero
+
case universeSucc(_ universe: Term)
+
case universeLub(_ universe1: Term, _ universe2: Term)
}
extension Term {
-
indirect enum Type_: Sendable {
+
public indirect enum Type_: Sendable {
case funType(dom: Type_, boundName: String?, fam: Type_)
case recordType(boundName: String?, fields: FieldDict<FieldSpec>)
+
case universeCodes(size: Size<Term>)
+
case decode(size: Size<()>, code: Term)
+
+
case universeType
}
-
indirect enum Frame {
+
public indirect enum Frame: Sendable {
case app(arg: Term)
case proj(fieldName: String)
}
-
struct FieldSpec {
+
public struct FieldSpec: Sendable {
let type: Type_
let manifest: Term?
}
-
struct TypedTerm {
+
public struct TypedTerm: Sendable {
let type: Type_
let term: Term
}
-
struct FieldImpl {
+
public struct FieldImpl: Sendable {
let type: Type_
let value: Term
}
+41 -12
Sources/PterodactylKernel/Core Types/Value.swift
···
//
// SPDX-License-Identifier: MPL-2.0
-
struct Closure<Body>{
+
public struct Closure<Body> {
let evaluator: Evaluator
let body: Body
}
extension Closure: Sendable where Body: Sendable {}
-
indirect enum Value: Sendable {
+
public indirect enum Value: Sendable {
case shift(neutral: Neutral)
case fun(dom: AsyncThunk<Type_>, boundName: String?, closure: Closure<Term>)
case record(boundName: String?, fields: FieldDict<FieldImpl>)
+
+
case shrink(size: Size<Value>, type: Type_)
+
+
case universeZero
+
case universeSucc(_ universe: Value)
+
case universeLub(_ universe1: Value, _ universe2: Value)
}
extension Value {
-
indirect enum Type_: Sendable {
+
public indirect enum Type_: Sendable {
case funType(dom: Type_, boundName: String?, fam: Closure<Term.Type_>)
case recordType(boundName: String?, fields: FieldDict<FieldSpec>)
+
+
/// The type of small universe levels.
+
case universeType
+
+
/// The type of `S`-small type codes for a size `S`.
+
case universeCodes(size: Size<Value>)
+
+
/// The decoding of a given `S`-small type code.
+
case decode(size: Size<()>, code: Neutral)
}
-
-
struct FieldSpec {
+
+
public struct FieldSpec: Sendable {
let type: Closure<Term.Type_>
let manifest: Closure<Term>?
}
-
-
struct FieldImpl {
+
+
public struct FieldImpl: Sendable {
let type: Closure<Term.Type_>
let value: Closure<Term>
}
-
indirect enum Frame {
+
// TODO: Kovacs makes the argument in an application frame lazy. Doing this would be a significant change, because it would make it necessary for the plugging operation to be asynchronous.
+
public indirect enum Frame: Sendable {
case app(arg: Value)
case proj(fieldName: String)
}
-
typealias Spine = [Frame]
+
/// When a ``Neutral`` is plugged into a ``Frame``, the frame is enriched with further information before being placed on the spine.
+
public indirect enum RichFrame: Sendable {
+
case app(dom: AsyncThunk<Value.Type_>, arg: Value)
+
case proj(fieldName: String)
+
}
-
enum Head {
+
public typealias Spine = [RichFrame]
+
+
public enum Head: Equatable, Sendable {
case local(level: Int)
case global(name: Name)
}
-
struct Neutral {
+
public struct Neutral: Sendable {
let head: Head
let spine: Spine
let boundary: AsyncThunk<Boundary>
···
}
-
+
extension Value.RichFrame {
+
var forget: Value.Frame {
+
switch self {
+
case let .app(dom: _, arg: arg): .app(arg: arg)
+
case let .proj(fieldName: fieldName): .proj(fieldName: fieldName)
+
}
+
}
+
}
+37 -6
Sources/PterodactylKernel/Evaluator.swift
···
struct Evaluator {
let globals: [Name: Term.TypedTerm]
let locals: [Value]
-
-
init(globals: [Name : Term.TypedTerm], locals: [Value]) {
+
+
public init(globals: [Name: Term.TypedTerm] = [:], locals: [Value] = []) {
self.globals = globals
self.locals = locals
-
}
-
-
init() {
-
self.init(globals: [:], locals: [])
}
func extendedBy(value: Value) -> Self {
···
boundName: boundName,
fields: fields.map(close(fieldImpl:))
)
+
case let .shrink(size: size, type: type):
+
return .shrink(
+
size: size.map(evaluate(term:)),
+
type: evaluate(type: type)
+
)
+
case .universeZero:
+
return .universeZero
+
case let .universeSucc(universe: universe):
+
return .universeSucc(evaluate(term: universe))
+
case let .universeLub(universe1, universe2):
+
return .universeLub(
+
evaluate(term: universe1),
+
evaluate(term: universe2)
+
)
}
}
···
boundName: boundName,
fam: close(body: fam)
)
+
case let .recordType(boundName: boundName, fields: fields):
.recordType(boundName: boundName, fields: fields.map(close(fieldSpec:)))
+
+
case let .decode(size: size, code: code):
+
evaluate(term: code).decode(size: size)
+
+
case let .universeCodes(size: size):
+
.universeCodes(size: size.map(evaluate(term:)))
+
+
case .universeType:
+
.universeType
}
}
}
+
+
extension Value {
+
func decode(size: Size<()>) -> Value.Type_ {
+
switch self {
+
case let .shrink(_, type: type): type
+
case let .shift(neutral: neutral): .decode(size: size, code: neutral)
+
default: fatalError()
+
}
+
}
+
}
+
extension Closure where Body == Term {
func instantiate(with value: Value) -> Value {
+148
Sources/PterodactylKernel/Local Analysis/Equality.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
extension LocalAnalysis {
+
enum EqualityError: Error {
+
case values(lhs: Value, rhs: Value, type: Value.Type_)
+
case types(lhs: Value.Type_, rhs: Value.Type_)
+
case heads(lhs: Value.Head, rhs: Value.Head)
+
case frames(lhs: Value.RichFrame, rhs: Value.RichFrame)
+
case spines(lhs: Value.Spine, rhs: Value.Spine)
+
}
+
+
func equate(value lhs: Value, with rhs: Value, in type: Value.Type_) async throws {
+
switch await (lhs.whnf(), rhs.whnf(), type.whnf()) {
+
case (_, _, .universeType):
+
try await equate(size: .little(universe: lhs), with: .little(universe: rhs))
+
+
case let (lhs, rhs, .funType(dom: dom, boundName: _, fam: fam)):
+
let dom = AsyncThunk(value: dom)
+
let x = fresh(type: dom)
+
let frame = Value.Frame.app(arg: x)
+
let lhs = lhs.plug(frame: frame)
+
let rhs = rhs.plug(frame: frame)
+
let fibre = fam.instantiate(with: x)
+
try await next.equate(value: lhs, with: rhs, in: fibre)
+
+
case let (lhs, rhs, .recordType(boundName: boundName, fields: fields)):
+
var fieldImpls: [(String, Value.FieldImpl)] = []
+
for (key, field) in fields.elements {
+
let prefixRecord: Value = .record(boundName: boundName, fields: FieldDict(fieldImpls))
+
if field.manifest == nil {
+
let frame = Value.Frame.proj(fieldName: key)
+
let lhs = lhs.plug(frame: frame)
+
let rhs = rhs.plug(frame: frame)
+
let type = field.type.instantiate(with: prefixRecord)
+
try await equate(value: lhs, with: rhs, in: type)
+
}
+
+
let impl = Value.FieldImpl(
+
type: field.type,
+
value: Closure(
+
evaluator: Evaluator(),
+
body: .cut(
+
term: .local(index: 0),
+
frame: .proj(fieldName: key)
+
)
+
)
+
)
+
fieldImpls.append((key, impl))
+
}
+
+
case (_, _, .universeCodes):
+
try await equate(type: lhs.decode(size: .little(universe: ())), with: rhs.decode(size: .little(universe: ())))
+
+
case let (.shift(neutral: lhs), .shift(neutral: rhs), .decode):
+
try await equate(neutral: lhs, with: rhs)
+
+
default:
+
throw EqualityError.values(lhs: lhs, rhs: rhs, type: type)
+
}
+
}
+
+
func equate(type lhs: Value.Type_, with rhs: Value.Type_) async throws {
+
switch await (lhs.whnf(), rhs.whnf()) {
+
+
case let (.funType(dom: dom1, boundName: _, fam: fam1), .funType(dom: dom2, boundName: _, fam: fam2)):
+
try await equate(type: dom1, with: dom2)
+
let x = fresh(type: AsyncThunk(value: dom1))
+
let fibre1 = fam1.instantiate(with: x)
+
let fibre2 = fam2.instantiate(with: x)
+
try await next.equate(type: fibre1, with: fibre2)
+
+
case let (.recordType(boundName: boundName, fields: fields1), .recordType(boundName: _, fields: fields2)):
+
let error = EqualityError.types(lhs: lhs, rhs: rhs)
+
guard fields1.keys == fields2.keys else { throw error }
+
+
var fieldSpecs: [(String, Value.FieldSpec)] = []
+
+
for (key, field1) in fields1.elements {
+
let field2 = fields2[key]!
+
guard (field1.manifest == nil) == ((field2.manifest == nil)) else { throw error }
+
+
let prefixType: AsyncThunk<Value.Type_> = AsyncThunk(value: .recordType(boundName: boundName, fields: FieldDict(fieldSpecs)))
+
let x = fresh(type: prefixType)
+
let type1 = field1.type.instantiate(with: x)
+
let type2 = field2.type.instantiate(with: x)
+
try await next.equate(type: type1, with: type2)
+
+
if let manifest1 = field1.manifest, let manifest2 = field2.manifest {
+
let value1 = manifest1.instantiate(with: x)
+
let value2 = manifest2.instantiate(with: x)
+
try await next.equate(value: value1, with: value2, in: type1)
+
}
+
+
fieldSpecs.append((key, field1))
+
}
+
+
case (.universeType, .universeType): ()
+
+
case let (.decode(size: size1, code: neutral1), .decode(size: size2, code: neutral2)):
+
guard size1 == size2 else { throw EqualityError.types(lhs: lhs, rhs: rhs) }
+
try await equate(neutral: neutral1, with: neutral2)
+
+
case let (.universeCodes(size: lhs), .universeCodes(size: rhs)):
+
try await equate(size: lhs, with: rhs)
+
+
default:
+
throw EqualityError.types(lhs: lhs, rhs: rhs)
+
}
+
}
+
+
func equate(head lhs: Value.Head, with rhs: Value.Head) throws {
+
guard lhs == rhs else { throw EqualityError.heads(lhs: lhs, rhs: rhs) }
+
}
+
+
func equate(frame lhs: Value.RichFrame, with rhs: Value.RichFrame) async throws {
+
switch (lhs, rhs) {
+
case let (.proj(fieldName: fieldName1), .proj(fieldName: fieldName2)):
+
guard fieldName1 == fieldName2 else { throw EqualityError.frames(lhs: lhs, rhs: rhs) }
+
case let (.app(dom: dom1, arg: arg1), .app(dom: _, arg: arg2)):
+
try await equate(value: arg1, with: arg2, in: dom1.value())
+
default:
+
throw EqualityError.frames(lhs: lhs, rhs: rhs)
+
}
+
}
+
+
func equate(spine lhs: Value.Spine, with rhs: Value.Spine) async throws {
+
guard lhs.count == rhs.count else { throw EqualityError.spines(lhs: lhs, rhs: rhs) }
+
+
for (index, frame1) in lhs.enumerated() {
+
let frame2 = rhs[index]
+
try await equate(frame: frame1, with: frame2)
+
}
+
}
+
+
func equate(neutral lhs: Value.Neutral, with rhs: Value.Neutral) async throws {
+
try equate(head: lhs.head, with: rhs.head)
+
try await equate(spine: lhs.spine, with: rhs.spine)
+
}
+
+
func equate(size lhs: Size<Value>, with rhs: Size<Value>) async throws {
+
try await assertLeq(size1: lhs, size2: rhs)
+
try await assertLeq(size1: rhs, size2: lhs)
+
}
+
}
+31
Sources/PterodactylKernel/Local Analysis/LocalAnalysis.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
struct LocalAnalysis {
+
let depth: Int
+
+
init(depth: Int = 0) {
+
self.depth = depth
+
}
+
+
var next: Self {
+
Self(depth: depth + 1)
+
}
+
+
func fresh(type: AsyncThunk<Value.Type_>) -> Value {
+
let boundary: AsyncThunk<Value.Boundary> = AsyncThunk {
+
await Value.Boundary(type: type.value(), value: nil)
+
}
+
+
return Value.shift(
+
neutral: Value.Neutral(
+
head: .local(level: depth),
+
spine: [],
+
boundary: boundary
+
)
+
)
+
}
+
}
+33 -13
Sources/PterodactylKernel/Plug.swift
···
extension Plug {
func plug(spine: Value.Spine) -> Self {
spine.reduce(self) { partialResult, frame in
-
partialResult.plug(frame: frame)
+
partialResult.plug(frame: frame.forget)
}
}
}
···
return impl.value.instantiate(with: self)
case let .shift(neutral: neutral):
return .shift(neutral: neutral.plug(frame: frame))
+
default:
+
fatalError()
}
}
}
extension Value.Neutral: Plug {
-
// Not async now, but might need to be later.
func plug(boundary: Value.Boundary, frame: Value.Frame) async -> Value.Boundary {
-
switch boundary.type {
+
switch await boundary.type.whnf() {
+
case let .funType(_, _, fam: fam):
guard case let .app(arg: arg) = frame else {
-
fatalError("Attempted to plug element of function type into invalid frame")
+
fatalError("Attempted to plug boundary of function type into invalid frame")
}
let fibre = fam.instantiate(with: arg)
return Value.Boundary(type: fibre, value: boundary.value?.plug(frame: frame))
-
+
case let .recordType(boundName, fields: fields):
guard case let .proj(fieldName) = frame else {
-
fatalError("Attempted to plug element of record type into invalid frame")
+
fatalError("Attempted to plug boundary of record type into invalid frame")
}
guard let field = fields[fieldName] else {
-
fatalError("Attempted to project invalid field from element of record type")
+
fatalError("Attempted to project invalid field from boundary of record type")
}
let prefix = fields.prefix(until: fieldName)
let prefixImpls = prefix.mapWithKeys { (key, spec) in
Value.FieldImpl(
type: spec.type,
value: Closure(
-
evaluator: Evaluator(globals: [:], locals: [.shift(neutral: self)]),
+
evaluator: Evaluator(),
body: .cut(term: .local(index: 0), frame: .proj(fieldName: key))
)
)
}
let prefixRecord: Value = .record(boundName: boundName, fields: prefixImpls)
let fieldTypeValue = field.type.instantiate(with: prefixRecord)
-
let value = boundary.value?.plug(frame: frame) ?? field.manifest.map { manifest in
-
return manifest.instantiate(with: .shift(neutral: self))
-
}
+
let value =
+
boundary.value?.plug(frame: frame)
+
?? field.manifest.map { $0.instantiate(with: .shift(neutral: self)) }
return Value.Boundary(type: fieldTypeValue, value: value)
+
+
default:
+
fatalError("Boundary has unexpected type for plugging")
+
}
+
}
+
+
func enrich(frame: Value.Frame) -> Value.RichFrame {
+
switch frame {
+
case let .app(arg: arg):
+
let dom: AsyncThunk<Value.Type_> = AsyncThunk {
+
guard case let .funType(dom: dom, _, _) = await boundary.value().type.whnf() else { fatalError() }
+
return dom
+
}
+
return .app(dom: dom, arg: arg)
+
+
case let .proj(fieldName: fieldName):
+
return .proj(fieldName: fieldName)
}
}
func plug(frame: Value.Frame) -> Self {
-
Self(
+
let richFrame = enrich(frame: frame)
+
return Self(
head: head,
-
spine: CollectionOfOne(frame) + spine,
+
spine: spine + CollectionOfOne(richFrame),
boundary: AsyncThunk { await plug(boundary: boundary.value(), frame: frame) }
)
}
+27 -23
Sources/PterodactylKernel/Quotation.swift Sources/PterodactylKernel/Local Analysis/Quotation.swift
···
import Foundation
-
struct Quotation {
-
let depth: Int
-
var next: Self {
-
Quotation(depth: depth + 1)
-
}
-
+
extension LocalAnalysis {
func quote(type: Value.Type_) -> Term.Type_ {
switch type {
+
case .universeType: return .universeType
+
+
case let .universeCodes(size: size):
+
return .universeCodes(size: size.map(quote(value:)))
+
+
case let .decode(size: size, code: code):
+
return .decode(size: size, code: quote(neutral: code))
+
case let .funType(dom: dom, boundName: boundName, fam: fam):
return .funType(
dom: quote(type: dom),
···
case let .shift(neutral: neutral):
return quote(neutral: neutral)
+
case let .shrink(size: size, type: type):
+
return .shrink(size: size.map(quote(value:)), type: quote(type: type))
+
+
case .universeZero:
+
return .universeZero
+
+
case let .universeSucc(universe: universe):
+
return .universeSucc(quote(value: universe))
+
+
case let .universeLub(universe1, universe2):
+
return .universeLub(
+
quote(value: universe1),
+
quote(value: universe2)
+
)
+
case let .fun(dom: dom, boundName: boundName, closure: closure):
return .fun(
dom: AsyncThunk { await quote(type: dom.value()) },
···
}
}
-
func fresh(type: AsyncThunk<Value.Type_>) -> Value {
-
let boundary: AsyncThunk<Value.Boundary> = AsyncThunk {
-
await Value.Boundary(type: type.value(), value: nil)
-
}
-
-
return Value.shift(
-
neutral: Value.Neutral(
-
head: .local(level: depth),
-
spine: [],
-
boundary: boundary
-
)
-
)
-
}
-
func quote(dom: AsyncThunk<Value.Type_>, closure: Closure<Term>) -> Term {
let x = fresh(type: dom)
let value = closure.instantiate(with: x)
···
}
}
-
private func quote(frame: Value.Frame) -> Term.Frame {
+
private func quote(frame: Value.RichFrame) -> Term.Frame {
switch frame {
-
case let .app(arg: arg):
+
case let .app(_, arg: arg):
return .app(arg: quote(value: arg))
case let .proj(fieldName: fieldName):
return .proj(fieldName: fieldName)
}
}
-
private func quote(neutral: Value.Neutral) -> Term {
+
public func quote(neutral: Value.Neutral) -> Term {
let headTerm = quote(head: neutral.head)
return neutral.spine.reduce(headTerm) { term, frame in
.cut(term: term, frame: quote(frame: frame))
+81
Sources/PterodactylKernel/Smallness.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
extension LocalAnalysis {
+
struct SizeError: Error { }
+
+
func assertSize(type: Value.Type_, size: Size<Value>) async throws {
+
switch await type.whnf() {
+
+
// In principle the type of universes could be small, but I am worried about about the strictness of the semilattice operations. I know how to interpret them if universeType is exoNat as in 2LTT, but in that case universeType probably should not be fibrant.
+
case .universeType, .universeCodes(size: .moderate):
+
throw SizeError()
+
+
case let .universeCodes(size: .little(universe: littleUniverse)):
+
try await assertLeq(size1: .little(universe: .universeSucc(littleUniverse)), size2: size)
+
+
case let .decode(size: _, code: code):
+
guard case let .universeCodes(size: littleSize) = await code.boundary.value().type.whnf() else { fatalError() }
+
try await assertLeq(size1: littleSize, size2: size)
+
+
case let .funType(dom: dom, boundName: _, fam: fam):
+
try await assertSize(type: dom, size: size)
+
let domThunk = AsyncThunk(value: dom)
+
let x = fresh(type: domThunk)
+
let fibre = fam.instantiate(with: x)
+
try await next.assertSize(type: fibre, size: size)
+
+
case let .recordType(boundName: boundName, fields: fields):
+
var fieldSpecs: [(String, Value.FieldSpec)] = []
+
for (key, fieldSpec) in fields.elements {
+
if fieldSpec.manifest == nil {
+
let prefixType: AsyncThunk<Value.Type_> = AsyncThunk(value: .recordType(boundName: boundName, fields: FieldDict(fieldSpecs)))
+
let prefixRecord = fresh(type: prefixType)
+
try await next.assertSize(type: fieldSpec.type.instantiate(with: prefixRecord), size: size)
+
}
+
+
fieldSpecs.append((key, fieldSpec))
+
}
+
}
+
}
+
+
func assertSmall(type: Value.Type_, universe: Value) async throws {
+
try await assertSize(type: type, size: .little(universe: universe))
+
}
+
+
func assertLeq(size1: Size<Value>, size2: Size<Value>) async throws {
+
switch (size1, size2) {
+
case (_, .moderate): ()
+
case let (.little(universe: universe1), .little(universe: universe2)):
+
try await assertLeq(universe1: universe1, universe2: universe2)
+
default:
+
throw SizeError()
+
}
+
}
+
+
func assertLeq(universe1: Value, universe2: Value) async throws {
+
switch await (universe1.whnf(), universe2.whnf()) {
+
case (.universeZero, _): ()
+
case let (.universeLub(universe11, universe12), _):
+
try await assertLeq(universe1: universe11, universe2: universe2)
+
try await assertLeq(universe1: universe12, universe2: universe2)
+
case let (.universeSucc(universe: universe1), .universeSucc(universe: universe2)):
+
try await assertLeq(universe1: universe1, universe2: universe2)
+
case let (_, .universeLub(universe21, universe22)):
+
do {
+
try await assertLeq(universe1: universe1, universe2: universe21)
+
} catch {
+
try await assertLeq(universe1: universe1, universe2: universe22)
+
}
+
case let (_, .universeSucc(universe: universe2)):
+
try await assertLeq(universe1: universe1, universe2: universe2)
+
case let (.shift(neutral: neutral1), .shift(neutral: neutral2)):
+
try await equate(neutral: neutral1, with: neutral2)
+
default:
+
throw SizeError()
+
}
+
}
+
}
+13 -1
Sources/PterodactylKernel/Whnf.swift
···
extension Value {
func whnf() async -> Value {
switch self {
-
case .fun, .record : self
+
case .fun, .record, .shrink, .universeZero, .universeSucc, .universeLub: self
case let .shift(neutral: neutral): await neutral.boundary.value().value ?? self
}
}
}
+
+
extension Value.Type_ {
+
func whnf() async -> Self {
+
switch self {
+
case .funType, .recordType, .universeCodes, .universeType: return self
+
case let .decode(size: size, code: neutral):
+
guard let code = await neutral.boundary.value().value else { return self }
+
return code.decode(size: size)
+
}
+
}
+
}
+
-11
Tests/PterodactylKernelTests/PterodactylKernelTests.swift
···
-
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
-
//
-
// SPDX-License-Identifier: MPL-2.0
-
-
import Testing
-
-
@testable import PterodactylKernel
-
-
@Test func example() async throws {
-
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
-
}
+26
Tests/PterodactylKernelTests/Quotation.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Testing
+
+
@testable import PterodactylKernel
+
+
struct QuotationTests {
+
@Test func quotingNeutrals() async throws {
+
let analysis = LocalAnalysis()
+
let recordType1 = Term.Type_.recordType(boundName: nil, fields: FieldDict([("foo", Term.FieldSpec(type: .universeType, manifest: nil))]))
+
let recordType2 = Term.Type_.recordType(boundName: nil, fields: FieldDict([("bar", Term.FieldSpec(type: recordType1, manifest: nil))]))
+
let recordTypeValue = Evaluator().evaluate(type: recordType2)
+
let base = Value.Neutral(head: .global(name: "X"), spine: [], boundary: AsyncThunk { Value.Boundary(type: recordTypeValue, value: nil) })
+
let bar = base.plug(frame: .proj(fieldName: "bar"))
+
let foo = bar.plug(frame: .proj(fieldName: "foo"))
+
let term = analysis.quote(neutral: foo)
+
let result =
+
switch term {
+
case .cut(term: .cut(term: .global(name: "X"), frame: .proj(fieldName: "bar")), frame: .proj(fieldName: "foo")): true
+
default: false
+
}
+
#expect(result, "Neutral incorrectly quoted as: \(term)")
+
}
+
}
+61
Tests/PterodactylKernelTests/Size.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Testing
+
@testable import PterodactylKernel
+
+
struct SizeTests {
+
@Test func leqIsReflexive() async throws {
+
let analysis = LocalAnalysis()
+
+
await #expect(throws: Never.self) {
+
try await analysis.assertLeq(size1: .moderate, size2: .moderate)
+
}
+
}
+
+
@Test func moderateIsNotSmall() async throws {
+
let analysis = LocalAnalysis()
+
+
await #expect(throws: LocalAnalysis.SizeError.self) {
+
try await analysis.assertLeq(size1: .moderate, size2: .little(universe: .universeZero))
+
}
+
}
+
+
@Test func lubIsCommutative() async throws {
+
let analysis = LocalAnalysis()
+
await #expect(throws: Never.self) {
+
let lhs = Value.universeLub(.universeZero, .universeSucc(.universeZero))
+
let rhs = Value.universeLub(.universeSucc(.universeZero), .universeZero)
+
try await analysis.equate(size: .little(universe: lhs), with: .little(universe: rhs))
+
}
+
}
+
+
@Test func lubZeroLeftUnit() async throws {
+
let analysis = LocalAnalysis()
+
await #expect(throws: Never.self) {
+
let lhs = Value.universeLub(.universeZero, .universeSucc(.universeZero))
+
let rhs = Value.universeSucc(.universeZero)
+
try await analysis.equate(size: .little(universe: lhs), with: .little(universe: rhs))
+
}
+
}
+
+
@Test func lubZeroRightUnit() async throws {
+
let analysis = LocalAnalysis()
+
await #expect(throws: Never.self) {
+
let lhs = Value.universeLub(.universeSucc(.universeZero), .universeZero)
+
let rhs = Value.universeSucc(.universeZero)
+
try await analysis.equate(size: .little(universe: lhs), with: .little(universe: rhs))
+
}
+
}
+
+
@Test func succDistributesOverLub() async throws {
+
let analysis = LocalAnalysis()
+
await #expect(throws: Never.self) {
+
let lhs = Value.universeSucc(Value.universeLub(.universeZero, .universeSucc(.universeZero)))
+
let rhs = Value.universeLub(.universeSucc(.universeSucc(.universeZero)), .universeSucc(.universeZero))
+
try await analysis.equate(size: .little(universe: lhs), with: .little(universe: rhs))
+
}
+
}
+
+
}