Login
Track Z: The Zephyr seam
Login

After ExitBootServices, firmware's console is gone, and Glint needs a driver of its own to be seen at all. The plan's answer is the second kind of provider under Concept in PLAN.md: an upstream Zephyr driver compiled into Glint through compat/zephyr, as Run modes there states for native mode's console. Track Z builds the smallest seam that makes this work. Zephyr's PL011 driver (on QEMU virt) and its NS16550 driver (on q35, by I/O port) are compiled unmodified, run in poll mode, and stand behind Glint's console protocols, so that Phase 2's native system table (2.5) is built on them and its exit (2.6) prints through them.

The evidence for this track is docs/zephyr-survey.md. Its probe compiled both drivers, linked them into EFI images and printed through them on both sides of ExitBootServices. It found that the hard part is the toolchain, not the API: Glint's images were built for *-unknown-windows, which is LLP64 and MSVC-flavoured, while Zephyr assumes LP64 GNU C. Zephyr's own source states the assumption, in include/zephyr/kernel.h: "Zephyr currently assumes the size of a couple standard types to simplify print string formats." The track therefore started by moving Glint's images to ELF (Z.1), checks the drivers Phase 6 will want under that build, and only then writes the generators. The generators and shim follow the survey's section 6 ("What a minimal early 6.8 should contain", "What a minimal early 6.9 should contain"). Which Phase 6 slices extend them is stated once, under What Track Z leaves below.

Zephyr is pinned at release v4.4.2, commit dccb09599635bdff17633fa7e9dab014b91dce90. The source is an external tree, not a copy in the checkout: as Track T does with QEMU_SRC, the build finds it through ZEPHYR_SRC and checks that it is that commit, against the record in third_party/zephyr/PIN.md (Z.2), which is the one place the pin is written down in the repository. Zephyr's build system is not used (Build constraints). Each generator follows Track T's rule: it works from the data Zephyr ships and the document that specifies it, not from the Python program or its output. The oracle is Zephyr itself: its unchanged headers must accept the generated ones, and its drivers must compile with warnings as errors and run. Every Zephyr name stays inside compat/zephyr and the generated headers; Glint's core sees only EFI-shaped protocols. The track cites Zephyr's own documentation and source in place of the development text, as rule 2 allows for tracks.

The track runs after Track T. Z.1 to Z.9 are merged; they were refined with Phase 2 (rule 9). Three constraints fix their order against Phase 2. Z.1 came before any Phase 2 slice added code to images built the old way. Z.3 lands before 2.4a, because 2.4a's spec checks Glint's devicetree reader against cleave on the virt devicetree that Z.3 records. Z.9 lands before 2.5, which builds native mode's system table on Z.9's provider. Otherwise Z.4–Z.9 may interleave with 2.2–2.4. From Z.2 on, what differs between the architectures is data (a devicetree, a symbol table), so each slice lands for both at once. Z.1 was different: its entry code in boot/crt0.S, its relocation check and its calling-convention thunks are code that differs by architecture, under #ifdef, though one linker script serves both. Sizes are non-spec lines, as in rule 2.

Slices

Z.1 ELF images with a PE header. Merged as check-in 8b4357fa. This slice changed how Glint's two images are built, and nothing they do. Glint's Crystal and C are compiled for aarch64-unknown-linux-gnu and x86_64-unknown-linux-gnu, freestanding and position-independent, instead of *-unknown-windows, and tools/glint_build.cr links them with ld.lld as an ELF shared object under one linker script, boot/efi.lds, then copies out the loadable bytes with llvm-objcopy. The PE32+ header is not written by the build driver: it is assembled in boot/crt0.S, at the front of the image, from symbols the linker script defines (the sizes and offsets of .text, .data and .reloc). Its .reloc directory holds one empty block, which is all firmware needs, because the real fix-ups are ELF RELATIVE relocations: _start in crt0.S calls glint_relocate in c/efi_reloc.c, which applies them, refusing any other relocation type, before it calls efi_main. This is the method Linux's arm64 Image (arch/arm64/kernel/efi-header.S) and U-Boot's EFI applications (arch/arm/lib/crt0_aarch64_efi.S, elf_aarch64_efi.lds) use. On x86_64, UEFI's functions use the Microsoft x64 calling convention (UEFI 2.10 §2.3.4) while ELF code uses System V. Firmware's entry is converted in crt0.S, which moves the two arguments from rcx and rdx to rdi and rsi, and every call into UEFI goes through one boundary: c/efi_boundary.c holds one small C function per firmware call, declared ms_abi on x86_64, and boot/efi_boundary.cr routes Crystal's calls through it in the images and straight to the fake table in specs. On aarch64 both sides use AAPCS64, so the same functions compile as plain calls. It was demonstrated by re-proving Phase 1 under the new build: bin/glint-run aarch64 --expect "Hello from C via Glint" (and x86_64) boots at rung 3, spec/efi_image_spec.cr checks the PE32+ header, subsystem 10, .reloc and no imports at rung 2, and spec/elf_dialect_spec.cr checks that a C long and Crystal's LibC::Long are 8 bytes on both targets. It came first because the survey found the object format to be the riskiest assumption (risk 1), and because every Phase 2 slice adds code to these images; the generator slices that follow run on the Linux host and do not need them until Z.8 links drivers in. Under ELF, Zephyr's C compiles as Zephyr's own build compiles it: the survey's two patches and two MSVC-countering flags are no longer needed, and a linker script can give Zephyr's section-sorted tables a home (risk 3). One C dialect for Glint and Zephyr, stated and checked, is Clarity; removing the patches and flags is Parsimony. It came to 297 lines added and 32 removed, over ~200 because the header, the relocation step and the thunks only boot together: an image missing any one of them does not load.

Z.2 Checking the Phase 6 drivers first. Merged as check-in 1e8246d1. The survey's section 6 asks for one cheap check "before any generator is written": compile the drivers Phase 6 is likely to want with the pointer-cast warnings promoted to errors (-Werror=pointer-to-int-cast -Werror=int-to-pointer-cast), and with -Wshorten-64-to-32 as a warning whose every instance is recorded. A narrowing from a fixed-width type, such as uint64_t to uint32_t, is the same on every ABI and only noted; one whose source type is long, size_t, uintptr_t or another width-dependent type is what this check exists to find. The first run found the former in uart_pl011.c (the baud divisor), which Zephyr's own build does not warn about. This slice adds third_party/zephyr/PIN.md (release, commit, how to fetch the tree, and an empty list of patches), tools/zephyr/source.cr, which finds ZEPHYR_SRC and checks its commit against PIN.md, and tools/zephyr/flags.cr, the one list of flags every Zephyr object is built with. spec/zephyr/width_spec.cr then compiles, for both architectures, uart_pl011.c and uart_ns16550.c with CONFIG_UART_INTERRUPT_DRIVEN, drivers/virtio/virtio_mmio.c, virtio_pci.c, virtqueue.c and drivers/disk/nvme/*.c, against hand-written generated headers kept as spec data in spec/fixtures/zephyr/, as the survey's probe did. The fixtures come in two configurations per architecture. irq/ is the one this check uses: interrupt-driven, with interrupt cells in its devicetree header. poll/ is the configuration the first console runs in: the survey's 18-line autoconf.h without UART_INTERRUPT_DRIVEN, and the two syscall headers the poll path needs. Z.5 compiles against poll/ until Z.6 and Z.7 generate their own. The slice also lists the inline assembly that could be wrong-sized on LP64. The method is clang's own: for each target, clang -M with Z.2's flags names the headers each listed driver includes, and the spec takes those under include/zephyr/arch/ (the arm64 and common headers for aarch64, the x86 and common headers for x86_64). It then runs clang -fsyntax-only -Xclang -ast-dump=json on each driver and keeps every inline assembly statement (GCCAsmStmt) in those headers that has an operand whose type is long or unsigned long. The list goes in PIN.md under Inline assembly with long operands, one line per operand, sorted by header and line: include/zephyr/arch/x86/intel64/arch.h:61 arch_irq_lock output0 "=g" unsigned long, that is, header and line, enclosing function, operand, constraint and C type. The spec checks that the list it finds equals the one recorded. On LP64 such operands are 64 bits, which is what the assembly expects, so the list is a record to review when the pin moves, not a list of patches. It is demonstrated at rung 1 by ZEPHYR_SRC=… crystal spec spec/zephyr/width_spec.cr. It comes right after Z.1 because it tests Z.1's claim on the code Phase 6 will actually use, and because a failure here should reach the maintainer before any generator is written against an assumption that does not hold. Showing the assumption rather than trusting it is Demonstrability. It is about 150 lines; the fixtures are spec data and not counted.

Z.3 Reading Zephyr's bindings, and the build devicetrees. Merged as check-in 26a1c624. Zephyr's drivers read their devicetree through macros, and the macros carry each property in a form set by the node's binding, a YAML file in dts/bindings/. Zephyr's documentation puts it this way: "The build system uses bindings both to validate devicetree nodes and to convert the devicetree's contents into the generated devicetree_generated.h header file" (doc/build/dts/bindings-intro.rst). This slice adds tools/zephyr/bindings.cr, which reads bindings with Crystal's YAML, following doc/build/dts/bindings-syntax.rst: it merges include: files, records each property's type (int, boolean, string, string-array, array, phandle, phandles, phandle-array), its default and whether it is required, and matches nodes to bindings by compatible. It also adds the build devicetree for each machine class, as the plan's principle 6 describes, and keeps every input to them in compat/zephyr/dts/, since they are build inputs and not spec data. For virt, the input is QEMU's own devicetree, recorded by this slice as compat/zephyr/dts/qemu-virt.dtb from the project-built QEMU with exactly the machine arguments glint-run passes (the runner gives one method for both, plus dumpdtb=…), so a slice that changes them, such as turning on virtualization=on for EL2, regenerates the file. compat/zephyr/dts/qemu-virt-overlay.dts relabels its UART arm,sbsa-uart, the maintainer's choice: in that mode Zephyr's driver keeps the baud rate and format U-Boot already set, and reads no clock or speed property (survey section 4). The overlay is applied by this slice's tools/zephyr/build_dt.cr, because cleave at Glint's pin compiles overlays (/plugin/, with __fixups__, as dtc 1.7 does) but has no counterpart of fdtoverlay to apply one, and its compiler reads source, not a DTB. build_dt.cr reads the recorded DTB with Cleave::Fdt, compiles the overlay with Cleave::Dts and Cleave::Dtc, and for each fragment that names its target by target-path sets the fragment's properties on that node. It refuses a fragment that targets a phandle, and any overlay with __fixups__, since a DTB dumped by QEMU carries no labels; so the overlay names its node by path (&{/pl011@9000000}). Cleave's compiler emits a target-path fragment for a path reference, as dtc 1.7 does (add_orphan in src/cleave/dtc.cr), so the applier needs nothing new from cleave. For q35, which has no devicetree, compat/zephyr/dts/pc-com1.dts writes the PC's COM1 as data (ns16550, port 0x3f8, io-mapped, reg-shift, clock-frequency = <1843200>, current-speed = <115200>, and no interrupts), as Zephyr's own dts/x86/intel/atom.dtsi does. A spec loads the bindings of every node in both trees from ZEPHYR_SRC, checks the merged types of the UART properties, rejects a node that lacks a required property, and checks that the overlay changed only the UART's compatible. With QEMU_SRC, it also dumps the devicetree again and checks that it is byte-identical to the recorded one, so a change of QEMU pin shows up here. It is demonstrated at rung 1 by crystal spec spec/zephyr/bindings_spec.cr. It opens the generator because every macro the next two slices write depends on a property's type, and it must land before 2.4a, whose spec reads the recorded virt devicetree; the file is made once and serves both. Reading Zephyr's bindings as data, through cleave's existing devicetree code, keeps each mechanism in one place, which is Parsimony. It is about 230 lines, plus about 40 lines of devicetree source.

Z.4 Node and property macros. Merged as check-in 4183503f. This slice adds tools/zephyr/devicetree_header.cr, which writes the first half of devicetree_generated.h: the per-node and per-property macro families that the survey's section 4 lists for the two drivers. For each node it writes _PATH, _FULL_NAME, _PARENT, _EXISTS, _STATUS_okay, _COMPAT_MATCHES_<compat>, _REG_NUM, _REG_IDX_0_EXISTS, _REG_IDX_0_VAL_ADDRESS and _REG_IDX_0_VAL_SIZE. For each property its binding declares it writes _P_<prop> and _P_<prop>_EXISTS, with booleans always present as 0 or 1. For compatible it adds _P_compatible_LEN and _P_compatible_IDX_<i> with its _STRING_TOKEN, _STRING_UPPER_TOKEN and _EXISTS forms, and for clocks the phandle reference _P_clocks_IDX_<i>_PH. It also writes the helper DT_DEBRACKET_INTERNAL. The names follow doc/build/dts/macros.bnf, "An RFC 7405 ABNF grammar for devicetree macros", which is the specification this generator works from. Zephyr's own include/zephyr/devicetree.h, used unchanged, is the oracle: a spec compiles a small C file of _Static_asserts through the real header with clang -fsyntax-only (for example, that DT_REG_ADDR of the virt UART is 0x9000000 and its first compatible token is arm_sbsa_uart, and that COM1's io_mapped is 1). It is demonstrated at rung 1 by crystal spec spec/zephyr/devicetree_header_spec.cr. It follows Z.3 because each macro family is chosen by property type. Judging the generator by Zephyr's header rather than by Python's output is Demonstrability. It is about 240 lines. That passes ~200 because the eight property types are one table: until every type the two trees use is written, devicetree.h cannot evaluate any property, so a smaller split would leave a half that cannot be checked.

Z.5 Ordinals, instances and iteration. Merged as check-in aab5992c. Zephyr's device model needs more than per-node facts. Every C file that includes device.h expands DT_FOREACH_OKAY_HELPER over the whole tree (include/zephyr/device.h), and drivers find their instances through DT_INST_FOREACH_STATUS_OKAY. This slice extends devicetree_header.cr with the rest of the survey's section 4 list: dependency ordinals (_ORD and _ORD_STR_SORTABLE, in an order where a node follows its parent and the nodes its phandles name), instance identifiers (DT_N_INST_<i>_<compat>), and the global families DT_FOREACH_HELPER, DT_FOREACH_OKAY_HELPER, DT_COMPAT_HAS_OKAY_<compat>, DT_N_INST_<compat>_NUM_OKAY, DT_FOREACH_OKAY_<compat>, DT_FOREACH_OKAY_VARGS_<compat>, DT_FOREACH_OKAY_INST_<compat> and DT_FOREACH_OKAY_INST_VARGS_<compat>. Its acceptance test is the one the survey set: uart_pl011.c and uart_ns16550.c compile with -Werror (Z.2's recorded narrowings excepted) against the generated devicetree header. They compile in poll mode, since the generated header has no interrupt cells: autoconf.h and the syscall headers are still Z.2's hand-written poll/ fixtures, which leave CONFIG_UART_INTERRUPT_DRIVEN undefined. It is demonstrated at rung 1 by ZEPHYR_SRC=… crystal spec spec/zephyr/devicetree_header_spec.cr. It comes after Z.4 because ordinals and iteration are built over the node macros. With it, the devicetree generator is complete for poll-mode UARTs. Stopping at what the two drivers reach is Parsimony. It is about 200 lines.

Z.6 autoconf.h from a checked table. Merged as check-in 5c8819ca. Zephyr's drivers also read configuration symbols. As Zephyr's documentation says, "The output from Kconfig is a header file autoconf.h with macros that can be tested at build time" (doc/build/kconfig/index.rst). Zephyr's Kconfig tree expects a board and a SoC, which Glint does not have (principle 6; survey risk 6). So this slice does not run that tree. It adds a short symbol table per architecture, compat/zephyr/config/aarch64.cr and x86_64.cr, holding the symbols the survey's section 4 lists. Both tables hold SERIAL, SERIAL_HAS_DRIVER, SERIAL_INIT_PRIORITY (50), 64BIT and ATOMIC_OPERATIONS_BUILTIN, and, because kernel.h reads them, NUM_PREEMPT_PRIORITIES, NUM_COOP_PRIORITIES, MP_MAX_NUM_CPUS, SYS_CLOCK_TICKS_PER_SEC, SYS_CLOCK_HW_CYCLES_PER_SEC and SYS_CLOCK_MAX_TIMEOUT_DAYS, with SYS_CLOCK_EXISTS, on which the hardware cycle rate depends. PRIVILEGED_STACK_SIZE is in the aarch64 table only, as 2048: Kconfig gives it a value there from arch/arm64/core/Kconfig, and none on x86_64. The aarch64 table adds ARM64, UART_PL011 and UART_PL011_SBSA; the x86_64 table adds X86, X86_64, MAX_IRQ_LINES, UART_NS16550 and UART_NS16550_VARIANT_NS16550. The DT_HAS_<compat>_ENABLED symbols come from the build devicetree, not the table. The survey probe's value is written beside each symbol; where Kconfig would pick another for a symbol with no visible prompt, Kconfig's value wins, and the spec checks it. Both tables also name the symbols that must stay absent (UART_INTERRUPT_DRIVEN, UART_ASYNC_API, PINCTRL, RESET, CLOCK_CONTROL, PM_DEVICE, MMU, USERSPACE, LOG). Each table is closed under select, with one exception, chosen by the maintainer: on x86_64, X86_64 selects X86_MMU and so MMU, and the table overrides it to keep MMU unset as on aarch64. The spec prints the override whenever it runs. The slice adds tools/zephyr/autoconf.cr, which writes the header as Kconfig does, leaving a disabled boolean undefined, not 0. The DT_HAS_<compat>_ENABLED symbols are computed from the build devicetree (Z.3), the job Zephyr's gen_driver_kconfig_dts.py does. The table is checked rather than trusted, with Cleave::Kconfig's Zephyr dialect, which reads Kconfiglib's grammar, its source forms and its preprocessor functions (cleave 42316ae7, added in answer to docs/upstream/cleave-zephyr-kconfig-dialect.md). Zephyr's tree expects a board, so the spec does not load it from the top. It loads each file that defines a table symbol under a two-line root that sources Kconfig.constants and then the file, following the file's own source lines as Kconfiglib would: drivers/serial/Kconfig, Kconfig.pl011 and Kconfig.ns16550, arch/Kconfig, arch/arm64/core/Kconfig, arch/x86/Kconfig, kernel/Kconfig and kernel/Kconfig.smp. The one file Zephyr's CMake would have generated, $(KCONFIG_BINARY_DIR)/arch/Kconfig, is an empty stand-in, since the architecture files are loaded on their own. Through cleave's functions hook the spec answers $(dt_compat_any_has_prop,…), the one devicetree function the serial files call, from the build devicetrees. The three calls ask whether an ns16550 node has interrupts or pinctrl-0, and whether an arm,pl011 node has pinctrl-0; for these trees all three are n, since COM1 has neither property and virt's UART is arm,sbsa-uart. Every other $(dt_…) call (arch/Kconfig has eight, for SRAM, flash and cache-line sizes) gets cleave's default, Kconfiglib's answer with no devicetree, and the spec fails if a table symbol's definition contains one. It then checks that each table symbol is defined in one of those files with the table's type, and that its depends on holds, for a symbol defined in several places, as the OR of every definition's dependencies, each default applying under its own definition's conditions, when evaluated with the table's symbols at their values, the DT_HAS_* symbols from the build devicetree, and every symbol outside the table as n, which is how Kconfig treats a symbol that is not set. The survey also names UART_LOG_LEVEL for the NS16550; it is defined by the logging template with depends on LOG, so the table leaves it out, and Z.7's compile shows whether the driver needs it with LOG unset. It is demonstrated at rung 1 by ZEPHYR_SRC=… crystal spec spec/zephyr/autoconf_spec.cr. It needs only Z.3, for the DT_HAS_* symbols, so it may land before Z.4 and Z.5; Z.7 needs all three. A short table that every reader can see, checked against the files that define it, is Clarity; the full tree belongs to Track S, which has a Zephyr board. It is about 170 lines.

Z.7 Syscall stubs. Merged as check-in 4eb32a10. Zephyr's headers declare many functions with __syscall and then include a generated zephyr/syscalls/<header>.h. Glint never enables Zephyr's user mode, and Zephyr's documentation states what then happens: "If CONFIG_USERSPACE is not enabled, all APIs just directly call the implementation function" (doc/kernel/usermode/syscalls.rst). This slice adds tools/zephyr/syscalls.cr. It finds the headers the build includes with clang rather than from a fixed list: for each driver in the source list, clang -M -MG with Z.2's flags names the header closure, counting the not-yet-generated zephyr/syscalls/*.h as dependencies rather than failing on them. For every header in the closure that includes one of those, it reads the __syscall declarations and writes the matching zephyr/syscalls/<header>.h with an inline wrapper per declaration that calls z_impl_<name>; it also writes empty syscall_list.h, kobj-types-enum.h and heap_constants.h. Its demonstration is that both UART drivers now compile with -Werror in poll mode from generated headers alone, with none of Z.2's hand-written ones: ZEPHYR_SRC=… crystal spec spec/zephyr/syscalls_spec.cr at rung 1. Z.2's fixtures stay, for the Phase 6 drivers the generators do not yet cover. It comes after Z.5 and Z.6 because that compile needs their headers. Writing stubs that call straight through, instead of building any system-call machinery, is Parsimony. It is about 140 lines.

Z.8 The minimal shim runtime. Merged as check-in 36aeae9c. Compiled drivers must now be linked into Glint and started. Zephyr's documentation gives the job: "The device model is responsible for initializing all the drivers configured into the system" (doc/kernel/drivers/index.rst). Zephyr does this by walking linker sections; Glint generates a table instead (survey section 4, risk 3). This slice adds tools/zephyr/device_table.cr, which writes glint_devices.c, a table of DEVICE_DT_GET for each enabled node whose compatible a built driver claims, in ordinal order. Beside each device the table records the compatible and first reg address it was built from, so that 2.6 can compare the firmware's description with the class the image was built for. It adds compat/zephyr/device.cr, with the init runner (call init, store a positive errno in init_res, set initialized, as do_device_init in kernel/device.c does) and z_impl_device_is_ready, and compat/zephyr/libc.cr, with memcpy and memset as Crystal exports. The only kernel object the two drivers reach needs no code from Glint: the NS16550 takes k_spin_lock and k_spin_unlock around each register access, and those are Zephyr's own inline functions in include/zephyr/spinlock.h, used unchanged, over arch_irq_lock; on one CPU, masking interrupts is all a spinlock must do. Finally, tools/zephyr/sources.cr holds the source list, and tools/glint_build.cr compiles the listed drivers into both images. It is demonstrated at two rungs. At rung 1, a spec compiles spec/fixtures/zephyr/test_device.c for the Linux host with headers generated from spec/fixtures/zephyr/test.dts, a test devicetree with two nodes of the compatible vnd,device-with-props. That compatible's binding is Zephyr's own, dts/bindings/test/vnd,device-with-props.yaml, read from ZEPHYR_SRC like every other binding, so the test needs no binding of Glint's. The test driver's init succeeds for one instance and fails with -EIO for the other, and the spec runs the init runner and checks init_res, initialized and device_is_ready. The host is x86_64 and LP64, so it uses the x86_64 table, and the test driver calls nothing that needs privilege. At rung 2, both images link with the real drivers, and a spec checks that, before linking, their only undefined symbols are the ones the survey found (memcpy for the PL011, z_impl_device_is_ready for the NS16550). The runtime does not initialise any device at boot: 2.5's console bring-up does that, after the exit. It comes after Z.7 because it links objects compiled from generated headers alone. Implementing Zephyr's API in Crystal, rather than copying Zephyr's kernel, is Loose Coupling. It is about 220 lines. That passes ~200 because a driver cannot link without the runtime, and the runtime cannot be shown in the images without the build step, so either half alone would need a temporary stub.

Z.9 Zephyr's UART as a console provider. Merged as check-in 710533c9. This slice adds compat/zephyr/console.cr, which wraps a Zephyr UART device as Glint's console protocols, and compat/zephyr/status.cr, the one table that maps Zephyr's results to EFI_STATUS (risk 7): negative errno values, the -1 of poll_in, and the positive errno that init_res holds. The table lands here because this is its first user. SIMPLE_TEXT_OUTPUT.OutputString sends each UCS-2 character through uart_poll_out encoded as UTF-8 (callers send \r\n, as UEFI expects); an unpaired surrogate goes out as ?. Reset succeeds. The calls a UART cannot honour, such as SetAttribute and ClearScreen, return EFI_UNSUPPORTED. SIMPLE_TEXT_INPUT.ReadKeyStroke calls uart_poll_in, which "is a non-blocking function and returns a character or -1 when no valid data is available" (doc/hardware/peripherals/uart.rst); its "nothing yet" becomes EFI_NOT_READY, which Phase 1's Console.read_line already polls past. Bytes become keys with a scan code of 0, following the maintainer's rules for serial input: UTF-8 is decoded, so 0x20–0x7E and each complete multi-byte sequence for a character in the Basic Multilingual Plane become that character, and an invalid or out-of-plane sequence is dropped; carriage return as Enter, and a line feed as Enter unless it directly follows a carriage return, in which case it is ignored; 0x08 and 0x7F as Backspace; an escape and the control sequence it starts are read to the sequence's final byte and dropped, since Phase 1's shell uses no arrow keys; other control bytes are dropped. poll_out waits for the FIFO with no timeout (survey section 2), which is acceptable for a console. Its demonstration runs Phase 1's own keyboard and shell specs through Zephyr's API, without changing them. A spec compiles spec/fixtures/zephyr/fake_uart.c, a Zephyr UART driver whose poll calls read and write buffers, for the Linux host, as a node of the compatible vnd,serial added to Z.8's test devicetree; its binding is again Zephyr's own, dts/bindings/test/vnd,serial.yaml. spec/support/fake_efi.cr learns one option, the environment variable GLINT_FAKE_CONSOLE=zephyr: with it set, the fake table's ConIn and ConOut are this provider in front of the fake UART, script_keys fills the UART's receive buffer, and text reads its transmit buffer. spec/zephyr/console_spec.cr runs crystal spec spec/keyboard_spec.cr spec/shell_spec.cr in a child process with the variable set and checks that the same examples pass, then checks the byte-to-key rules and the status table directly. The two Phase 1 files are not edited. That is rung 1, crystal spec spec/zephyr/console_spec.cr; at rung 2 both images link the provider. Its rung-3 evidence is 2.6's first boot through it: printing through a second console during a hosted boot would need a mechanism that would then be removed. It comes last because it wraps a device that Z.8 makes linkable. A Zephyr driver behind the same protocol as the firmware's console, with no call site changed, is the plan's Loose Coupling in its smallest form. It is about 170 lines.

Track demo: Phase 2's exit demonstration (2.6). bin/glint-run aarch64 --type native --expect "Mode: native" (and x86_64) leaves boot services, prints Left boot services and the lines after it through Zephyr's PL011 and NS16550 drivers, compiled unmodified from the pinned release, and the shell keeps answering through them. Within the track, the last demonstration is Z.9's: Phase 1's keyboard and shell specs passing through Zephyr's UART API.

Z.10 Uninitialised data the loader zero-fills. Merged as check-in 1bf86292. A maintenance slice to Z.1's layout, found while preparing 2.6. .bss sat inside .data, so each image stored its zero-initialised data as zeros: 2.3's 16 KiB map buffer now, and 2.6's 64 KiB stack next. UEFI loaders zero-fill a section's VirtualSize past its SizeOfRawData, as U-Boot does (lib/efi_loader/efi_image_loader.c:1024–1027). So .bss becomes its own NOBITS section, and the PE header gives .data a virtual size larger than its raw size. Storing only what is not zero is Parsimony, and a spec that checks the header fields against the file keeps it Demonstrable.

What Track Z leaves

This is the one place the plan says who extends Track Z. Z.3–Z.9 come to about 1,370 lines, within the survey's estimate of 1,150–1,550 for this scope; Z.1 (297 as merged) and Z.2 add about 450. The track leaves generators limited to what two poll-mode UARTs reach, a shim with one device table and no kernel object of Glint's own, and one console provider. Three Phase 6 slices extend them in place, and no other slice does:

Pinctrl, clocks and resets stay avoided by the build devicetree, as the survey's section 3 advises, because firmware has already configured the UART's pins and clock. None of the three Phase 6 slices takes them. They are needed first by a machine class whose UART cannot be used as firmware left it. The first such slice is Track S's S.2, whose emulated board names its UART's pins (The devicetree on a small target there). Late binding (principle 6) is not placed in a slice either. CONFIG_MMU stays unset: Glint's own page tables (3.2) map the registers that Z.8's device table records, at their physical addresses, so the drivers need no k_mem_map_phys_bare. The survey's section 6 lists the rest.

References