Skip to content
academia.sh

Lesson 01 / 12

Platform Layers

Measuring how eight meetings of two definitions fall into four resolutions, and ordering the platform layers from the core operating system to the interface layer.

Published Updated

In this lesson

  • Classifies the meeting of two definitions as precedence, intersection, union or rejection
  • Measures eight meetings and reads the 2/2/2/2 distribution of the resolutions
  • Orders the platform layers from the core operating system to the interface layer
  • Measures why two nested definitions fall into none of the resolutions
Contents

The previous course read the Swift language through a moment measure: is an expression evaluated immediately, deferred, or never. This course moves from the language to the platform. On the platform the same thing is often defined in two places: a build setting lives in both the project and the target, the screen orientations of an app live in both the app and the device, and the version range of a package lives in both the package itself and the packages that request it. What the system does when these two definitions meet is the measuring question of the course: how are these two definitions resolved when they meet? The umbrella name is resolution.

Four shapes are expected, and at first glance they all look the same. Sometimes one of the two definitions wins (precedence), sometimes only what is in both remains (intersection), sometimes everything that is in either one is collected (union), and sometimes no result comes out at all (rejection). This lesson builds eight small meetings and counts which one falls into which resolution. The platform layers are ordered after that count, to show where the resolutions live.

The Core

Every lesson uses the same helper: a type that holds two definitions, the result that comes out of their meeting, and the resolution the result falls into. Meeting does not change the result, it only classifies it. The result always comes from the model; that is, it is the output of real operations such as dictionary merging, filtering, name lookup and range checks, not a hand-written set. The classification order is as follows: if neither set is empty and neither is a subset of the other, the measurement is valid, otherwise out-of-scope is written; if there is no result or it is empty, rejection; if the result equals the union, union; if it equals the intersection, intersection; if it equals one of the sets, precedence; if it fits none of these, off-rule. Set values are printed sorted, because the iteration order of a Set can change from run to run.

// Meeting.swift — classifies how two definitions are resolved when they meet
struct Meeting {
    let label: String
    let first: Set<String>
    let second: Set<String>
    let result: Set<String>?

    init(_ label: String, first: Set<String>, second: Set<String>, result: Set<String>?) {
        self.label = label
        self.first = first
        self.second = second
        self.result = result
    }

    var resolution: String {
        if first.isEmpty || second.isEmpty
            || first.isSubset(of: second) || second.isSubset(of: first) {
            return "out-of-scope"
        }
        guard let result, !result.isEmpty else { return "rejection" }
        if result == first.union(second) { return "union" }
        if result == first.intersection(second) { return "intersection" }
        if result == first || result == second { return "precedence" }
        return "off-rule"
    }

    static func list(_ values: Set<String>) -> String {
        values.isEmpty ? "empty" : values.sorted().joined(separator: ",")
    }

    func report() {
        let outcome = result.map(Meeting.list) ?? "none"
        print("\(label): first \(Meeting.list(first)) | second \(Meeting.list(second)) | result \(outcome) -> \(resolution)")
    }
}

func summarize(_ meetings: [Meeting]) {
    for resolution in ["precedence", "intersection", "union", "rejection"] {
        let count = meetings.filter { $0.resolution == resolution }.count
        print("\(resolution): \(count)")
    }
}

Eight Meetings

The measurement below builds eight meetings. The two sides and the result of each are produced in the same form. In the build setting, the target level is written over the project level. In name lookup, the local definition is searched first, and the same name from two imported modules stays ambiguous. In screen orientation, what the app supports is filtered by what the device offers. In the version range, the ranges requested by two packages intersect. In the inherited paths, the target expands the project value with $(inherited). In the property list, the template file and the keys generated from the settings are merged.

// main.swift — eight meetings, four resolutions
func parts(_ version: String) -> [Int] {
    version.split(separator: ".").map { Int($0)! }
}

func versions(in published: [String], from lower: String, below upper: String) -> Set<String> {
    Set(published.filter {
        !parts($0).lexicographicallyPrecedes(parts(lower))
            && parts($0).lexicographicallyPrecedes(parts(upper))
    })
}

func lookup(_ name: String, local: [String: String], imported: [[String: String]]) -> Set<String>? {
    if let found = local[name] { return [found] }
    let matches = imported.compactMap { $0[name] }
    return matches.count == 1 ? Set(matches) : nil
}

func expand(_ values: [String], inherited: [String]) -> [String] {
    values.flatMap { $0 == "$(inherited)" ? inherited : [$0] }
}

// Build setting: the target level overrides the project level
let projectLevel = ["SWIFT_VERSION": "5", "TARGETED_DEVICE_FAMILY": "1"]
let targetLevel = ["SWIFT_VERSION": "6"]
let effective = projectLevel.merging(targetLevel) { _, fromTarget in fromTarget }

// Name lookup: the local definition is searched first; the same name in two modules is ambiguous
let kitNames = ["format": "Kit.format", "Logger": "Kit.Logger"]
let toolsNames = ["Logger": "Tools.Logger"]
let localNames = ["format": "local.format"]

// Screen orientation: what the app supports and what the device offers
let appOrientations: Set = ["portrait", "portrait-upside-down"]
let deviceOrientations: Set = ["portrait", "landscape-left", "landscape-right"]

// Version ranges: what two packages request for the same dependency
let published = ["1.2.0", "1.3.0", "1.4.0", "2.0.0", "2.1.0"]
let wideFirst = versions(in: published, from: "1.2.0", below: "2.0.0")
let wideSecond = versions(in: published, from: "1.3.0", below: "3.0.0")
let narrowFirst = versions(in: published, from: "1.2.0", below: "1.4.0")
let narrowSecond = versions(in: published, from: "2.0.0", below: "3.0.0")

// Inherited paths: the target expands the project value with `$(inherited)`
let projectPaths = ["Shared/Headers"]
let targetPaths = ["$(inherited)", "Target/Headers"]

// Info.plist: the template file and the keys generated from the settings
let templateKeys = ["CFBundleName": "Sample", "UILaunchScreen": "-"]
let generatedKeys = ["CFBundleName": "Sample", "NSCameraUsageDescription": "Camera"]

let meetings = [
    Meeting("build-setting",
            first: [projectLevel["SWIFT_VERSION"]!],
            second: [targetLevel["SWIFT_VERSION"]!],
            result: [effective["SWIFT_VERSION"]!]),
    Meeting("local-shadowing",
            first: [kitNames["format"]!],
            second: [localNames["format"]!],
            result: lookup("format", local: localNames, imported: [kitNames, toolsNames])),
    Meeting("screen-orientation",
            first: appOrientations,
            second: deviceOrientations,
            result: appOrientations.filter { deviceOrientations.contains($0) }),
    Meeting("version-range",
            first: wideFirst,
            second: wideSecond,
            result: wideFirst.intersection(wideSecond)),
    Meeting("inherited-paths",
            first: Set(projectPaths),
            second: Set(targetPaths.filter { $0 != "$(inherited)" }),
            result: Set(expand(targetPaths, inherited: projectPaths))),
    Meeting("plist-keys",
            first: Set(templateKeys.keys),
            second: Set(generatedKeys.keys),
            result: Set(templateKeys.merging(generatedKeys) { current, _ in current }.keys)),
    Meeting("duplicate-name",
            first: [kitNames["Logger"]!],
            second: [toolsNames["Logger"]!],
            result: lookup("Logger", local: localNames, imported: [kitNames, toolsNames])),
    Meeting("disjoint-range",
            first: narrowFirst,
            second: narrowSecond,
            result: narrowFirst.intersection(narrowSecond)),
]

for meeting in meetings {
    meeting.report()
}
summarize(meetings)
build-setting: first 5 | second 6 | result 6 -> precedence
local-shadowing: first Kit.format | second local.format | result local.format -> precedence
screen-orientation: first portrait,portrait-upside-down | second landscape-left,landscape-right,portrait | result portrait -> intersection
version-range: first 1.2.0,1.3.0,1.4.0 | second 1.3.0,1.4.0,2.0.0,2.1.0 | result 1.3.0,1.4.0 -> intersection
inherited-paths: first Shared/Headers | second Target/Headers | result Shared/Headers,Target/Headers -> union
plist-keys: first CFBundleName,UILaunchScreen | second CFBundleName,NSCameraUsageDescription | result CFBundleName,NSCameraUsageDescription,UILaunchScreen -> union
duplicate-name: first Kit.Logger | second Tools.Logger | result none -> rejection
disjoint-range: first 1.2.0,1.3.0 | second 2.0.0,2.1.0 | result empty -> rejection
precedence: 2
intersection: 2
union: 2
rejection: 2

The last four lines of the output give the 2/2/2/2 count: two precedence, two intersection, two union, two rejection. build-setting and local-shadowing are precedence: the value of the second side passed into the result as it was. screen-orientation and version-range are intersection: only the values contained in both sides remained in the result. inherited-paths and plist-keys are union: the result carries everything from both sets. duplicate-name and disjoint-range are rejection: in one, no result was produced at all (none), in the other an empty set was produced (empty), and the rule puts both into the same resolution.

The even distribution is a product of the meetings that were built, not of the rule: we chose the eight examples ourselves, and two fell into each of the four resolutions. For this reason we draw no conclusion from the distribution; the measurement only shows that all four resolutions really exist and that each comes from a different operation.

PS1. This measurement is for the eight meetings above and the rules of the model that was written. It was not measured whether the build setting resolution of a real development environment or the version resolution of a real package manager applies these rules exactly; the model builds this rule of the platform and classifies it.

Layers and Resolutions

The platform is read from bottom to top as four layers, and each layer is built on the layers below it. At the bottom is the core operating system layer: memory, processes, networking and device drivers are here. Above it comes the core services layer: the services that every app needs but that are not visible, such as collections, date and file work, network clients and logging. The third layer is the media layer: graphics, audio and video. At the top is the interface layer: views, event handling and everything the app keeps alive on the screen. An app speaks from the interface layer, but it takes services from all three layers beneath it as well.

The resolutions are not distributed evenly over these layers; the same pair of definitions appears in different forms in different layers. The precedence and rejection resolutions of name lookup are the work of the language and the modules; the screen orientation looks at the interface layer and the device, and therefore at the hardware below; the build setting, the inherited paths and the property list belong to how the app is built, so they live in no runtime layer and live in the project and target definitions of the development environment. The version range belongs to the declaration of the package and is resolved at build time.

This ordering has a practical consequence: knowing which layer a definition sits in tells who resolves it, that is, whether the compiler, the package manager or the system decides. In the following lessons we meet every meeting with this question.

PS2. The number and names of the layers are a sequence reduced to four here for teaching purposes; exactly which layer a framework belongs to depends on the internal structure of the framework, which is not measured in this lesson, and it was not measured.

Who Does the Resolving

Grouping the eight meetings by the party that does the resolving makes the layer question concrete. The compiler does the name lookup: it decides which definition will be visible while a source file is being compiled, and the decision is never carried to run time. The build system resolves the build setting, the inherited paths and the property list; these three are found not inside the app but inside the process that produces the app. The package manager resolves the version range and writes the result to a lock file. The system resolves the screen orientation at run time: the app declares what it supports, the device knows what it offers, and the result appears only when the two meet.

This distinction tells two things in advance. First, when the party that resolves changes, the error also shows up in a different place: a rejection given by the compiler is a build error, while a rejection given by the system is noticed while the app is running. Second, the same pair of definitions can be resolved differently in two different places; knowing how a setting meets in the project and the target does not tell how a dependency is requested from two packages.

The Bounding Measurement

The four resolutions do not separate when one of the two definitions contains the other: two nested sets are equal to the outer one in union, to the inner one in intersection, to one of the two in precedence; for rejection there is no conflicting element, because every element of the inner set is also in the outer one. Even though the results differ, in this case the classification says nothing. The measurement below builds all four results from the same nested pair and shows that Meeting leaves all four out of scope.

// main.swift — two nested definitions: all four results fall into the same class
let outer: Set = ["Shared/Headers", "Target/Headers"]
let inner: Set = ["Shared/Headers"]

let nested = [
    Meeting("nested-precedence", first: inner, second: outer, result: inner),
    Meeting("nested-intersection", first: inner, second: outer, result: inner.intersection(outer)),
    Meeting("nested-union", first: inner, second: outer, result: inner.union(outer)),
    Meeting("nested-rejection", first: inner, second: outer, result: nil),
]

for meeting in nested {
    meeting.report()
}
summarize(nested)
nested-precedence: first Shared/Headers | second Shared/Headers,Target/Headers | result Shared/Headers -> out-of-scope
nested-intersection: first Shared/Headers | second Shared/Headers,Target/Headers | result Shared/Headers -> out-of-scope
nested-union: first Shared/Headers | second Shared/Headers,Target/Headers | result Shared/Headers,Target/Headers -> out-of-scope
nested-rejection: first Shared/Headers | second Shared/Headers,Target/Headers | result none -> out-of-scope
precedence: 0
intersection: 0
union: 0
rejection: 0

All four lines are out-of-scope, and all four counters are zero. However a nested pair is combined, it cannot be written into any of the four resolutions; that is, what separates the resolutions is not the operation but the two definitions really saying different things. In the rest of the course this turns into a warning: if one definition already contains the other, no meeting is built, because even when the result changes, the classification stays the same whichever rule is chosen.

PS3. This measurement is for the pair in which the inner set is a subset of the outer set only. That two equal sets and the case where one of the sets is empty also give out-of-scope is read from the conditions of Meeting, but it was not measured separately.

Summary

  • When these two definitions meet, the result falls into one of four resolutions: precedence, intersection, union or rejection.
  • In eight meetings the output gave 2 precedence, 2 intersection, 2 union and 2 rejection; the distribution is a result of the examples that were built, not of the rule.
  • A result that never forms (none) and one that falls to an empty set (empty) are the same resolution: rejection.
  • The platform is built from four layers (core operating system, core services, media, interface); the layer of a definition tells who does the resolving.
  • Two nested definitions fall into no resolution: all four of the four results came out out-of-scope.
  • The resolution of this lesson: the four of the core (2/2/2/2).

Next Step

Two of the eight meetings were name lookup: the local definition shadowed the imported one, and the same name in two modules gave a rejection. In the next lesson we read frameworks and libraries with this eye: what happens when two modules open the same name, and why qualifying one removes the meeting.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close