From cecbfce59d8a033c0cc16bdaf0191c0a86f37587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 2 Mar 2026 15:50:47 +0100 Subject: [PATCH 01/36] Claude Code: Rewires the documentation generation in abi-mapper to have a proper document format instead of just a list of strings --- src/tools/abi-mapper/build.zig | 11 + src/tools/abi-mapper/rework/abi-doc-format.md | 1045 +++++++++++++++++ src/tools/abi-mapper/src/abi-parser.zig | 1 + src/tools/abi-mapper/src/doc_comment.zig | 389 ++++++ src/tools/abi-mapper/src/model.zig | 267 ++++- src/tools/abi-mapper/src/sema.zig | 93 +- src/tools/abi-mapper/tests/doc_parser.zig | 425 +++++++ 7 files changed, 2150 insertions(+), 81 deletions(-) create mode 100644 src/tools/abi-mapper/rework/abi-doc-format.md create mode 100644 src/tools/abi-mapper/src/doc_comment.zig create mode 100644 src/tools/abi-mapper/tests/doc_parser.zig diff --git a/src/tools/abi-mapper/build.zig b/src/tools/abi-mapper/build.zig index 3fb7276e..c0163683 100644 --- a/src/tools/abi-mapper/build.zig +++ b/src/tools/abi-mapper/build.zig @@ -35,6 +35,17 @@ pub fn build(b: *std.Build) void { const output_file = convert_test_file.addPrefixedOutputFileArg("--output=", "coverage.json"); test_step.dependOn(&b.addInstallFile(output_file, "test/coverage.json").step); + + const doc_parser_mod = b.createModule(.{ + .root_source_file = b.path("tests/doc_parser.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "abi-parser", .module = abi_parser_mod }, + }, + }); + const doc_parser_tests = b.addTest(.{ .root_module = doc_parser_mod }); + test_step.dependOn(&b.addRunArtifact(doc_parser_tests).step); } pub const Converter = struct { diff --git a/src/tools/abi-mapper/rework/abi-doc-format.md b/src/tools/abi-mapper/rework/abi-doc-format.md new file mode 100644 index 00000000..2a552db1 --- /dev/null +++ b/src/tools/abi-mapper/rework/abi-doc-format.md @@ -0,0 +1,1045 @@ +# Ashet IDL Documentation Comment Format + +**Status:** Draft + +## 1. Overview + +This document specifies the syntax, semantics, and data model for structured documentation comments in Ashet IDL files. It replaces the previous unstructured `docs: []const u8` (list of raw lines) representation with a validated, hyperlinked document fragment tree. + +### 1.1 Design goals + +- **Minimal migration friction.** Existing doc comments are nearly valid as-is. +- **Validated cross-references.** Every reference to an IDL declaration is resolved and checked at parse time. +- **Structured admonitions.** `NOTE`, `LORE`, `EXAMPLE`, etc. are first-class constructs, not conventions. +- **HyperDoc-compatible AST.** The output tree uses a subset of HyperDoc 2.0's semantic model, enabling shared rendering and tooling. +- **Parseable in Zig.** No complex grammar; every construct is recognizable by a simple line-prefix or character-level scan. + +### 1.2 Relationship to HyperDoc 2.0 + +The output AST of a parsed doc comment is a strict subset of HyperDoc 2.0's document model. Specifically, it uses the node types `p`, `note`, `warning`, `tip`, `ul`, `ol`, `pre`, `\mono`, `\em`, `\ref`, and `\link`, plus the custom extensions `lore`, `example`, `deprecated`, and `decision`. + +The *input syntax*, however, is a distinct lightweight format optimized for `///` comment lines, not HyperDoc surface syntax. Think of it as a convenient authoring frontend that compiles to a HyperDoc fragment. + +--- + +## 2. Source representation + +### 2.1 Comment extraction + +Documentation comments are lines beginning with `///`. The parser strips the `///` prefix and exactly one optional trailing space character (the separator between `///` and content). The resulting lines form the **raw doc text**, which is then parsed according to this specification. + +``` +/// This is a paragraph. → "This is a paragraph." +/// → "" +/// Indented continuation. → " Indented continuation." +``` + +Line comments (`//?`) are not part of the documentation and are discarded before doc parsing. + +### 2.2 Encoding + +Raw doc text inherits the encoding of the IDL source file (UTF-8). No additional encoding layer is defined. + +--- + +## 3. Document model (normative) + +A parsed doc comment produces a **DocComment** value. The model is specified here as a JSON schema; the canonical in-memory representation in Zig is derived from this. + +### 3.1 JSON schema + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ashet.org/schemas/abi-doc-comment/v1", + "title": "Ashet IDL DocComment", + + "$defs": { + + "DocComment": { + "description": "Root type. A parsed documentation comment.", + "type": "object", + "required": ["sections"], + "additionalProperties": false, + "properties": { + "sections": { + "type": "array", + "items": { "$ref": "#/$defs/Section" }, + "minItems": 0 + } + } + }, + + "Section": { + "description": "A thematic section of a doc comment. The first section with kind 'main' contains the primary description. Subsequent sections are admonitions.", + "type": "object", + "required": ["kind", "blocks"], + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "main", + "note", + "warning", + "lore", + "example", + "deprecated", + "decision" + ] + }, + "blocks": { + "type": "array", + "items": { "$ref": "#/$defs/Block" }, + "minItems": 1 + } + } + }, + + "Block": { + "description": "A block-level element inside a section.", + "oneOf": [ + { "$ref": "#/$defs/Paragraph" }, + { "$ref": "#/$defs/UnorderedList" }, + { "$ref": "#/$defs/OrderedList" }, + { "$ref": "#/$defs/CodeBlock" } + ] + }, + + "Paragraph": { + "type": "object", + "required": ["type", "content"], + "additionalProperties": false, + "properties": { + "type": { "const": "paragraph" }, + "content": { "$ref": "#/$defs/InlineContent" } + } + }, + + "UnorderedList": { + "type": "object", + "required": ["type", "items"], + "additionalProperties": false, + "properties": { + "type": { "const": "unordered_list" }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/InlineContent" }, + "minItems": 1 + } + } + }, + + "OrderedList": { + "type": "object", + "required": ["type", "items"], + "additionalProperties": false, + "properties": { + "type": { "const": "ordered_list" }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/InlineContent" }, + "minItems": 1 + } + } + }, + + "CodeBlock": { + "type": "object", + "required": ["type", "content"], + "additionalProperties": false, + "properties": { + "type": { "const": "code_block" }, + "syntax": { + "description": "Optional syntax identifier (HyperDoc §10.1.1 compatible).", + "type": ["string", "null"] + }, + "content": { + "description": "Raw text content of the code block. Line breaks are preserved.", + "type": "string" + } + } + }, + + "InlineContent": { + "description": "A sequence of inline spans forming a rich text run.", + "type": "array", + "items": { "$ref": "#/$defs/Inline" } + }, + + "Inline": { + "description": "A single inline element.", + "oneOf": [ + { "$ref": "#/$defs/Text" }, + { "$ref": "#/$defs/Code" }, + { "$ref": "#/$defs/Emphasis" }, + { "$ref": "#/$defs/Ref" }, + { "$ref": "#/$defs/Link" } + ] + }, + + "Text": { + "type": "object", + "required": ["type", "value"], + "additionalProperties": false, + "properties": { + "type": { "const": "text" }, + "value": { "type": "string" } + } + }, + + "Code": { + "description": "Inline monospace code span. Not validated as a reference.", + "type": "object", + "required": ["type", "value"], + "additionalProperties": false, + "properties": { + "type": { "const": "code" }, + "value": { "type": "string" } + } + }, + + "Emphasis": { + "type": "object", + "required": ["type", "content"], + "additionalProperties": false, + "properties": { + "type": { "const": "emphasis" }, + "content": { "$ref": "#/$defs/InlineContent" } + } + }, + + "Ref": { + "description": "A validated cross-reference to an IDL declaration. The fqn field always contains the fully-qualified resolved name, regardless of what the author wrote in the source.", + "type": "object", + "required": ["type", "fqn"], + "additionalProperties": false, + "properties": { + "type": { "const": "ref" }, + "fqn": { + "description": "The fully-qualified name of the referenced IDL declaration. Always stored in resolved form (e.g. 'resource.bind.target', never the shorthand 'target').", + "type": "string" + } + } + }, + + "Link": { + "description": "A hyperlink to an external resource.", + "type": "object", + "required": ["type", "url"], + "additionalProperties": false, + "properties": { + "type": { "const": "link" }, + "url": { "type": "string" }, + "content": { + "description": "Display text. If absent or empty, the URL is used as display text.", + "$ref": "#/$defs/InlineContent" + } + } + } + }, + + "$ref": "#/$defs/DocComment" +} +``` + +### 3.2 Type summary + +``` +DocComment + └─ sections: Section[] + +Section + ├─ kind: "main" | "note" | "warning" | "lore" | "example" + │ | "deprecated" | "decision" + └─ blocks: Block[] (at least 1) + +Block = Paragraph | UnorderedList | OrderedList | CodeBlock + +Paragraph + └─ content: Inline[] + +UnorderedList + └─ items: Inline[][] (each item is one inline run) + +OrderedList + └─ items: Inline[][] + +CodeBlock + ├─ syntax: string? + └─ content: string (raw text, newlines preserved) + +Inline = Text | Code | Emphasis | Ref | Link + +Text { value: string } +Code { value: string } +Emphasis { content: Inline[] } +Ref { fqn: string } (always fully-qualified, resolved form) +Link { url: string, content?: Inline[] } +``` + +### 3.3 HyperDoc AST mapping + +For tooling that consumes HyperDoc document trees, the mapping is: + +| Doc comment type | HyperDoc element | +|---|---| +| Section(main) | sequence of child blocks (no wrapper) | +| Section(note) | `note { ... }` | +| Section(warning) | `warning { ... }` | +| Section(lore) | `lore { ... }` (extension) | +| Section(example) | `example { ... }` (extension) | +| Section(deprecated) | `deprecated { ... }` (extension) | +| Section(decision) | `decision { ... }` (extension) | +| Paragraph | `p { ... }` | +| UnorderedList | `ul { li { ... } li { ... } }` | +| OrderedList | `ol { li { ... } li { ... } }` | +| CodeBlock | `pre(syntax="...") : \| ...` | +| Text | bare inline text | +| Code | `\mono { ... }` | +| Emphasis | `\em { ... }` | +| Ref | `\ref(ref="...");` | +| Link | `\link(uri="...") { ... }` | + +--- + +## 4. Syntax + +### 4.1 Block structure + +After prefix stripping (§2.1), the raw doc text is parsed line-by-line into blocks. Blank lines are block separators. + +#### 4.1.1 Paragraphs + +Any sequence of non-blank lines that does not match another block rule forms a paragraph. Adjacent lines are joined with a single space (whitespace normalization). + +``` +/// The process handle to terminate. +/// Must be a valid, non-destroyed handle. +``` + +Produces one paragraph: `The process handle to terminate. Must be a valid, non-destroyed handle.` + +#### 4.1.2 Admonition sections + +A line matching the pattern `: ` where `` is one of the recognized admonition keywords starts a new section. The text after the colon (plus any continuation lines) forms the first block of that section. Continuation lines are either: + +- Indented by at least one space beyond the tag's colon position (aligned continuation), or +- Any non-blank, non-admonition, non-list line (unindented continuation — for compatibility with existing docs). + +Recognized tags (case-sensitive): + +| Tag | Section kind | +|---|---| +| `NOTE` | `note` | +| `WARNING` | `warning` | +| `LORE` | `lore` | +| `EXAMPLE` | `example` | +| `DEPRECATED` | `deprecated` | +| `DECISION` | `decision` | + +A new admonition tag or a blank line followed by different content ends the current section and starts a new one. + +``` +/// NOTE: This will *always* destroy the resource, even if it's +/// still strongly bound by a process. +``` + +Produces: `Section(note)` containing one paragraph. + +Multiple admonitions of the same kind are separate sections: + +``` +/// NOTE: First note. +/// +/// NOTE: Second note. +``` + +Produces two `Section(note)` values. + +All text before the first admonition tag belongs to `Section(main)`. If no admonition tags appear, the entire doc comment is a single `Section(main)`. + +#### 4.1.3 Unordered lists + +A line beginning with `- ` (hyphen + space) starts an unordered list item. Continuation lines must be indented by at least two spaces. + +``` +/// - Resources are created through various calls in the kernel API, +/// but their lifetime is managed through this namespace. +/// - After creation, a resource is strongly bound to the creator. +``` + +Produces: `UnorderedList` with two items. + +Adjacent list items form a single list. A blank line or non-list-item line ends the list. + +#### 4.1.4 Ordered lists + +A line beginning with `. ` (decimal number + dot + space) starts an ordered list item. Continuation lines must be indented past the number prefix. + +``` +/// 1. Allocate memory. +/// 2. Write the payload. +/// 3. Schedule the ARC. +``` + +#### 4.1.5 Fenced code blocks + +A line consisting of exactly ` ``` ` or ` ``` ` starts a fenced code block. The block continues until a closing ` ``` ` line. Lines between the fences are taken verbatim (no inline parsing, no whitespace normalization). + +``` +/// ```zig +/// const handle = try resource.open(path); +/// defer resource.close(handle); +/// ``` +``` + +Produces: `CodeBlock { syntax: "zig", content: "const handle = try resource.open(path);\ndefer resource.close(handle);" }` + +The syntax identifier, if present, follows HyperDoc §10.1.1 rules. + +### 4.2 Inline syntax + +Within paragraphs, list items, and admonition text, the following inline constructs are recognized. Inline parsing operates on the joined, whitespace-normalized text of a block. + +#### 4.2.1 Inline code: `` `...` `` + +A backtick-delimited span produces an inline `Code` node. The content between backticks is taken verbatim (no nested inline parsing). + +``` +/// The `destroy` syscall always succeeds. +``` + +Produces: `[Text("The "), Code("destroy"), Text(" syscall always succeeds.")]` + +Backtick spans must not be empty. An unmatched backtick is a parse error. + +#### 4.2.2 Cross-reference: `` @`...` `` + +An `@` character immediately followed by a backtick-delimited span produces an inline `Ref` node. The content between the backticks is interpreted as a fully-qualified name (FQN) and **must** resolve to a declaration in the IDL. + +``` +/// See @`overlapped.ARC` for the completion queue model. +``` + +Produces: `[Text("See "), Ref("overlapped.ARC"), Text(" for the completion queue model.")]` + +FQN resolution rules: + +Resolution walks from the innermost scope outward. Given a reference `@`name`` on a declaration with FQN `a.b.c`: + +1. **Self scope.** Look up `a.b.c.name` — i.e., the reference names a child of the declaration the doc comment is attached to. This is the most common case for syscall docs referencing their own parameters, struct docs referencing their own fields, enum docs referencing their own items, etc. +2. **Sibling scope.** Look up `a.b.name` — i.e., a sibling declaration in the same namespace. +3. **Parent scopes.** Walk outward: `a.name`, then `name` at global scope. +4. **Global exact match.** Look up `name` as a fully-qualified path (for when the author writes the full FQN explicitly). +5. If no match is found, emit a validation error. + +Steps 1–3 check the *unqualified* name against progressively broader scopes. Step 4 handles the case where `name` itself contains dots and is already fully qualified (e.g., `@`overlapped.ARC.tag``). + +If a name is ambiguous (matches at multiple scopes), the innermost match wins. Tooling **should** emit a warning for ambiguous references and suggest using the full FQN. + +**Examples:** + +```abi +/// Parameter @`foo` is used for stuff. +/// ↑ resolves to call.foo (self scope) +syscall call { + in foo: u32; +} +``` + +```abi +namespace overlapped { + /// See @`ARC` for the structure layout. + /// ↑ resolves to overlapped.ARC (sibling scope) + syscall schedule { + in @"arc": *ARC; + } +} +``` + +```abi +/// Uses the @`overlapped.ARC` completion model. +/// ↑ resolves as-is (global exact match) +namespace process { ... } +``` + +For references to sub-items (fields, enum items, error variants), dot notation works at any scope level: `@`ARC.tag`` inside `namespace overlapped` resolves to `overlapped.ARC.tag` via sibling scope + child traversal. + +A bare `@` not immediately followed by a backtick has no special meaning and is treated as literal text. + +#### 4.2.3 Emphasis: `*...*` + +An asterisk-delimited span produces an inline `Emphasis` node. The content between asterisks is parsed for nested inline constructs (code, references, links — but not nested emphasis). + +``` +/// This will *always* destroy the resource. +``` + +Produces: `[Text("This will "), Emphasis([Text("always")]), Text(" destroy the resource.")]` + +Emphasis spans must not be empty. Opening `*` must be preceded by whitespace or start-of-text, and closing `*` must be followed by whitespace, punctuation, or end-of-text. This prevents false matches on expressions like `a*b*c`. + +#### 4.2.4 Links: `[text](url)` and `` + +**Titled link:** A `[` character starts a link's display text, closed by `]`, immediately followed by `(url)`. The display text is parsed for nested inline constructs (code, emphasis, references). The URL is taken verbatim. + +``` +/// See [the RISC-V specification](https://riscv.org/specifications/) for details. +``` + +Produces: `Link { url: "https://riscv.org/specifications/", content: [Text("the RISC-V specification")] }` + +**Autolink:** A `<` character followed by a URL scheme (`http://`, `https://`, or `mailto:`) and closed by `>` produces a link where the display text is the URL itself. + +``` +/// More information at . +``` + +Produces: `Link { url: "https://ashet.org/docs/abi", content: [Text("https://ashet.org/docs/abi")] }` + +A `<` not followed by a recognized URL scheme is treated as literal text. This avoids ambiguity with angle brackets in prose (e.g., ``). + +### 4.3 Escape sequences + +To use the special characters literally in inline text: + +| Sequence | Produces | +|---|---| +| `` \` `` | literal `` ` `` | +| `\*` | literal `*` | +| `\[` | literal `[` | +| `\<` | literal `<` | +| `\@` | literal `@` | +| `\\` | literal `\` | + +Escapes are only recognized in inline contexts. They are not recognized inside code spans (`` `...` ``), code blocks, or URLs. + +--- + +## 5. Parsing algorithm (non-normative) + +This section describes the intended parsing strategy. It is non-normative but should produce results identical to the normative syntax rules. + +### 5.1 Block pass (line-oriented) + +``` +input: list of stripped doc lines +output: list of (SectionKind, Block) + +state: + current_section = main + current_block = null + in_code_fence = false + +for each line: + if in_code_fence: + if line == "```": + emit CodeBlock, in_code_fence = false + else: + append line to code block buffer + continue + + if line starts with "```": + flush current_block + extract optional syntax tag + in_code_fence = true + continue + + if line is blank: + flush current_block + continue + + if line matches /^(NOTE|WARNING|LORE|EXAMPLE|DEPRECATED|DECISION):\s+(.*)/: + flush current_block + current_section = matched tag + start new paragraph with captured text + continue + + if line matches /^- (.*)/: + if current_block is not unordered_list: flush, start new list + append new list item with captured text + continue + + if line matches /^(\d+)\. (.*)/: + if current_block is not ordered_list: flush, start new list + append new list item with captured text + continue + + if current_block is list and line starts with sufficient indentation: + append to current list item (continuation) + continue + + if current_block is paragraph: + append line to paragraph (continuation) + else: + flush current_block + start new paragraph with line + +flush current_block +``` + +### 5.2 Inline pass (character-oriented) + +For each paragraph or list item text, scan left-to-right: + +``` +while not end-of-text: + if char == '\\' and next is escapable: + emit Text(next), advance 2 + elif char == '`': + scan to closing '`' + emit Code(content) + elif char == '@' and next == '`': + advance past '@' + scan to closing '`' + emit Ref(content) // validated later + elif char == '*': + if valid emphasis open (preceded by whitespace/start): + scan to closing '*' (with valid close context) + recursively parse content for nested inlines + emit Emphasis(parsed_content) + else: + emit Text('*') + elif char == '[': + scan to '](' + parse display text for nested inlines + scan to closing ')' + emit Link(url, parsed_display) + elif char == '<' and followed by url scheme: + scan to '>' + emit Link(url, [Text(url)]) + else: + accumulate into Text span +``` + +### 5.3 Validation pass + +After block and inline parsing: + +1. **FQN resolution:** For every `Ref` node, resolve the FQN against the IDL symbol table using the scoped resolution rules (§4.2.2). The resolution requires knowing the FQN of the declaration the doc comment is attached to. Emit an error for unresolvable references; emit a warning for ambiguous references. +2. **Empty section check:** Sections with zero blocks are invalid (parser bug, not user error). +3. **Unclosed constructs:** Unmatched backticks, asterisks, brackets, or code fences are parse errors. +4. **TODO rejection:** If any section's text starts with `TODO:` (or a line comment `//? TODO:` was mistakenly written as `/// TODO:`), emit a warning. TODOs are development artifacts; use `//? TODO:` instead. + +--- + +## 6. Examples + +### 6.1 Simple field and parameter documentation + +Source: + +```abi +/// The process handle to query. +/// If `null`, uses the current process. +in target: ?Process; +``` + +JSON output: + +```json +{ + "sections": [{ + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "The process handle to query. If " }, + { "type": "code", "value": "null" }, + { "type": "text", "value": ", uses the current process." } + ] + }] + }] +} +``` + +Self-scope reference example — a syscall doc referencing its own parameter: + +```abi +/// Binds @`resource` to a process. +/// +/// The success of this operation allows @`target` to access +/// @`resource`, and optionally gain/lose a strong binding. +syscall bind { + in resource: SystemResource; + in target: ?Process; + in binding: BindOperation; +} +``` + +Here `@`resource`` resolves to `resource.bind.resource` (self-scope), `@`target`` to `resource.bind.target`. + +### 6.2 Multiple notes + +Source: + +```abi +/// Immediately destroys the resource and releases its memory. +/// +/// NOTE: This will *always* destroy the resource, even if it's +/// still strongly bound by a process. +/// +/// NOTE: This immediately triggers tether chains and destroys +/// all tethered resources as well. +/// +/// NOTE: @`resource.destroy` always succeeds; destroying an invalid +/// or already-destroyed handle is a no-op. +``` + +JSON output: + +```json +{ + "sections": [ + { + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Immediately destroys the resource and releases its memory." } + ] + }] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "This will " }, + { "type": "emphasis", "content": [ + { "type": "text", "value": "always" } + ]}, + { "type": "text", "value": " destroy the resource, even if it's still strongly bound by a process." } + ] + }] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "This immediately triggers tether chains and destroys all tethered resources as well." } + ] + }] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "ref", "fqn": "resource.destroy" }, + { "type": "text", "value": " always succeeds; destroying an invalid or already-destroyed handle is a no-op." } + ] + }] + } + ] +} +``` + +### 6.3 LORE section with list and cross-references + +Source: + +```abi +/// All syscalls related to generic resource management. +/// +/// - Resources are created through various calls in the kernel API, but their +/// lifetime is managed through calls inside this namespace. +/// - After creation, a resource is strongly bound to the process that created it. +/// - When a resource is destroyed, it becomes unusable from userland. +/// +/// NOTE: Every kernel object the userland can interact with is a @`SystemResource`. +/// +/// LORE: Originally, Ashet OS had no concept of bindings, but only of ownership. +/// But this quickly led to problems like "the desktop server also owns the +/// window, so even if the application releases the window, it is not destroyed." +/// The idea of allowing a process to access a resource without keeping it alive +/// solves this problem completely. See @`resource.bind` for the binding API. +``` + +JSON output: + +```json +{ + "sections": [ + { + "kind": "main", + "blocks": [ + { + "type": "paragraph", + "content": [ + { "type": "text", "value": "All syscalls related to generic resource management." } + ] + }, + { + "type": "unordered_list", + "items": [ + [ + { "type": "text", "value": "Resources are created through various calls in the kernel API, but their lifetime is managed through calls inside this namespace." } + ], + [ + { "type": "text", "value": "After creation, a resource is strongly bound to the process that created it." } + ], + [ + { "type": "text", "value": "When a resource is destroyed, it becomes unusable from userland." } + ] + ] + } + ] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Every kernel object the userland can interact with is a " }, + { "type": "ref", "fqn": "SystemResource" }, + { "type": "text", "value": "." } + ] + }] + }, + { + "kind": "lore", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Originally, Ashet OS had no concept of bindings, but only of ownership. But this quickly led to problems like \"the desktop server also owns the window, so even if the application releases the window, it is not destroyed.\" The idea of allowing a process to access a resource without keeping it alive solves this problem completely. See " }, + { "type": "ref", "fqn": "resource.bind" }, + { "type": "text", "value": " for the binding API." } + ] + }] + } + ] +} +``` + +### 6.4 DECISION and EXAMPLE admonitions + +Source: + +```abi +/// Defines the process exit status. +/// +/// DECISION: Unlike POSIX, Ashet OS uses a single boolean for success/failure +/// rather than an integer exit code. Integer codes are overloaded in +/// practice (is 2 "worse" than 1? is 0 always success?) and the +/// meaningful information is carried by log output, not codes. +/// +/// EXAMPLE: A well-behaved application terminates with: +/// +/// ```zig +/// process.terminate(.success); +/// ``` +``` + +JSON output: + +```json +{ + "sections": [ + { + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Defines the process exit status." } + ] + }] + }, + { + "kind": "decision", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Unlike POSIX, Ashet OS uses a single boolean for success/failure rather than an integer exit code. Integer codes are overloaded in practice (is 2 \"worse\" than 1? is 0 always success?) and the meaningful information is carried by log output, not codes." } + ] + }] + }, + { + "kind": "example", + "blocks": [ + { + "type": "paragraph", + "content": [ + { "type": "text", "value": "A well-behaved application terminates with:" } + ] + }, + { + "type": "code_block", + "syntax": "zig", + "content": "process.terminate(.success);" + } + ] + } + ] +} +``` + +### 6.5 Links + +Source: + +```abi +/// The timezone database follows the IANA format. +/// See [the IANA tz database](https://www.iana.org/time-zones) for details, +/// or the mirror at . +``` + +JSON output: + +```json +{ + "sections": [{ + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "The timezone database follows the IANA format. See " }, + { "type": "link", + "url": "https://www.iana.org/time-zones", + "content": [ + { "type": "text", "value": "the IANA tz database" } + ] + }, + { "type": "text", "value": " for details, or the mirror at " }, + { "type": "link", + "url": "https://github.com/eggert/tz", + "content": [ + { "type": "text", "value": "https://github.com/eggert/tz" } + ] + }, + { "type": "text", "value": "." } + ] + }] + }] +} +``` + +### 6.6 Minimal field doc (the common case) + +Source: + +```abi +/// The time is adjusted to the first possible past point in time. +``` + +JSON output: + +```json +{ + "sections": [{ + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "The time is adjusted to the first possible past point in time." } + ] + }] + }] +} +``` + +--- + +## 7. Grammar (EBNF) + +This grammar describes the surface syntax after `///` prefix stripping. + +```ebnf +(* Block level — line oriented *) + +doc_comment = { blank_line } , { section } ; + +section = [ admonition_start ] , block , { blank_line , block } ; + +admonition_start = tag , ":" , ws , text_line ; +tag = "NOTE" | "WARNING" | "LORE" | "EXAMPLE" + | "DEPRECATED" | "DECISION" ; + +block = code_block | unordered_list | ordered_list | paragraph ; + +code_block = "```" , [ syntax_id ] , newline , + { code_line , newline } , + "```" , newline ; +syntax_id = ident_char , { ident_char | "-" | "." | ":" } ; +code_line = { any_char_except_newline } ; + +unordered_list = ul_item , { ul_item } ; +ul_item = "- " , inline_text , newline , + { " " , continuation_text , newline } ; + +ordered_list = ol_item , { ol_item } ; +ol_item = digit , { digit } , ". " , inline_text , newline , + { " " , continuation_text , newline } ; + +paragraph = text_line , { continuation_text , newline } ; + +text_line = inline_text , newline ; +continuation_text = inline_text ; + +blank_line = newline ; + +(* Inline level — character oriented *) + +inline_text = { inline_item } ; + +inline_item = ref | code_span | emphasis | titled_link + | autolink | escape | plain_text ; + +ref = "@" , "`" , fqn_chars , "`" ; +code_span = "`" , code_chars , "`" ; +emphasis = "*" , inline_text , "*" ; (* see §4.2.3 for open/close rules *) +titled_link = "[" , inline_text , "]" , "(" , url_chars , ")" ; +autolink = "<" , url_scheme , url_chars , ">" ; +escape = "\\" , escapable_char ; + +fqn_chars = fqn_char , { fqn_char } ; +fqn_char = letter | digit | "_" | "." | "@" ; (* @"..." for escaped IDL names *) +code_chars = { any_char_except_backtick }- ; (* at least one character *) +url_scheme = "http://" | "https://" | "mailto:" ; +url_chars = { any_char_except_closing }- ; +escapable_char = "`" | "*" | "[" | "<" | "@" | "\\" ; +plain_text = { text_char }- ; +text_char = ? any character not starting another inline construct ? ; +``` + +--- + +## 8. Diagnostics + +The parser **must** emit diagnostics for the following conditions: + +| Condition | Severity | +|---|---| +| Unresolvable `@\`fqn\`` | Error | +| Ambiguous `@\`fqn\`` (matches at multiple scopes) | Warning | +| Unclosed backtick span | Error | +| Unclosed emphasis span | Error | +| Unclosed code fence | Error | +| Unclosed `[text](url)` link | Error | +| Empty code span (``` `` ```) | Error | +| Empty emphasis span (`**`) | Error | +| `TODO:` used as admonition tag | Warning | +| Trailing whitespace on doc lines | Warning (non-fatal) | +| Section with no blocks (parser bug) | Error | + +--- + +## 9. Migration guide + +The following table summarizes the changes needed to migrate existing doc comments: + +| Current pattern | New pattern | Count (est.) | +|---|---|---| +| `` `fqn` `` where fqn is a real declaration | `` @`fqn` `` | ~50-100 | +| `NOTE:` | `NOTE:` (unchanged) | 454 | +| `LORE:` | `LORE:` (unchanged) | 30 | +| `EXAMPLE:` | `EXAMPLE:` (unchanged) | 7 | +| `*emphasis*` | `*emphasis*` (unchanged) | ~10 | +| `` `code` `` (non-referencing) | `` `code` `` (unchanged) | ~500 | +| `- list item` | `- list item` (unchanged) | ~20 | +| `//? TODO:` (line comments) | `//? TODO:` (unchanged) | 67 | +| `/// TODO:` (in doc comments) | Move to `//? TODO:` | 1 | + +**Effective migration:** Add `@` prefix to backtick spans that reference IDL declarations. Everything else stays the same. diff --git a/src/tools/abi-mapper/src/abi-parser.zig b/src/tools/abi-mapper/src/abi-parser.zig index 057dd28c..a41a4cbf 100644 --- a/src/tools/abi-mapper/src/abi-parser.zig +++ b/src/tools/abi-mapper/src/abi-parser.zig @@ -4,6 +4,7 @@ const args_parser = @import("args"); pub const syntax = @import("syntax.zig"); pub const model = @import("model.zig"); pub const sema = @import("sema.zig"); +pub const doc_comment = @import("doc_comment.zig"); const CliOptions = struct { output: []const u8 = "", diff --git a/src/tools/abi-mapper/src/doc_comment.zig b/src/tools/abi-mapper/src/doc_comment.zig new file mode 100644 index 00000000..b0c838ec --- /dev/null +++ b/src/tools/abi-mapper/src/doc_comment.zig @@ -0,0 +1,389 @@ +const std = @import("std"); +const model = @import("model.zig"); + +const DocComment = model.DocComment; + +/// Parses raw doc comment lines (after `///` prefix stripping) into a structured DocComment. +/// Each raw line should be `token.text[3..]` where token.text starts with `///`. +pub fn parse(allocator: std.mem.Allocator, raw_lines: []const []const u8) !DocComment { + if (raw_lines.len == 0) return .empty; + + var ctx: ParseContext = .{ .allocator = allocator }; + return ctx.parse_doc(raw_lines); +} + +const AccKind = enum { none, paragraph, unordered_list, ordered_list }; + +const ParseContext = struct { + allocator: std.mem.Allocator, + + fn parse_doc(ctx: *ParseContext, raw_lines: []const []const u8) !DocComment { + const a = ctx.allocator; + + // Normalize lines: strip one optional leading space (the /// separator), right-trim. + var norm_lines: std.ArrayList([]const u8) = .empty; + defer norm_lines.deinit(a); + + for (raw_lines) |raw| { + const stripped = if (raw.len > 0 and raw[0] == ' ') raw[1..] else raw; + try norm_lines.append(a, std.mem.trimRight(u8, stripped, " \t")); + } + const lines = norm_lines.items; + + var sections: std.ArrayList(DocComment.Section) = .empty; + defer sections.deinit(a); + + var current_kind: DocComment.Section.Kind = .main; + var blocks: std.ArrayList(DocComment.Block) = .empty; + defer blocks.deinit(a); + + // Current paragraph lines accumulator + var para_lines: std.ArrayList([]const u8) = .empty; + defer para_lines.deinit(a); + + // Current list items (each item is a list of lines) + var list_items: std.ArrayList(std.ArrayList([]const u8)) = .empty; + defer { + for (list_items.items) |*item| item.deinit(a); + list_items.deinit(a); + } + + var acc_kind: AccKind = .none; + + // Code fence state + var in_fence = false; + var fence_syntax: ?[]const u8 = null; + var fence_lines: std.ArrayList([]const u8) = .empty; + defer fence_lines.deinit(a); + + for (lines) |line| { + if (in_fence) { + if (std.mem.eql(u8, line, "```")) { + const content = try std.mem.join(a, "\n", fence_lines.items); + try blocks.append(a, .{ .code_block = .{ .syntax = fence_syntax, .content = content } }); + in_fence = false; + fence_syntax = null; + fence_lines.clearRetainingCapacity(); + } else { + try fence_lines.append(a, line); + } + continue; + } + + // Code fence start + if (std.mem.startsWith(u8, line, "```")) { + try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + const syn = std.mem.trim(u8, line[3..], " "); + fence_syntax = if (syn.len > 0) try a.dupe(u8, syn) else null; + in_fence = true; + continue; + } + + // Blank line: flush current block + if (line.len == 0) { + try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + continue; + } + + // Admonition tag: NOTE:, WARNING:, LORE:, EXAMPLE:, DEPRECATED:, DECISION: + if (parse_admonition(line)) |adm| { + try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + // Emit current section if it has content + if (blocks.items.len > 0) { + try sections.append(a, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(a) }); + } + current_kind = adm.kind; + acc_kind = .paragraph; + if (adm.text.len > 0) { + try para_lines.append(a, adm.text); + } + continue; + } + + // Unordered list item: starts with "- " + if (std.mem.startsWith(u8, line, "- ")) { + if (acc_kind != .unordered_list) { + try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + acc_kind = .unordered_list; + } + var new_item: std.ArrayList([]const u8) = .empty; + try new_item.append(a, line[2..]); + try list_items.append(a, new_item); + continue; + } + + // Ordered list item: starts with "N. " + if (parse_ordered_item(line)) |text| { + if (acc_kind != .ordered_list) { + try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + acc_kind = .ordered_list; + } + var new_item: std.ArrayList([]const u8) = .empty; + try new_item.append(a, text); + try list_items.append(a, new_item); + continue; + } + + // Continuation of current list item (indented by 2+ spaces) + if ((acc_kind == .unordered_list or acc_kind == .ordered_list) and + list_items.items.len > 0 and + std.mem.startsWith(u8, line, " ")) + { + const cont = std.mem.trimLeft(u8, line, " "); + try list_items.items[list_items.items.len - 1].append(a, cont); + continue; + } + + // Paragraph (default): accumulate lines, trimming leading whitespace + // so that admonition continuation indentation is normalized. + if (acc_kind != .paragraph) { + try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + acc_kind = .paragraph; + } + try para_lines.append(a, std.mem.trimLeft(u8, line, " \t")); + } + + // Flush whatever remains + try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + + // Emit the final section + if (blocks.items.len > 0) { + try sections.append(a, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(a) }); + } + + if (sections.items.len == 0) return .empty; + + return .{ .sections = try sections.toOwnedSlice(a) }; + } + + fn flush_acc( + ctx: *ParseContext, + a: std.mem.Allocator, + blocks: *std.ArrayList(DocComment.Block), + acc_kind: *AccKind, + para_lines: *std.ArrayList([]const u8), + list_items: *std.ArrayList(std.ArrayList([]const u8)), + ) !void { + switch (acc_kind.*) { + .none => {}, + .paragraph => { + if (para_lines.items.len > 0) { + const text = try std.mem.join(a, " ", para_lines.items); + const content = try ctx.parse_inline(text); + try blocks.append(a, .{ .paragraph = .{ .content = content } }); + para_lines.clearRetainingCapacity(); + } + }, + .unordered_list, .ordered_list => { + const items = try a.alloc([]const DocComment.Inline, list_items.items.len); + for (list_items.items, 0..) |item_lines, j| { + const text = try std.mem.join(a, " ", item_lines.items); + items[j] = try ctx.parse_inline(text); + } + for (list_items.items) |*item| item.deinit(a); + list_items.clearRetainingCapacity(); + if (acc_kind.* == .unordered_list) { + try blocks.append(a, .{ .unordered_list = .{ .items = items } }); + } else { + try blocks.append(a, .{ .ordered_list = .{ .items = items } }); + } + }, + } + acc_kind.* = .none; + } + + fn parse_inline(ctx: *ParseContext, text: []const u8) ![]const DocComment.Inline { + const a = ctx.allocator; + var result: std.ArrayList(DocComment.Inline) = .empty; + defer result.deinit(a); + + var text_start: usize = 0; + var i: usize = 0; + + while (i < text.len) { + const c = text[i]; + + // Escape sequence: \` \* \[ \< \@ \\ + if (c == '\\' and i + 1 < text.len) { + const next = text[i + 1]; + const escapable = switch (next) { + '`', '*', '[', '<', '@', '\\' => true, + else => false, + }; + if (escapable) { + if (i > text_start) { + try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + } + try result.append(a, .{ .text = .{ .value = text[i + 1 .. i + 2] } }); + i += 2; + text_start = i; + continue; + } + } + + // Cross-reference: @`fqn` + if (c == '@' and i + 1 < text.len and text[i + 1] == '`') { + if (i > text_start) { + try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + } + const ref_start = i + 2; + if (std.mem.indexOfScalar(u8, text[ref_start..], '`')) |rel_end| { + const fqn = text[ref_start .. ref_start + rel_end]; + try result.append(a, .{ .ref = .{ .fqn = fqn } }); + i = ref_start + rel_end + 1; + text_start = i; + } else { + // Unmatched backtick — treat as literal text + i += 1; + } + continue; + } + + // Inline code: `text` + if (c == '`') { + if (i > text_start) { + try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + } + const code_start = i + 1; + if (std.mem.indexOfScalar(u8, text[code_start..], '`')) |rel_end| { + const code_val = text[code_start .. code_start + rel_end]; + try result.append(a, .{ .code = .{ .value = code_val } }); + i = code_start + rel_end + 1; + text_start = i; + } else { + i += 1; + } + continue; + } + + // Emphasis: *content* + // Opening * must be preceded by whitespace or start-of-text. + if (c == '*') { + const at_word_start = (i == 0 or text[i - 1] == ' ' or text[i - 1] == '\t'); + if (at_word_start and i + 1 < text.len) { + const em_start = i + 1; + var j = em_start; + var found_close: ?usize = null; + while (j < text.len) : (j += 1) { + if (text[j] == '*') { + // Closing * must be followed by whitespace, non-alphanumeric, or end-of-text + const after_close = j + 1; + const valid_close = (after_close >= text.len or + !std.ascii.isAlphanumeric(text[after_close])); + if (valid_close and j > em_start) { + found_close = j; + break; + } + } + } + if (found_close) |close_pos| { + if (i > text_start) { + try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + } + const inner = text[em_start..close_pos]; + const inner_content = try ctx.parse_inline(inner); + try result.append(a, .{ .emphasis = .{ .content = inner_content } }); + i = close_pos + 1; + text_start = i; + continue; + } + } + } + + // Titled link: [display](url) + if (c == '[') { + if (std.mem.indexOfScalarPos(u8, text, i + 1, ']')) |close_bracket| { + if (close_bracket + 1 < text.len and text[close_bracket + 1] == '(') { + if (std.mem.indexOfScalarPos(u8, text, close_bracket + 2, ')')) |close_paren| { + if (i > text_start) { + try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + } + const display = text[i + 1 .. close_bracket]; + const url = text[close_bracket + 2 .. close_paren]; + const content = try ctx.parse_inline(display); + try result.append(a, .{ .link = .{ .url = url, .content = content } }); + i = close_paren + 1; + text_start = i; + continue; + } + } + } + } + + // Autolink: + if (c == '<') { + const url_schemes = [_][]const u8{ "http://", "https://", "mailto:" }; + var matched_scheme = false; + for (url_schemes) |scheme| { + if (i + 1 + scheme.len <= text.len and + std.mem.eql(u8, text[i + 1 .. i + 1 + scheme.len], scheme)) + { + matched_scheme = true; + break; + } + } + if (matched_scheme) { + if (std.mem.indexOfScalarPos(u8, text, i + 1, '>')) |close_angle| { + if (i > text_start) { + try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + } + const url = text[i + 1 .. close_angle]; + const content = try a.alloc(DocComment.Inline, 1); + content[0] = .{ .text = .{ .value = url } }; + try result.append(a, .{ .link = .{ .url = url, .content = content } }); + i = close_angle + 1; + text_start = i; + continue; + } + } + } + + i += 1; + } + + // Flush remaining literal text + if (text_start < text.len) { + try result.append(a, .{ .text = .{ .value = text[text_start..] } }); + } + + return result.toOwnedSlice(a); + } +}; + +const AdmonitionResult = struct { + kind: DocComment.Section.Kind, + text: []const u8, +}; + +fn parse_admonition(line: []const u8) ?AdmonitionResult { + const Tag = struct { tag: []const u8, kind: DocComment.Section.Kind }; + const tags = [_]Tag{ + .{ .tag = "NOTE", .kind = .note }, + .{ .tag = "WARNING", .kind = .warning }, + .{ .tag = "LORE", .kind = .lore }, + .{ .tag = "EXAMPLE", .kind = .example }, + .{ .tag = "DEPRECATED", .kind = .deprecated }, + .{ .tag = "DECISION", .kind = .decision }, + }; + + for (tags) |entry| { + if (std.mem.startsWith(u8, line, entry.tag)) { + const rest = line[entry.tag.len..]; + if (std.mem.startsWith(u8, rest, ": ")) { + return .{ .kind = entry.kind, .text = rest[2..] }; + } else if (std.mem.eql(u8, rest, ":")) { + return .{ .kind = entry.kind, .text = "" }; + } + } + } + return null; +} + +fn parse_ordered_item(line: []const u8) ?[]const u8 { + var i: usize = 0; + while (i < line.len and std.ascii.isDigit(line[i])) : (i += 1) {} + if (i == 0 or i >= line.len) return null; + if (line[i] != '.') return null; + if (i + 1 >= line.len or line[i + 1] != ' ') return null; + return line[i + 2 ..]; +} diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index b442852d..17c7b08b 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -25,8 +25,243 @@ pub fn to_json_str(document: Document, writer: anytype) !void { /// A full qualified name is a name consisting of a sequence of namespaces and ending with the actual name. pub const FQN = []const []const u8; -/// A documentation string is a sequence of text lines. -pub const DocString = []const []const u8; +/// A documentation comment with structured content. +pub const DocComment = struct { + sections: []const Section, + + pub const empty: DocComment = .{ .sections = &.{} }; + + pub const Section = struct { + kind: Kind, + blocks: []const Block, + + pub const Kind = enum { + main, + note, + warning, + lore, + example, + deprecated, + decision, + }; + }; + + pub const Block = union(enum) { + paragraph: Paragraph, + unordered_list: UnorderedList, + ordered_list: OrderedList, + code_block: CodeBlock, + + pub const Paragraph = struct { + content: []const Inline, + }; + + pub const UnorderedList = struct { + items: []const []const Inline, + }; + + pub const OrderedList = struct { + items: []const []const Inline, + }; + + pub const CodeBlock = struct { + syntax: ?[]const u8, + content: []const u8, + }; + + pub fn jsonStringify(block: Block, jws: anytype) !void { + try jws.beginObject(); + switch (block) { + .paragraph => |p| { + try jws.objectField("type"); + try jws.write("paragraph"); + try jws.objectField("content"); + try jws.write(p.content); + }, + .unordered_list => |ul| { + try jws.objectField("type"); + try jws.write("unordered_list"); + try jws.objectField("items"); + try jws.write(ul.items); + }, + .ordered_list => |ol| { + try jws.objectField("type"); + try jws.write("ordered_list"); + try jws.objectField("items"); + try jws.write(ol.items); + }, + .code_block => |cb| { + try jws.objectField("type"); + try jws.write("code_block"); + try jws.objectField("syntax"); + try jws.write(cb.syntax); + try jws.objectField("content"); + try jws.write(cb.content); + }, + } + try jws.endObject(); + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) std.json.ParseError(@TypeOf(source.*))!Block { + const dynamic = try std.json.innerParse(std.json.Value, allocator, source, options); + return block_from_value(allocator, dynamic) catch return error.UnexpectedToken; + } + }; + + pub const Inline = union(enum) { + text: Text, + code: Code, + emphasis: Emphasis, + ref: Ref, + link: Link, + + pub const Text = struct { value: []const u8 }; + pub const Code = struct { value: []const u8 }; + pub const Emphasis = struct { content: []const Inline }; + pub const Ref = struct { fqn: []const u8 }; + pub const Link = struct { url: []const u8, content: []const Inline }; + + pub fn jsonStringify(inl: Inline, jws: anytype) !void { + try jws.beginObject(); + switch (inl) { + .text => |t| { + try jws.objectField("type"); + try jws.write("text"); + try jws.objectField("value"); + try jws.write(t.value); + }, + .code => |c| { + try jws.objectField("type"); + try jws.write("code"); + try jws.objectField("value"); + try jws.write(c.value); + }, + .emphasis => |e| { + try jws.objectField("type"); + try jws.write("emphasis"); + try jws.objectField("content"); + try jws.write(e.content); + }, + .ref => |r| { + try jws.objectField("type"); + try jws.write("ref"); + try jws.objectField("fqn"); + try jws.write(r.fqn); + }, + .link => |l| { + try jws.objectField("type"); + try jws.write("link"); + try jws.objectField("url"); + try jws.write(l.url); + try jws.objectField("content"); + try jws.write(l.content); + }, + } + try jws.endObject(); + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) std.json.ParseError(@TypeOf(source.*))!Inline { + const dynamic = try std.json.innerParse(std.json.Value, allocator, source, options); + return inline_from_value(allocator, dynamic) catch return error.UnexpectedToken; + } + }; +}; + +fn block_from_value(allocator: std.mem.Allocator, value: std.json.Value) !DocComment.Block { + const obj = switch (value) { + .object => |o| o, + else => return error.UnexpectedToken, + }; + const type_str = switch (obj.get("type") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + if (std.mem.eql(u8, type_str, "paragraph")) { + const cv = obj.get("content") orelse return error.UnexpectedToken; + return .{ .paragraph = .{ .content = try inline_array_from_value(allocator, cv) } }; + } else if (std.mem.eql(u8, type_str, "unordered_list")) { + const iv = obj.get("items") orelse return error.UnexpectedToken; + return .{ .unordered_list = .{ .items = try inline_array_array_from_value(allocator, iv) } }; + } else if (std.mem.eql(u8, type_str, "ordered_list")) { + const iv = obj.get("items") orelse return error.UnexpectedToken; + return .{ .ordered_list = .{ .items = try inline_array_array_from_value(allocator, iv) } }; + } else if (std.mem.eql(u8, type_str, "code_block")) { + const syntax_v = obj.get("syntax"); + const syntax: ?[]const u8 = if (syntax_v) |sv| switch (sv) { + .string => |s| s, + .null => null, + else => return error.UnexpectedToken, + } else null; + const content = switch (obj.get("content") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + return .{ .code_block = .{ .syntax = syntax, .content = content } }; + } + return error.UnexpectedToken; +} + +fn inline_from_value(allocator: std.mem.Allocator, value: std.json.Value) !DocComment.Inline { + const obj = switch (value) { + .object => |o| o, + else => return error.UnexpectedToken, + }; + const type_str = switch (obj.get("type") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + if (std.mem.eql(u8, type_str, "text")) { + return .{ .text = .{ .value = switch (obj.get("value") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + } } }; + } else if (std.mem.eql(u8, type_str, "code")) { + return .{ .code = .{ .value = switch (obj.get("value") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + } } }; + } else if (std.mem.eql(u8, type_str, "emphasis")) { + const cv = obj.get("content") orelse return error.UnexpectedToken; + return .{ .emphasis = .{ .content = try inline_array_from_value(allocator, cv) } }; + } else if (std.mem.eql(u8, type_str, "ref")) { + return .{ .ref = .{ .fqn = switch (obj.get("fqn") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + } } }; + } else if (std.mem.eql(u8, type_str, "link")) { + const url = switch (obj.get("url") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + const cv = obj.get("content") orelse return error.UnexpectedToken; + return .{ .link = .{ .url = url, .content = try inline_array_from_value(allocator, cv) } }; + } + return error.UnexpectedToken; +} + +fn inline_array_from_value(allocator: std.mem.Allocator, value: std.json.Value) ![]const DocComment.Inline { + const arr = switch (value) { + .array => |a| a, + else => return error.UnexpectedToken, + }; + const result = try allocator.alloc(DocComment.Inline, arr.items.len); + for (arr.items, 0..) |item, i| { + result[i] = try inline_from_value(allocator, item); + } + return result; +} + +fn inline_array_array_from_value(allocator: std.mem.Allocator, value: std.json.Value) ![]const []const DocComment.Inline { + const arr = switch (value) { + .array => |a| a, + else => return error.UnexpectedToken, + }; + const result = try allocator.alloc([]const DocComment.Inline, arr.items.len); + for (arr.items, 0..) |item, i| { + result[i] = try inline_array_from_value(allocator, item); + } + return result; +} /// Returns the last item of the full qualified name. pub fn local_name(fqn: FQN) []const u8 { @@ -93,7 +328,7 @@ pub const ConstantIndex = GenericIndex(Constant, "constants"); pub const TypeIndex = GenericIndex(Type, "types"); pub const Declaration = struct { - docs: DocString, + docs: DocComment, full_qualified_name: FQN, children: []const Declaration, data: Data, @@ -242,20 +477,20 @@ pub const ArrayType = struct { pub const TypeId = std.meta.Tag(Type); pub const ExternalType = struct { - docs: DocString, + docs: DocComment, full_qualified_name: FQN, alias: []const u8, }; pub const TypeDefition = struct { - docs: DocString, + docs: DocComment, full_qualified_name: FQN, alias: TypeIndex, }; pub const BitStruct = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, backing_type: StandardType, bit_count: u8, @@ -264,7 +499,7 @@ pub const BitStruct = struct { }; pub const BitStructField = struct { - docs: DocString, + docs: DocComment, name: ?[]const u8, // null is reserved type: TypeIndex, default: ?Value, @@ -275,14 +510,14 @@ pub const BitStructField = struct { pub const Struct = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, logic_fields: []const StructField, native_fields: []const StructField, }; pub const StructField = struct { - docs: DocString, + docs: DocComment, name: []const u8, type: TypeIndex, default: ?Value, @@ -298,7 +533,7 @@ pub const StructFieldRole = union(enum) { pub const Enumeration = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, backing_type: StandardType, kind: Kind, @@ -312,14 +547,14 @@ pub const Enumeration = struct { }; pub const EnumItem = struct { - docs: DocString, + docs: DocComment, name: []const u8, value: i65, }; pub const GenericCall = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, no_return: bool, @@ -333,7 +568,7 @@ pub const GenericCall = struct { }; pub const Parameter = struct { - docs: DocString, + docs: DocComment, name: []const u8, type: TypeIndex, default: ?Value, @@ -377,19 +612,19 @@ pub const ParameterRole = union(enum) { pub const Resource = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, }; pub const Error = struct { - docs: DocString, + docs: DocComment, name: []const u8, value: u32, }; pub const Constant = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, type: ?TypeIndex, value: Value, diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index b08e941d..8f062836 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -1,6 +1,7 @@ const std = @import("std"); const model = @import("model.zig"); const syntax = @import("syntax.zig"); +const doc_comment_parser = @import("doc_comment.zig"); const Location = syntax.Location; @@ -327,7 +328,7 @@ const Analyzer = struct { // TODO: Implement stable item id assignment! try items.append(ana.allocator, .{ - .docs = &.{}, + .docs = .empty, .name = try item_name.toOwnedSlice(ana.allocator), .value = @intCast(index), }); @@ -620,9 +621,9 @@ const Analyzer = struct { .default = null, }); try list.append(a.allocator, .{ - .docs = try a.allocator.dupe([]const u8, &.{ + .docs = try a.synthetic_doc( try a.format("The number of elements referenced by {s}_ptr.", .{param.name}), - }), + ), .name = len_name, .type = try a.map_model_type(.{ .well_known = .usize }), .role = switch (mode) { @@ -649,7 +650,7 @@ const Analyzer = struct { try native_outputs.append(ana.allocator, .{ .name = "error_code", .default = null, - .docs = &.{}, + .docs = .empty, .role = .@"error", .type = try ana.map_model_type(.{ .well_known = .u16 }), }); @@ -684,7 +685,7 @@ const Analyzer = struct { fn emit_slice( h: @This(), basename: []const u8, - docs: model.DocString, + docs: model.DocComment, ptr_type: model.Type, ) !void { try h.nf.append(h.ana.allocator, .{ @@ -695,9 +696,9 @@ const Analyzer = struct { .default = null, }); try h.nf.append(h.ana.allocator, .{ - .docs = try h.ana.allocator.dupe([]const u8, &.{ + .docs = try h.ana.synthetic_doc( try h.ana.format("The number of elements referenced by {s}_ptr.", .{basename}), - }), + ), .name = try h.ana.format("{s}_len", .{basename}), .type = try h.ana.map_model_type(.{ .well_known = .usize }), .role = .{ .slice_len = basename }, @@ -727,9 +728,9 @@ const Analyzer = struct { .default = null, }); try native_fields.append(ana.allocator, .{ - .docs = try ana.allocator.dupe([]const u8, &.{ + .docs = try ana.synthetic_doc( try ana.format("The amount of bytes referenced by {s}_ptr.", .{fld.name}), - }), + ), .name = try ana.format("{s}_len", .{fld.name}), .type = try ana.map_model_type(.{ .well_known = .usize }), .role = .{ .slice_len = fld.name }, @@ -761,9 +762,9 @@ const Analyzer = struct { .default = null, }); try native_fields.append(ana.allocator, .{ - .docs = try ana.allocator.dupe([]const u8, &.{ + .docs = try ana.synthetic_doc( try ana.format("The amount of bytes referenced by {s}_ptr.", .{fld.name}), - }), + ), .name = try ana.format("{s}_len", .{fld.name}), .type = try ana.map_model_type(.{ .well_known = .usize }), .role = .{ .slice_len = fld.name }, @@ -884,7 +885,7 @@ const Analyzer = struct { const full_name, const scope = try ana.push_scope(typedef.name, .typedef); defer ana.pop_scope(); - const doc_comment = try ana.allocator.dupe([]const u8, node.doc_comment); + const doc_comment = try ana.map_doc_comment(node.doc_comment); const alias_id = try ana.map_type(typedef.alias); @@ -912,7 +913,7 @@ const Analyzer = struct { const full_name, const scope = try ana.push_scope(constant.name, .constant); defer ana.pop_scope(); - const doc_comment = try ana.allocator.dupe([]const u8, node.doc_comment); + const doc_comment = try ana.map_doc_comment(node.doc_comment); const value = try ana.resolve_value(constant.value.?); @@ -942,63 +943,25 @@ const Analyzer = struct { const NodeInfo = struct { full_name: model.FQN, - docs: model.DocString, + docs: model.DocComment, sub_type: ?model.StandardType, location: Location, }; - /// Strips empty heads and tails, then left-aligns a doc comment - fn map_doc_comment(ana: *Analyzer, doc_comment: []const []const u8) !model.DocString { - const ws = " "; - - var output = try ana.allocator.dupe([]const u8, doc_comment); - - // right-trim all lines - for (output) |*item| { - item.* = std.mem.trimRight(u8, item.*, ws); - } - - // trim empty heads: - while (output.len > 0 and output[0].len == 0) { - output = output[1..]; - } - - // trim empty tails: - while (output.len > 0 and output[output.len - 1].len == 0) { - output = output[0 .. output.len - 1]; - } - - // Determine common whitespace prefix length: - var common_prefix_len: usize = std.math.maxInt(usize); - for (output) |line| { - if (line.len == 0) - continue; - - const prefix_len = for (line, 0..) |c, i| { - if (std.mem.indexOfScalar(u8, ws, c) == null) - break i; - } else unreachable; // lines are non-empty, and they must contain at least a non-space char - - common_prefix_len = @min(common_prefix_len, prefix_len); - } - - // trim common prefix: - for (output) |*line| { - if (line.len == 0) - continue; - - const prefix = line.*[0..common_prefix_len]; - - // Prefix must be only whitespace: - std.debug.assert(for (prefix) |c| { - if (std.mem.indexOfScalar(u8, ws, c) == null) - break false; - } else true); - - line.* = line.*[common_prefix_len..]; - } + /// Parses a raw doc comment into a structured DocComment. + fn map_doc_comment(ana: *Analyzer, raw_lines: []const []const u8) !model.DocComment { + return doc_comment_parser.parse(ana.allocator, raw_lines); + } - return output; + /// Creates a synthetic one-paragraph DocComment from a plain text string. + fn synthetic_doc(ana: *Analyzer, text: []const u8) !model.DocComment { + const inlines = try ana.allocator.alloc(model.DocComment.Inline, 1); + inlines[0] = .{ .text = .{ .value = text } }; + const blocks = try ana.allocator.alloc(model.DocComment.Block, 1); + blocks[0] = .{ .paragraph = .{ .content = inlines } }; + const sections = try ana.allocator.alloc(model.DocComment.Section, 1); + sections[0] = .{ .kind = .main, .blocks = blocks }; + return .{ .sections = sections }; } fn map_decl(ana: *Analyzer, node: syntax.Node) !model.Declaration { diff --git a/src/tools/abi-mapper/tests/doc_parser.zig b/src/tools/abi-mapper/tests/doc_parser.zig new file mode 100644 index 00000000..f68b020c --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_parser.zig @@ -0,0 +1,425 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); +const doc_comment_parser = abi_parser.doc_comment; +const DocComment = abi_parser.model.DocComment; + +// Helper: parse raw lines (as produced by the tokenizer after stripping `///`) +// Lines with `/// text` produce `" text"` (one leading space). +// Lines with `///` (empty doc line) produce `""`. +fn parse(arena: std.mem.Allocator, lines: []const []const u8) !DocComment { + return doc_comment_parser.parse(arena, lines); +} + +// ── Empty / blank ──────────────────────────────────────────────────────────── + +test "empty input returns empty DocComment" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{}); + try std.testing.expectEqual(@as(usize, 0), result.sections.len); +} + +test "only blank lines returns empty DocComment" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ "", "", "" }); + try std.testing.expectEqual(@as(usize, 0), result.sections.len); +} + +// ── Paragraphs ─────────────────────────────────────────────────────────────── + +test "simple paragraph" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{" Hello, world!"}); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.main, result.sections[0].kind); + try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + + const block = result.sections[0].blocks[0]; + try std.testing.expect(block == .paragraph); + try std.testing.expectEqual(@as(usize, 1), block.paragraph.content.len); + try std.testing.expect(block.paragraph.content[0] == .text); + try std.testing.expectEqualStrings("Hello, world!", block.paragraph.content[0].text.value); +} + +test "multi-line paragraph joined with space" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " First line", + " second line", + " third line", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + + const block = result.sections[0].blocks[0]; + try std.testing.expect(block == .paragraph); + try std.testing.expectEqual(@as(usize, 1), block.paragraph.content.len); + try std.testing.expectEqualStrings( + "First line second line third line", + block.paragraph.content[0].text.value, + ); +} + +test "blank line separates paragraphs" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " First paragraph.", + "", + " Second paragraph.", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(@as(usize, 2), result.sections[0].blocks.len); + try std.testing.expect(result.sections[0].blocks[0] == .paragraph); + try std.testing.expect(result.sections[0].blocks[1] == .paragraph); + + try std.testing.expectEqualStrings( + "First paragraph.", + result.sections[0].blocks[0].paragraph.content[0].text.value, + ); + try std.testing.expectEqualStrings( + "Second paragraph.", + result.sections[0].blocks[1].paragraph.content[0].text.value, + ); +} + +// ── Inline elements ────────────────────────────────────────────────────────── + +test "inline code span" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{" Call `foo()` now."}); + const content = result.sections[0].blocks[0].paragraph.content; + + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("Call ", content[0].text.value); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("foo()", content[1].code.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" now.", content[2].text.value); +} + +test "cross-reference @`fqn`" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{" See @`foo.bar.Baz` for details."}); + const content = result.sections[0].blocks[0].paragraph.content; + + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("See ", content[0].text.value); + try std.testing.expect(content[1] == .ref); + try std.testing.expectEqualStrings("foo.bar.Baz", content[1].ref.fqn); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" for details.", content[2].text.value); +} + +test "emphasis *text*" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{" This is *important* text."}); + const content = result.sections[0].blocks[0].paragraph.content; + + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("This is ", content[0].text.value); + try std.testing.expect(content[1] == .emphasis); + try std.testing.expectEqual(@as(usize, 1), content[1].emphasis.content.len); + try std.testing.expectEqualStrings("important", content[1].emphasis.content[0].text.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" text.", content[2].text.value); +} + +test "escape sequences suppress special syntax" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + // \` prevents inline code, \* prevents emphasis + const result = try parse(arena.allocator(), &.{" Escape: \\`not code\\`."}); + const content = result.sections[0].blocks[0].paragraph.content; + + // None of the nodes should be a .code span + for (content) |item| { + try std.testing.expect(item != .code); + } + // First text node is "Escape: " + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("Escape: ", content[0].text.value); + // Second text node is the escaped backtick character + try std.testing.expect(content[1] == .text); + try std.testing.expectEqualStrings("`", content[1].text.value); +} + +test "titled link [display](url)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{" See [the docs](https://example.com/docs)."}); + const content = result.sections[0].blocks[0].paragraph.content; + + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("See ", content[0].text.value); + try std.testing.expect(content[1] == .link); + try std.testing.expectEqualStrings("https://example.com/docs", content[1].link.url); + try std.testing.expectEqual(@as(usize, 1), content[1].link.content.len); + try std.testing.expectEqualStrings("the docs", content[1].link.content[0].text.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(".", content[2].text.value); +} + +test "autolink " { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{" Visit ."}); + const content = result.sections[0].blocks[0].paragraph.content; + + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("Visit ", content[0].text.value); + try std.testing.expect(content[1] == .link); + try std.testing.expectEqualStrings("https://example.com", content[1].link.url); + try std.testing.expectEqual(@as(usize, 1), content[1].link.content.len); + try std.testing.expectEqualStrings("https://example.com", content[1].link.content[0].text.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(".", content[2].text.value); +} + +test "autolink " { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{" Mail us."}); + const content = result.sections[0].blocks[0].paragraph.content; + + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[1] == .link); + try std.testing.expectEqualStrings("mailto:foo@example.com", content[1].link.url); +} + +// ── Admonitions ────────────────────────────────────────────────────────────── + +test "NOTE admonition starts new section" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " Main text.", + "", + " NOTE: This is a note.", + }); + try std.testing.expectEqual(@as(usize, 2), result.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.main, result.sections[0].kind); + try std.testing.expectEqual(DocComment.Section.Kind.note, result.sections[1].kind); + + const note_content = result.sections[1].blocks[0].paragraph.content; + try std.testing.expectEqualStrings("This is a note.", note_content[0].text.value); +} + +test "all admonition kinds are recognized" { + const Case = struct { tag: []const u8, kind: DocComment.Section.Kind }; + const cases = [_]Case{ + .{ .tag = "NOTE", .kind = .note }, + .{ .tag = "WARNING", .kind = .warning }, + .{ .tag = "LORE", .kind = .lore }, + .{ .tag = "EXAMPLE", .kind = .example }, + .{ .tag = "DEPRECATED", .kind = .deprecated }, + .{ .tag = "DECISION", .kind = .decision }, + }; + + for (cases) |c| { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var buf: [64]u8 = undefined; + const line = try std.fmt.bufPrint(&buf, " {s}: test text", .{c.tag}); + const result = try parse(arena.allocator(), &.{line}); + + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(c.kind, result.sections[0].kind); + } +} + +test "admonition with empty body starts section" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " NOTE:", + " Text on the next line.", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.note, result.sections[0].kind); + try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + try std.testing.expectEqualStrings( + "Text on the next line.", + result.sections[0].blocks[0].paragraph.content[0].text.value, + ); +} + +test "multiple admonition sections" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " Main description.", + "", + " NOTE: Important note.", + "", + " WARNING: Be careful.", + }); + try std.testing.expectEqual(@as(usize, 3), result.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.main, result.sections[0].kind); + try std.testing.expectEqual(DocComment.Section.Kind.note, result.sections[1].kind); + try std.testing.expectEqual(DocComment.Section.Kind.warning, result.sections[2].kind); +} + +// ── Lists ──────────────────────────────────────────────────────────────────── + +test "unordered list" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " - First item", + " - Second item", + " - Third item", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + + const block = result.sections[0].blocks[0]; + try std.testing.expect(block == .unordered_list); + try std.testing.expectEqual(@as(usize, 3), block.unordered_list.items.len); + try std.testing.expectEqualStrings("First item", block.unordered_list.items[0][0].text.value); + try std.testing.expectEqualStrings("Second item", block.unordered_list.items[1][0].text.value); + try std.testing.expectEqualStrings("Third item", block.unordered_list.items[2][0].text.value); +} + +test "ordered list" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " 1. First", + " 2. Second", + " 3. Third", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + + const block = result.sections[0].blocks[0]; + try std.testing.expect(block == .ordered_list); + try std.testing.expectEqual(@as(usize, 3), block.ordered_list.items.len); + try std.testing.expectEqualStrings("First", block.ordered_list.items[0][0].text.value); + try std.testing.expectEqualStrings("Second", block.ordered_list.items[1][0].text.value); + try std.testing.expectEqualStrings("Third", block.ordered_list.items[2][0].text.value); +} + +test "list item continuation line" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " - First item", + " continues here", + " - Second item", + }); + const block = result.sections[0].blocks[0]; + try std.testing.expect(block == .unordered_list); + try std.testing.expectEqual(@as(usize, 2), block.unordered_list.items.len); + // The two continuation lines are joined with a space + try std.testing.expectEqualStrings( + "First item continues here", + block.unordered_list.items[0][0].text.value, + ); + try std.testing.expectEqualStrings("Second item", block.unordered_list.items[1][0].text.value); +} + +test "paragraph after list" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " - Item one", + " - Item two", + "", + " Trailing paragraph.", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(@as(usize, 2), result.sections[0].blocks.len); + try std.testing.expect(result.sections[0].blocks[0] == .unordered_list); + try std.testing.expect(result.sections[0].blocks[1] == .paragraph); +} + +// ── Code fences ────────────────────────────────────────────────────────────── + +test "code fence without syntax hint" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " ```", + " some code", + " more code", + " ```", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + + const block = result.sections[0].blocks[0]; + try std.testing.expect(block == .code_block); + try std.testing.expectEqual(@as(?[]const u8, null), block.code_block.syntax); + try std.testing.expectEqualStrings("some code\nmore code", block.code_block.content); +} + +test "code fence with syntax hint" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " ```zig", + " const x = 42;", + " ```", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + + const block = result.sections[0].blocks[0]; + try std.testing.expect(block == .code_block); + try std.testing.expect(block.code_block.syntax != null); + try std.testing.expectEqualStrings("zig", block.code_block.syntax.?); + try std.testing.expectEqualStrings("const x = 42;", block.code_block.content); +} + +test "code fence preceded and followed by text" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const result = try parse(arena.allocator(), &.{ + " Before.", + "", + " ```", + " code here", + " ```", + "", + " After.", + }); + try std.testing.expectEqual(@as(usize, 1), result.sections.len); + try std.testing.expectEqual(@as(usize, 3), result.sections[0].blocks.len); + try std.testing.expect(result.sections[0].blocks[0] == .paragraph); + try std.testing.expect(result.sections[0].blocks[1] == .code_block); + try std.testing.expect(result.sections[0].blocks[2] == .paragraph); +} From d788bf2806ddb7baaf3169d0d7030c8ae2bd37bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 2 Mar 2026 16:06:56 +0100 Subject: [PATCH 02/36] Improves memory management strategy. --- src/tools/abi-mapper/src/doc_comment.zig | 141 +++++++----- src/tools/abi-mapper/src/sema.zig | 3 +- src/tools/abi-mapper/tests/doc_parser.zig | 261 +++++++++------------- 3 files changed, 191 insertions(+), 214 deletions(-) diff --git a/src/tools/abi-mapper/src/doc_comment.zig b/src/tools/abi-mapper/src/doc_comment.zig index b0c838ec..eb56b20e 100644 --- a/src/tools/abi-mapper/src/doc_comment.zig +++ b/src/tools/abi-mapper/src/doc_comment.zig @@ -3,12 +3,39 @@ const model = @import("model.zig"); const DocComment = model.DocComment; -/// Parses raw doc comment lines (after `///` prefix stripping) into a structured DocComment. +/// A parsed doc comment together with the arena that owns its memory. +/// Call deinit() when the DocComment is no longer needed. +pub const ParsedDocComment = struct { + arena: std.heap.ArenaAllocator, + comment: DocComment, + + pub fn deinit(self: *ParsedDocComment) void { + self.arena.deinit(); + } +}; + +/// Parses raw doc comment lines into a freshly allocated arena. +/// The caller owns the result and must call deinit() to release memory. +/// +/// Each raw line should be `token.text[3..]` where token.text starts with `///`. +pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8) !ParsedDocComment { + var result: ParsedDocComment = .{ + .arena = std.heap.ArenaAllocator.init(backing_allocator), + .comment = undefined, + }; + errdefer result.arena.deinit(); + result.comment = try parse_into_arena(&result.arena, raw_lines); + return result; +} + +/// Parses raw doc comment lines into an existing arena. +/// The caller owns the arena and is responsible for its lifetime. +/// /// Each raw line should be `token.text[3..]` where token.text starts with `///`. -pub fn parse(allocator: std.mem.Allocator, raw_lines: []const []const u8) !DocComment { +pub fn parse_into_arena(arena: *std.heap.ArenaAllocator, raw_lines: []const []const u8) !DocComment { if (raw_lines.len == 0) return .empty; - var ctx: ParseContext = .{ .allocator = allocator }; + var ctx: ParseContext = .{ .allocator = arena.allocator() }; return ctx.parse_doc(raw_lines); } @@ -18,34 +45,32 @@ const ParseContext = struct { allocator: std.mem.Allocator, fn parse_doc(ctx: *ParseContext, raw_lines: []const []const u8) !DocComment { - const a = ctx.allocator; - // Normalize lines: strip one optional leading space (the /// separator), right-trim. var norm_lines: std.ArrayList([]const u8) = .empty; - defer norm_lines.deinit(a); + defer norm_lines.deinit(ctx.allocator); for (raw_lines) |raw| { const stripped = if (raw.len > 0 and raw[0] == ' ') raw[1..] else raw; - try norm_lines.append(a, std.mem.trimRight(u8, stripped, " \t")); + try norm_lines.append(ctx.allocator, std.mem.trimRight(u8, stripped, " \t")); } const lines = norm_lines.items; var sections: std.ArrayList(DocComment.Section) = .empty; - defer sections.deinit(a); + defer sections.deinit(ctx.allocator); var current_kind: DocComment.Section.Kind = .main; var blocks: std.ArrayList(DocComment.Block) = .empty; - defer blocks.deinit(a); + defer blocks.deinit(ctx.allocator); // Current paragraph lines accumulator var para_lines: std.ArrayList([]const u8) = .empty; - defer para_lines.deinit(a); + defer para_lines.deinit(ctx.allocator); // Current list items (each item is a list of lines) var list_items: std.ArrayList(std.ArrayList([]const u8)) = .empty; defer { - for (list_items.items) |*item| item.deinit(a); - list_items.deinit(a); + for (list_items.items) |*item| item.deinit(ctx.allocator); + list_items.deinit(ctx.allocator); } var acc_kind: AccKind = .none; @@ -54,48 +79,48 @@ const ParseContext = struct { var in_fence = false; var fence_syntax: ?[]const u8 = null; var fence_lines: std.ArrayList([]const u8) = .empty; - defer fence_lines.deinit(a); + defer fence_lines.deinit(ctx.allocator); for (lines) |line| { if (in_fence) { if (std.mem.eql(u8, line, "```")) { - const content = try std.mem.join(a, "\n", fence_lines.items); - try blocks.append(a, .{ .code_block = .{ .syntax = fence_syntax, .content = content } }); + const content = try std.mem.join(ctx.allocator, "\n", fence_lines.items); + try blocks.append(ctx.allocator, .{ .code_block = .{ .syntax = fence_syntax, .content = content } }); in_fence = false; fence_syntax = null; fence_lines.clearRetainingCapacity(); } else { - try fence_lines.append(a, line); + try fence_lines.append(ctx.allocator, line); } continue; } // Code fence start if (std.mem.startsWith(u8, line, "```")) { - try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); const syn = std.mem.trim(u8, line[3..], " "); - fence_syntax = if (syn.len > 0) try a.dupe(u8, syn) else null; + fence_syntax = if (syn.len > 0) try ctx.allocator.dupe(u8, syn) else null; in_fence = true; continue; } // Blank line: flush current block if (line.len == 0) { - try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); continue; } // Admonition tag: NOTE:, WARNING:, LORE:, EXAMPLE:, DEPRECATED:, DECISION: if (parse_admonition(line)) |adm| { - try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); // Emit current section if it has content if (blocks.items.len > 0) { - try sections.append(a, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(a) }); + try sections.append(ctx.allocator, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(ctx.allocator) }); } current_kind = adm.kind; acc_kind = .paragraph; if (adm.text.len > 0) { - try para_lines.append(a, adm.text); + try para_lines.append(ctx.allocator, adm.text); } continue; } @@ -103,24 +128,24 @@ const ParseContext = struct { // Unordered list item: starts with "- " if (std.mem.startsWith(u8, line, "- ")) { if (acc_kind != .unordered_list) { - try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); acc_kind = .unordered_list; } var new_item: std.ArrayList([]const u8) = .empty; - try new_item.append(a, line[2..]); - try list_items.append(a, new_item); + try new_item.append(ctx.allocator, line[2..]); + try list_items.append(ctx.allocator, new_item); continue; } // Ordered list item: starts with "N. " if (parse_ordered_item(line)) |text| { if (acc_kind != .ordered_list) { - try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); acc_kind = .ordered_list; } var new_item: std.ArrayList([]const u8) = .empty; - try new_item.append(a, text); - try list_items.append(a, new_item); + try new_item.append(ctx.allocator, text); + try list_items.append(ctx.allocator, new_item); continue; } @@ -130,35 +155,34 @@ const ParseContext = struct { std.mem.startsWith(u8, line, " ")) { const cont = std.mem.trimLeft(u8, line, " "); - try list_items.items[list_items.items.len - 1].append(a, cont); + try list_items.items[list_items.items.len - 1].append(ctx.allocator, cont); continue; } // Paragraph (default): accumulate lines, trimming leading whitespace // so that admonition continuation indentation is normalized. if (acc_kind != .paragraph) { - try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); acc_kind = .paragraph; } - try para_lines.append(a, std.mem.trimLeft(u8, line, " \t")); + try para_lines.append(ctx.allocator, std.mem.trimLeft(u8, line, " \t")); } // Flush whatever remains - try ctx.flush_acc(a, &blocks, &acc_kind, ¶_lines, &list_items); + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); // Emit the final section if (blocks.items.len > 0) { - try sections.append(a, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(a) }); + try sections.append(ctx.allocator, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(ctx.allocator) }); } if (sections.items.len == 0) return .empty; - return .{ .sections = try sections.toOwnedSlice(a) }; + return .{ .sections = try sections.toOwnedSlice(ctx.allocator) }; } fn flush_acc( ctx: *ParseContext, - a: std.mem.Allocator, blocks: *std.ArrayList(DocComment.Block), acc_kind: *AccKind, para_lines: *std.ArrayList([]const u8), @@ -168,24 +192,24 @@ const ParseContext = struct { .none => {}, .paragraph => { if (para_lines.items.len > 0) { - const text = try std.mem.join(a, " ", para_lines.items); + const text = try std.mem.join(ctx.allocator, " ", para_lines.items); const content = try ctx.parse_inline(text); - try blocks.append(a, .{ .paragraph = .{ .content = content } }); + try blocks.append(ctx.allocator, .{ .paragraph = .{ .content = content } }); para_lines.clearRetainingCapacity(); } }, .unordered_list, .ordered_list => { - const items = try a.alloc([]const DocComment.Inline, list_items.items.len); + const items = try ctx.allocator.alloc([]const DocComment.Inline, list_items.items.len); for (list_items.items, 0..) |item_lines, j| { - const text = try std.mem.join(a, " ", item_lines.items); + const text = try std.mem.join(ctx.allocator, " ", item_lines.items); items[j] = try ctx.parse_inline(text); } - for (list_items.items) |*item| item.deinit(a); + for (list_items.items) |*item| item.deinit(ctx.allocator); list_items.clearRetainingCapacity(); if (acc_kind.* == .unordered_list) { - try blocks.append(a, .{ .unordered_list = .{ .items = items } }); + try blocks.append(ctx.allocator, .{ .unordered_list = .{ .items = items } }); } else { - try blocks.append(a, .{ .ordered_list = .{ .items = items } }); + try blocks.append(ctx.allocator, .{ .ordered_list = .{ .items = items } }); } }, } @@ -193,9 +217,8 @@ const ParseContext = struct { } fn parse_inline(ctx: *ParseContext, text: []const u8) ![]const DocComment.Inline { - const a = ctx.allocator; var result: std.ArrayList(DocComment.Inline) = .empty; - defer result.deinit(a); + defer result.deinit(ctx.allocator); var text_start: usize = 0; var i: usize = 0; @@ -212,9 +235,9 @@ const ParseContext = struct { }; if (escapable) { if (i > text_start) { - try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } - try result.append(a, .{ .text = .{ .value = text[i + 1 .. i + 2] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[i + 1 .. i + 2] } }); i += 2; text_start = i; continue; @@ -224,12 +247,12 @@ const ParseContext = struct { // Cross-reference: @`fqn` if (c == '@' and i + 1 < text.len and text[i + 1] == '`') { if (i > text_start) { - try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } const ref_start = i + 2; if (std.mem.indexOfScalar(u8, text[ref_start..], '`')) |rel_end| { const fqn = text[ref_start .. ref_start + rel_end]; - try result.append(a, .{ .ref = .{ .fqn = fqn } }); + try result.append(ctx.allocator, .{ .ref = .{ .fqn = fqn } }); i = ref_start + rel_end + 1; text_start = i; } else { @@ -242,12 +265,12 @@ const ParseContext = struct { // Inline code: `text` if (c == '`') { if (i > text_start) { - try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } const code_start = i + 1; if (std.mem.indexOfScalar(u8, text[code_start..], '`')) |rel_end| { const code_val = text[code_start .. code_start + rel_end]; - try result.append(a, .{ .code = .{ .value = code_val } }); + try result.append(ctx.allocator, .{ .code = .{ .value = code_val } }); i = code_start + rel_end + 1; text_start = i; } else { @@ -278,11 +301,11 @@ const ParseContext = struct { } if (found_close) |close_pos| { if (i > text_start) { - try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } const inner = text[em_start..close_pos]; const inner_content = try ctx.parse_inline(inner); - try result.append(a, .{ .emphasis = .{ .content = inner_content } }); + try result.append(ctx.allocator, .{ .emphasis = .{ .content = inner_content } }); i = close_pos + 1; text_start = i; continue; @@ -296,12 +319,12 @@ const ParseContext = struct { if (close_bracket + 1 < text.len and text[close_bracket + 1] == '(') { if (std.mem.indexOfScalarPos(u8, text, close_bracket + 2, ')')) |close_paren| { if (i > text_start) { - try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } const display = text[i + 1 .. close_bracket]; const url = text[close_bracket + 2 .. close_paren]; const content = try ctx.parse_inline(display); - try result.append(a, .{ .link = .{ .url = url, .content = content } }); + try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); i = close_paren + 1; text_start = i; continue; @@ -325,12 +348,12 @@ const ParseContext = struct { if (matched_scheme) { if (std.mem.indexOfScalarPos(u8, text, i + 1, '>')) |close_angle| { if (i > text_start) { - try result.append(a, .{ .text = .{ .value = text[text_start..i] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } const url = text[i + 1 .. close_angle]; - const content = try a.alloc(DocComment.Inline, 1); + const content = try ctx.allocator.alloc(DocComment.Inline, 1); content[0] = .{ .text = .{ .value = url } }; - try result.append(a, .{ .link = .{ .url = url, .content = content } }); + try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); i = close_angle + 1; text_start = i; continue; @@ -343,10 +366,10 @@ const ParseContext = struct { // Flush remaining literal text if (text_start < text.len) { - try result.append(a, .{ .text = .{ .value = text[text_start..] } }); + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..] } }); } - return result.toOwnedSlice(a); + return result.toOwnedSlice(ctx.allocator); } }; diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index 8f062836..1588dd12 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -950,7 +950,8 @@ const Analyzer = struct { /// Parses a raw doc comment into a structured DocComment. fn map_doc_comment(ana: *Analyzer, raw_lines: []const []const u8) !model.DocComment { - return doc_comment_parser.parse(ana.allocator, raw_lines); + var arena = std.heap.ArenaAllocator.init(ana.allocator); + return doc_comment_parser.parse_into_arena(&arena, raw_lines); } /// Creates a synthetic one-paragraph DocComment from a plain text string. diff --git a/src/tools/abi-mapper/tests/doc_parser.zig b/src/tools/abi-mapper/tests/doc_parser.zig index f68b020c..cd11cafa 100644 --- a/src/tools/abi-mapper/tests/doc_parser.zig +++ b/src/tools/abi-mapper/tests/doc_parser.zig @@ -3,43 +3,33 @@ const abi_parser = @import("abi-parser"); const doc_comment_parser = abi_parser.doc_comment; const DocComment = abi_parser.model.DocComment; -// Helper: parse raw lines (as produced by the tokenizer after stripping `///`) -// Lines with `/// text` produce `" text"` (one leading space). -// Lines with `///` (empty doc line) produce `""`. -fn parse(arena: std.mem.Allocator, lines: []const []const u8) !DocComment { - return doc_comment_parser.parse(arena, lines); -} - // ── Empty / blank ──────────────────────────────────────────────────────────── test "empty input returns empty DocComment" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{}); + defer parsed.deinit(); - const result = try parse(arena.allocator(), &.{}); - try std.testing.expectEqual(@as(usize, 0), result.sections.len); + try std.testing.expectEqual(@as(usize, 0), parsed.comment.sections.len); } test "only blank lines returns empty DocComment" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ "", "", "" }); + defer parsed.deinit(); - const result = try parse(arena.allocator(), &.{ "", "", "" }); - try std.testing.expectEqual(@as(usize, 0), result.sections.len); + try std.testing.expectEqual(@as(usize, 0), parsed.comment.sections.len); } // ── Paragraphs ─────────────────────────────────────────────────────────────── test "simple paragraph" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Hello, world!"}); + defer parsed.deinit(); - const result = try parse(arena.allocator(), &.{" Hello, world!"}); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(DocComment.Section.Kind.main, result.sections[0].kind); - try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.main, parsed.comment.sections[0].kind); + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); - const block = result.sections[0].blocks[0]; + const block = parsed.comment.sections[0].blocks[0]; try std.testing.expect(block == .paragraph); try std.testing.expectEqual(@as(usize, 1), block.paragraph.content.len); try std.testing.expect(block.paragraph.content[0] == .text); @@ -47,18 +37,16 @@ test "simple paragraph" { } test "multi-line paragraph joined with space" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " First line", " second line", " third line", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + defer parsed.deinit(); - const block = result.sections[0].blocks[0]; + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); + + const block = parsed.comment.sections[0].blocks[0]; try std.testing.expect(block == .paragraph); try std.testing.expectEqual(@as(usize, 1), block.paragraph.content.len); try std.testing.expectEqualStrings( @@ -68,38 +56,34 @@ test "multi-line paragraph joined with space" { } test "blank line separates paragraphs" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " First paragraph.", "", " Second paragraph.", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(@as(usize, 2), result.sections[0].blocks.len); - try std.testing.expect(result.sections[0].blocks[0] == .paragraph); - try std.testing.expect(result.sections[0].blocks[1] == .paragraph); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 2), parsed.comment.sections[0].blocks.len); + try std.testing.expect(parsed.comment.sections[0].blocks[0] == .paragraph); + try std.testing.expect(parsed.comment.sections[0].blocks[1] == .paragraph); try std.testing.expectEqualStrings( "First paragraph.", - result.sections[0].blocks[0].paragraph.content[0].text.value, + parsed.comment.sections[0].blocks[0].paragraph.content[0].text.value, ); try std.testing.expectEqualStrings( "Second paragraph.", - result.sections[0].blocks[1].paragraph.content[0].text.value, + parsed.comment.sections[0].blocks[1].paragraph.content[0].text.value, ); } // ── Inline elements ────────────────────────────────────────────────────────── test "inline code span" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{" Call `foo()` now."}); - const content = result.sections[0].blocks[0].paragraph.content; + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Call `foo()` now."}); + defer parsed.deinit(); + const content = parsed.comment.sections[0].blocks[0].paragraph.content; try std.testing.expectEqual(@as(usize, 3), content.len); try std.testing.expect(content[0] == .text); try std.testing.expectEqualStrings("Call ", content[0].text.value); @@ -110,12 +94,10 @@ test "inline code span" { } test "cross-reference @`fqn`" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{" See @`foo.bar.Baz` for details."}); - const content = result.sections[0].blocks[0].paragraph.content; + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" See @`foo.bar.Baz` for details."}); + defer parsed.deinit(); + const content = parsed.comment.sections[0].blocks[0].paragraph.content; try std.testing.expectEqual(@as(usize, 3), content.len); try std.testing.expect(content[0] == .text); try std.testing.expectEqualStrings("See ", content[0].text.value); @@ -126,12 +108,10 @@ test "cross-reference @`fqn`" { } test "emphasis *text*" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{" This is *important* text."}); - const content = result.sections[0].blocks[0].paragraph.content; + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" This is *important* text."}); + defer parsed.deinit(); + const content = parsed.comment.sections[0].blocks[0].paragraph.content; try std.testing.expectEqual(@as(usize, 3), content.len); try std.testing.expect(content[0] == .text); try std.testing.expectEqualStrings("This is ", content[0].text.value); @@ -143,13 +123,10 @@ test "emphasis *text*" { } test "escape sequences suppress special syntax" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - // \` prevents inline code, \* prevents emphasis - const result = try parse(arena.allocator(), &.{" Escape: \\`not code\\`."}); - const content = result.sections[0].blocks[0].paragraph.content; + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Escape: \\`not code\\`."}); + defer parsed.deinit(); + const content = parsed.comment.sections[0].blocks[0].paragraph.content; // None of the nodes should be a .code span for (content) |item| { try std.testing.expect(item != .code); @@ -163,12 +140,10 @@ test "escape sequences suppress special syntax" { } test "titled link [display](url)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{" See [the docs](https://example.com/docs)."}); - const content = result.sections[0].blocks[0].paragraph.content; + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" See [the docs](https://example.com/docs)."}); + defer parsed.deinit(); + const content = parsed.comment.sections[0].blocks[0].paragraph.content; try std.testing.expectEqual(@as(usize, 3), content.len); try std.testing.expect(content[0] == .text); try std.testing.expectEqualStrings("See ", content[0].text.value); @@ -181,12 +156,10 @@ test "titled link [display](url)" { } test "autolink " { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{" Visit ."}); - const content = result.sections[0].blocks[0].paragraph.content; + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Visit ."}); + defer parsed.deinit(); + const content = parsed.comment.sections[0].blocks[0].paragraph.content; try std.testing.expectEqual(@as(usize, 3), content.len); try std.testing.expect(content[0] == .text); try std.testing.expectEqualStrings("Visit ", content[0].text.value); @@ -199,12 +172,10 @@ test "autolink " { } test "autolink " { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{" Mail us."}); - const content = result.sections[0].blocks[0].paragraph.content; + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Mail us."}); + defer parsed.deinit(); + const content = parsed.comment.sections[0].blocks[0].paragraph.content; try std.testing.expectEqual(@as(usize, 3), content.len); try std.testing.expect(content[1] == .link); try std.testing.expectEqualStrings("mailto:foo@example.com", content[1].link.url); @@ -213,19 +184,18 @@ test "autolink " { // ── Admonitions ────────────────────────────────────────────────────────────── test "NOTE admonition starts new section" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " Main text.", "", " NOTE: This is a note.", }); - try std.testing.expectEqual(@as(usize, 2), result.sections.len); - try std.testing.expectEqual(DocComment.Section.Kind.main, result.sections[0].kind); - try std.testing.expectEqual(DocComment.Section.Kind.note, result.sections[1].kind); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 2), parsed.comment.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.main, parsed.comment.sections[0].kind); + try std.testing.expectEqual(DocComment.Section.Kind.note, parsed.comment.sections[1].kind); - const note_content = result.sections[1].blocks[0].paragraph.content; + const note_content = parsed.comment.sections[1].blocks[0].paragraph.content; try std.testing.expectEqualStrings("This is a note.", note_content[0].text.value); } @@ -241,67 +211,62 @@ test "all admonition kinds are recognized" { }; for (cases) |c| { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - var buf: [64]u8 = undefined; const line = try std.fmt.bufPrint(&buf, " {s}: test text", .{c.tag}); - const result = try parse(arena.allocator(), &.{line}); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(c.kind, result.sections[0].kind); + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{line}); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections.len); + try std.testing.expectEqual(c.kind, parsed.comment.sections[0].kind); } } test "admonition with empty body starts section" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " NOTE:", " Text on the next line.", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(DocComment.Section.Kind.note, result.sections[0].kind); - try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.note, parsed.comment.sections[0].kind); + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); try std.testing.expectEqualStrings( "Text on the next line.", - result.sections[0].blocks[0].paragraph.content[0].text.value, + parsed.comment.sections[0].blocks[0].paragraph.content[0].text.value, ); } test "multiple admonition sections" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " Main description.", "", " NOTE: Important note.", "", " WARNING: Be careful.", }); - try std.testing.expectEqual(@as(usize, 3), result.sections.len); - try std.testing.expectEqual(DocComment.Section.Kind.main, result.sections[0].kind); - try std.testing.expectEqual(DocComment.Section.Kind.note, result.sections[1].kind); - try std.testing.expectEqual(DocComment.Section.Kind.warning, result.sections[2].kind); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 3), parsed.comment.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.main, parsed.comment.sections[0].kind); + try std.testing.expectEqual(DocComment.Section.Kind.note, parsed.comment.sections[1].kind); + try std.testing.expectEqual(DocComment.Section.Kind.warning, parsed.comment.sections[2].kind); } // ── Lists ──────────────────────────────────────────────────────────────────── test "unordered list" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " - First item", " - Second item", " - Third item", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + defer parsed.deinit(); - const block = result.sections[0].blocks[0]; + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); + + const block = parsed.comment.sections[0].blocks[0]; try std.testing.expect(block == .unordered_list); try std.testing.expectEqual(@as(usize, 3), block.unordered_list.items.len); try std.testing.expectEqualStrings("First item", block.unordered_list.items[0][0].text.value); @@ -310,18 +275,16 @@ test "unordered list" { } test "ordered list" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " 1. First", " 2. Second", " 3. Third", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + defer parsed.deinit(); - const block = result.sections[0].blocks[0]; + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); + + const block = parsed.comment.sections[0].blocks[0]; try std.testing.expect(block == .ordered_list); try std.testing.expectEqual(@as(usize, 3), block.ordered_list.items.len); try std.testing.expectEqualStrings("First", block.ordered_list.items[0][0].text.value); @@ -330,15 +293,14 @@ test "ordered list" { } test "list item continuation line" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " - First item", " continues here", " - Second item", }); - const block = result.sections[0].blocks[0]; + defer parsed.deinit(); + + const block = parsed.comment.sections[0].blocks[0]; try std.testing.expect(block == .unordered_list); try std.testing.expectEqual(@as(usize, 2), block.unordered_list.items.len); // The two continuation lines are joined with a space @@ -350,54 +312,47 @@ test "list item continuation line" { } test "paragraph after list" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " - Item one", " - Item two", "", " Trailing paragraph.", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(@as(usize, 2), result.sections[0].blocks.len); - try std.testing.expect(result.sections[0].blocks[0] == .unordered_list); - try std.testing.expect(result.sections[0].blocks[1] == .paragraph); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 2), parsed.comment.sections[0].blocks.len); + try std.testing.expect(parsed.comment.sections[0].blocks[0] == .unordered_list); + try std.testing.expect(parsed.comment.sections[0].blocks[1] == .paragraph); } // ── Code fences ────────────────────────────────────────────────────────────── test "code fence without syntax hint" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " ```", " some code", " more code", " ```", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(@as(usize, 1), result.sections[0].blocks.len); + defer parsed.deinit(); - const block = result.sections[0].blocks[0]; + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); + + const block = parsed.comment.sections[0].blocks[0]; try std.testing.expect(block == .code_block); try std.testing.expectEqual(@as(?[]const u8, null), block.code_block.syntax); try std.testing.expectEqualStrings("some code\nmore code", block.code_block.content); } test "code fence with syntax hint" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " ```zig", " const x = 42;", " ```", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); + defer parsed.deinit(); - const block = result.sections[0].blocks[0]; + const block = parsed.comment.sections[0].blocks[0]; try std.testing.expect(block == .code_block); try std.testing.expect(block.code_block.syntax != null); try std.testing.expectEqualStrings("zig", block.code_block.syntax.?); @@ -405,10 +360,7 @@ test "code fence with syntax hint" { } test "code fence preceded and followed by text" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = try parse(arena.allocator(), &.{ + var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ " Before.", "", " ```", @@ -417,9 +369,10 @@ test "code fence preceded and followed by text" { "", " After.", }); - try std.testing.expectEqual(@as(usize, 1), result.sections.len); - try std.testing.expectEqual(@as(usize, 3), result.sections[0].blocks.len); - try std.testing.expect(result.sections[0].blocks[0] == .paragraph); - try std.testing.expect(result.sections[0].blocks[1] == .code_block); - try std.testing.expect(result.sections[0].blocks[2] == .paragraph); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 3), parsed.comment.sections[0].blocks.len); + try std.testing.expect(parsed.comment.sections[0].blocks[0] == .paragraph); + try std.testing.expect(parsed.comment.sections[0].blocks[1] == .code_block); + try std.testing.expect(parsed.comment.sections[0].blocks[2] == .paragraph); } From 798b635ae94dcef8012229d6b4393cc3eca7f58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 2 Mar 2026 21:06:02 +0100 Subject: [PATCH 03/36] Claude Code: Resolves most open TODOs --- src/tools/abi-mapper/build.zig | 3 +- src/tools/abi-mapper/src/abi-parser.zig | 20 ++- src/tools/abi-mapper/src/model.zig | 18 +- src/tools/abi-mapper/src/sema.zig | 193 ++++++++++++++++------ src/tools/abi-mapper/src/uid_db.zig | 103 ++++++++++++ src/tools/abi-mapper/tests/doc_parser.zig | 7 + 6 files changed, 290 insertions(+), 54 deletions(-) create mode 100644 src/tools/abi-mapper/src/uid_db.zig diff --git a/src/tools/abi-mapper/build.zig b/src/tools/abi-mapper/build.zig index c0163683..82e62df8 100644 --- a/src/tools/abi-mapper/build.zig +++ b/src/tools/abi-mapper/build.zig @@ -54,8 +54,7 @@ pub const Converter = struct { pub fn get_json_dump(cc: Converter, id_database: std.Build.LazyPath, input: std.Build.LazyPath) std.Build.LazyPath { const generate_json = cc.b.addRunArtifact(cc.executable); - // TODO: generate_json.addPrefixedFileArg("--id-db=", id_database); - _ = id_database; + generate_json.addPrefixedFileArg("--id-db=", id_database); const abi_json = generate_json.addPrefixedOutputFileArg("--output=", "abi.json"); generate_json.addFileArg(input); return abi_json; diff --git a/src/tools/abi-mapper/src/abi-parser.zig b/src/tools/abi-mapper/src/abi-parser.zig index a41a4cbf..e873169a 100644 --- a/src/tools/abi-mapper/src/abi-parser.zig +++ b/src/tools/abi-mapper/src/abi-parser.zig @@ -8,6 +8,7 @@ pub const doc_comment = @import("doc_comment.zig"); const CliOptions = struct { output: []const u8 = "", + @"id-db": []const u8 = "", }; pub fn main() !u8 { @@ -51,7 +52,24 @@ pub fn main() !u8 { return err; }; - const analyzed_document: model.Document = try sema.analyze(allocator, ast_document); + // Load UID database if --id-db was specified + const id_db_path = args.options.@"id-db"; + var uid_database: ?sema.uid_db.UidDatabase = if (id_db_path.len > 0) + try sema.uid_db.UidDatabase.load(allocator, id_db_path) + else + null; + defer if (uid_database) |*db| db.deinit(); + + const analyzed_document: model.Document = try sema.analyze( + allocator, + ast_document, + if (uid_database != null) &uid_database.? else null, + ); + + // Save UID database back if it was loaded + if (uid_database != null and id_db_path.len > 0) { + try uid_database.?.save(id_db_path); + } var atomic_buffer: [4096]u8 = undefined; var atomic_output = try std.fs.cwd().atomicFile( diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index 17c7b08b..4500f337 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -239,7 +239,7 @@ fn inline_from_value(allocator: std.mem.Allocator, value: std.json.Value) !DocCo return error.UnexpectedToken; } -fn inline_array_from_value(allocator: std.mem.Allocator, value: std.json.Value) ![]const DocComment.Inline { +fn inline_array_from_value(allocator: std.mem.Allocator, value: std.json.Value) error{ OutOfMemory, UnexpectedToken }![]const DocComment.Inline { const arr = switch (value) { .array => |a| a, else => return error.UnexpectedToken, @@ -384,7 +384,7 @@ pub const Type = union(enum) { /// to be reified into `enum MyName : u32 { item … } unset_magic_type: MagicType, - pub fn is_c_abi_compatible(t: Type) bool { + pub fn is_c_abi_compatible(t: Type, types: []const Type) bool { return switch (t) { .@"struct" => true, .@"union" => true, @@ -417,10 +417,20 @@ pub const Type = union(enum) { .one, .unknown => true, .slice => false, }, - .optional => false, // TODO: ?*T and ?[*]T are C-abi-compatible + .optional => |inner_idx| blk: { + // ?*T and ?[*]T are C-ABI compatible (represented as nullable pointers) + const inner = types[@intFromEnum(inner_idx)]; + break :blk switch (inner) { + .ptr => |ptr| switch (ptr.size) { + .one, .unknown => true, + .slice => false, + }, + else => false, + }; + }, .fnptr => true, - // TODO: These types should not exist anymore when C-ABI check is performed + // These types should not exist anymore when C-ABI check is performed .alias => true, .unknown_named_type => false, .unset_magic_type => false, diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index 1588dd12..f19c9694 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -2,10 +2,11 @@ const std = @import("std"); const model = @import("model.zig"); const syntax = @import("syntax.zig"); const doc_comment_parser = @import("doc_comment.zig"); +pub const uid_db = @import("uid_db.zig"); const Location = syntax.Location; -pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document) !model.Document { +pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document, uid_database: ?*uid_db.UidDatabase) !model.Document { var analyzer: Analyzer = .{ .allocator = allocator, .scope_stack = .empty, @@ -22,6 +23,8 @@ pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document) !model.D .resources = .init(allocator), .constants = .init(allocator), .types = .init(allocator), + + .uid_db = uid_database, }; try analyzer.scope_map.put(&.{}, &analyzer.root_scope); @@ -51,7 +54,7 @@ pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document) !model.D // TODO: Implement garbage collection for unreferenced things - // analyzer.validate_constraints(); + try analyzer.validate_constraints(); return .{ .root = try analyzer.root.toOwnedSlice(analyzer.allocator), @@ -118,6 +121,7 @@ const Analyzer = struct { types: Collector(model.TypeIndex), uid_base: u32 = 1, + uid_db: ?*uid_db.UidDatabase = null, const Scope = struct { parent: ?*Scope, @@ -138,8 +142,19 @@ const Analyzer = struct { }; /// Returns a unique ID based on the `fqn` of the object. + /// When a UID database is present, IDs are stable across re-runs for a + /// given FQN. Without a database, IDs are sequentially assigned. fn get_uid(ana: *Analyzer, fqn: model.FQN) error{OutOfMemory}!model.UniqueID { - _ = fqn; // TODO: Implement derivation from FQN and a UID database. + if (ana.uid_db) |db| { + var key: std.ArrayList(u8) = .empty; + defer key.deinit(ana.allocator); + for (fqn, 0..) |part, i| { + if (i > 0) try key.append(ana.allocator, '.'); + try key.appendSlice(ana.allocator, part); + } + const uid_val = try db.get_or_assign(key.items); + return @enumFromInt(uid_val); + } const uid: model.UniqueID = @enumFromInt(ana.uid_base); ana.uid_base += 1; return uid; @@ -257,9 +272,18 @@ const Analyzer = struct { .bitstruct => |index| .{ .bitstruct = index }, .resource => |index| .{ .resource = index }, .typedef => |index| .{ .alias = index }, - .syscall => @panic("TODO: Invalid type reference!"), - .async_call => @panic("TODO: Invalid type reference!"), - .constant => @panic("TODO: Invalid type reference!"), + .syscall => blk: { + try ana.emit_error(Location.empty, "type reference '{f}' resolves to a syscall, which cannot be used as a type", .{dotJoin(unknown_type.local_qualified_name)}); + break :blk .{ .well_known = .void }; + }, + .async_call => blk: { + try ana.emit_error(Location.empty, "type reference '{f}' resolves to an async_call, which cannot be used as a type", .{dotJoin(unknown_type.local_qualified_name)}); + break :blk .{ .well_known = .void }; + }, + .constant => blk: { + try ana.emit_error(Location.empty, "type reference '{f}' resolves to a constant, which cannot be used as a type", .{dotJoin(unknown_type.local_qualified_name)}); + break :blk .{ .well_known = .void }; + }, }; // std.log.debug(" ! candidate found {s} ({s})!", .{ sub_scope.name, @tagName(sub_scope.type) }); @@ -267,7 +291,7 @@ const Analyzer = struct { continue :element_resolution; } } - std.log.err("no candidate found for type {f} at {f}!", .{ + try ana.emit_error(Location.empty, "unknown type '{f}' referenced from scope '{f}'", .{ dotJoin(unknown_type.local_qualified_name), dotJoin(unknown_type.declared_scope), }); @@ -306,7 +330,7 @@ const Analyzer = struct { const collector = &@field(ana, collector_name); - for (collector.items, 1..) |item, index| { + for (collector.items) |item| { var item_name: std.ArrayList(u8) = .empty; defer item_name.deinit(ana.allocator); @@ -325,12 +349,10 @@ const Analyzer = struct { } } - // TODO: Implement stable item id assignment! - try items.append(ana.allocator, .{ .docs = .empty, .name = try item_name.toOwnedSlice(ana.allocator), - .value = @intCast(index), + .value = @intCast(@intFromEnum(item.uid)), }); } }, @@ -398,16 +420,19 @@ const Analyzer = struct { const expected_size = bitstruct.backing_type.size_in_bits().?; - // std.log.err("bitstruct {s}", .{bitstruct.full_qualified_name}); - var struct_size: u8 = 0; + var has_error = false; for (@constCast(bitstruct.fields)) |*field| { const field_type = ana.get_resolved_type(field.type); - const maybe_type_size = get_type_bit_size(field_type); - // std.log.err(" {?s} => {} ({?} bits)", .{ field.name, field_type, maybe_type_size }); + const maybe_type_size = ana.get_type_bit_size(field_type); const type_size = maybe_type_size orelse { - @panic("TODO: error report for 'type not bit-packable'"); + try ana.emit_error(Location.empty, "bitstruct '{s}': field '{s}' has a type that cannot be packed into bits", .{ + model.local_name(bitstruct.full_qualified_name), + field.name orelse "", + }); + has_error = true; + continue; }; field.bit_shift = struct_size; @@ -416,16 +441,26 @@ const Analyzer = struct { struct_size += type_size; } - if (struct_size > expected_size) { - @panic("TODO: error reporting for 'fields too big'"); - } else if (struct_size < expected_size) { - @panic("TODO: error reporting for 'fields too little'"); + if (!has_error) { + if (struct_size > expected_size) { + try ana.emit_error(Location.empty, "bitstruct '{s}': fields occupy {d} bits but backing type has {d} bits (too large)", .{ + model.local_name(bitstruct.full_qualified_name), + struct_size, + expected_size, + }); + } else if (struct_size < expected_size) { + try ana.emit_error(Location.empty, "bitstruct '{s}': fields occupy {d} bits but backing type has {d} bits (use 'reserve' to add padding)", .{ + model.local_name(bitstruct.full_qualified_name), + struct_size, + expected_size, + }); + } } } } /// `tvalue` must be fully resolved and must not be any type alias - fn get_type_bit_size(tvalue: model.Type) ?u8 { + fn get_type_bit_size(ana: *Analyzer, tvalue: model.Type) ?u8 { return switch (tvalue) { .alias => unreachable, .typedef => unreachable, @@ -437,8 +472,8 @@ const Analyzer = struct { .well_known => |stdtype| stdtype.size_in_bits(), - .@"enum" => @panic("TODO"), - .bitstruct => @panic("TODO"), + .@"enum" => |idx| ana.enums.get(idx).backing_type.size_in_bits(), + .bitstruct => |idx| ana.bitstructs.get(idx).bit_count, .fnptr => null, .ptr => null, @@ -505,7 +540,7 @@ const Analyzer = struct { fn render(a: *Analyzer, list: *std.ArrayList(model.Parameter), params: []model.Parameter, mode: RenderMode) !void { for (params) |*param| { const resolved = a.get_resolved_type(param.type); - if (resolved.is_c_abi_compatible()) { + if (resolved.is_c_abi_compatible(a.types.items)) { try list.append(a.allocator, param.*); continue; } @@ -587,9 +622,10 @@ const Analyzer = struct { }, else => { - - // TODO! - std.log.err("implement type resolution for {}", .{a.get_resolved_type(param.type)}); + try a.emit_error(Location.empty, "parameter '{s}' has type '{s}' which cannot appear in a native call signature", .{ + param.name, + @tagName(a.get_resolved_type(param.type)), + }); }, } } @@ -820,7 +856,7 @@ const Analyzer = struct { .fnptr => .keep, .uint, .int => .keep, .array => .keep, - .typedef => .keep, // TODO: Check if slice! + .typedef => unreachable, // get_resolved_type always resolves through typedefs .external => .keep, .alias => unreachable, @@ -917,7 +953,6 @@ const Analyzer = struct { const value = try ana.resolve_value(constant.value.?); - // TODO: Implement explicit constant typing! const type_id: ?model.TypeIndex = if (constant.type) |type_node| try ana.map_type(type_node) else @@ -1175,8 +1210,12 @@ const Analyzer = struct { }, .@"error" => |data| { + // Build FQN for this error: [syscall_fqn..., error_name] + const error_fqn = try std.mem.concat(ana.allocator, []const u8, &.{ info.full_name, &.{data} }); + defer ana.allocator.free(error_fqn); + const error_uid = try ana.get_uid(error_fqn); try errors.append(child.location, .{ - .value = @intCast(errors.fields.items.len + 1), // TODO: Implement fqn + error name based caching in database file + .value = @intFromEnum(error_uid), .docs = try ana.map_doc_comment(child.doc_comment), .name = data, }); @@ -1454,9 +1493,8 @@ const Analyzer = struct { while (iter.next()) |part| { if (part.len == 0) { - @panic("TODO: Empty parts!"); - // try ana.emit_error(); - // continue; + try ana.emit_error(Location.empty, "empty identifier segment in type name '{s}'", .{data}); + continue; } try fqn.append(ana.allocator, part); } @@ -1500,11 +1538,11 @@ const Analyzer = struct { const size: u32 = switch (size_val) { .int => |int| std.math.cast(u32, int) orelse blk: { - std.log.err("TODO: Array size too large: {}", .{int}); + try ana.emit_error(Location.empty, "array size {d} is too large (maximum is {d})", .{ int, std.math.maxInt(u32) }); break :blk 0; }, else => blk: { - std.log.err("TODO: Invalid array size {}", .{size_val}); + try ana.emit_error(Location.empty, "array size must be an integer, not a {s}", .{@tagName(size_val)}); break :blk 0; }, }; @@ -1636,7 +1674,21 @@ const Analyzer = struct { } } - fn validate_constraints(ana: *Analyzer) void { + fn find_param_by_name(params: []const model.Parameter, name: []const u8) ?model.Parameter { + for (params) |p| { + if (std.mem.eql(u8, p.name, name)) return p; + } + return null; + } + + fn find_field_by_name(fields: []const model.StructField, name: []const u8) ?model.StructField { + for (fields) |f| { + if (std.mem.eql(u8, f.name, name)) return f; + } + return null; + } + + fn validate_constraints(ana: *Analyzer) !void { for (ana.syscalls.items) |sc| { // native calls must have either a return value // or none. @@ -1662,18 +1714,42 @@ const Analyzer = struct { } for (sc.native_inputs) |inp| { - std.debug.assert(ana.get_resolved_type(inp.type).is_c_abi_compatible()); + std.debug.assert(ana.get_resolved_type(inp.type).is_c_abi_compatible(ana.types.items)); // inputs cannot be the error role std.debug.assert(inp.role != .@"error"); - // TODO: Assert that referenced parameters exist, and that they have the right pointer type + switch (inp.role) { + .default, .output => {}, + .input_slice, .output_slice => unreachable, // slices are split into ptr+len in native params + .@"error" => unreachable, // asserted above + .input_ptr => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (input_ptr) references unknown logic input '{s}'", .{ inp.name, ref_name }); + } + }, + .input_len => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (input_len) references unknown logic input '{s}'", .{ inp.name, ref_name }); + } + }, + .output_ptr => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (output_ptr) references unknown logic output '{s}'", .{ inp.name, ref_name }); + } + }, + .output_len => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (output_len) references unknown logic output '{s}'", .{ inp.name, ref_name }); + } + }, + } } var has_error_output = false; const needs_error_output = (sc.errors.len > 0); for (sc.native_outputs) |outp| { - std.debug.assert(ana.get_resolved_type(outp.type).is_c_abi_compatible()); + std.debug.assert(ana.get_resolved_type(outp.type).is_c_abi_compatible(ana.types.items)); switch (outp.role) { .default => {}, .@"error" => { @@ -1681,11 +1757,27 @@ const Analyzer = struct { std.debug.assert(needs_error_output); has_error_output = true; }, - .input_len, .input_ptr => { - // TODO: Assert that referenced parameters exist, and that they have the right pointer type + .input_slice, .output_slice => unreachable, + .output => {}, + .input_len => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (input_len) references unknown logic input '{s}'", .{ outp.name, ref_name }); + } + }, + .input_ptr => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (input_ptr) references unknown logic input '{s}'", .{ outp.name, ref_name }); + } + }, + .output_len => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (output_len) references unknown logic output '{s}'", .{ outp.name, ref_name }); + } }, - .output_len, .output_ptr => { - // TODO: Assert that referenced parameters exist, and that they have the right pointer type + .output_ptr => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (output_ptr) references unknown logic output '{s}'", .{ outp.name, ref_name }); + } }, } } @@ -1701,7 +1793,7 @@ const Analyzer = struct { } for (un.native_fields) |fld| { std.debug.assert(fld.role == .default); - std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible()); + std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible(ana.types.items)); } } @@ -1713,11 +1805,18 @@ const Analyzer = struct { for (str.native_fields) |fld| { switch (fld.role) { .default => {}, - .slice_len, .slice_ptr => { - // TODO: Assert the referenced slice exists + .slice_ptr => |ref_name| { + if (find_field_by_name(str.logic_fields, ref_name) == null) { + try ana.emit_error(Location.empty, "native field '{s}' (slice_ptr) references unknown logic field '{s}'", .{ fld.name, ref_name }); + } + }, + .slice_len => |ref_name| { + if (find_field_by_name(str.logic_fields, ref_name) == null) { + try ana.emit_error(Location.empty, "native field '{s}' (slice_len) references unknown logic field '{s}'", .{ fld.name, ref_name }); + } }, } - std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible()); + std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible(ana.types.items)); } } diff --git a/src/tools/abi-mapper/src/uid_db.zig b/src/tools/abi-mapper/src/uid_db.zig new file mode 100644 index 00000000..86b498e9 --- /dev/null +++ b/src/tools/abi-mapper/src/uid_db.zig @@ -0,0 +1,103 @@ +const std = @import("std"); + +/// A database that maps fully-qualified names (as dot-joined strings) to stable +/// u32 UIDs. On first run the file is created; on subsequent runs it reads the +/// file and reuses existing IDs, allocating new ones for new FQNs. +pub const UidDatabase = struct { + allocator: std.mem.Allocator, + entries: std.StringArrayHashMap(u32), + next_id: u32, + + /// JSON schema used for persistence. + const FileFormat = struct { + entries: []const Entry, + + const Entry = struct { + fqn: []const u8, + uid: u32, + }; + }; + + pub fn init(allocator: std.mem.Allocator) UidDatabase { + return .{ + .allocator = allocator, + .entries = .init(allocator), + .next_id = 1, + }; + } + + pub fn deinit(db: *UidDatabase) void { + // Free owned key copies + for (db.entries.keys()) |key| { + db.allocator.free(key); + } + db.entries.deinit(); + db.* = undefined; + } + + /// Look up `fqn`; if not present, assign the next available ID and persist + /// that mapping. Returns the (possibly newly-assigned) UID. + pub fn get_or_assign(db: *UidDatabase, fqn: []const u8) !u32 { + if (db.entries.get(fqn)) |uid| return uid; + + const key = try db.allocator.dupe(u8, fqn); + const uid = db.next_id; + db.next_id += 1; + try db.entries.put(key, uid); + return uid; + } + + /// Load a database from `path`. If the file does not exist an empty + /// database is returned instead. + pub fn load(allocator: std.mem.Allocator, path: []const u8) !UidDatabase { + var db = init(allocator); + errdefer db.deinit(); + + const content = std.fs.cwd().readFileAlloc(allocator, path, 1 << 20) catch |err| switch (err) { + error.FileNotFound => return db, + else => return err, + }; + defer allocator.free(content); + + const parsed = try std.json.parseFromSlice(FileFormat, allocator, content, .{ + .allocate = .alloc_always, + .ignore_unknown_fields = true, + }); + defer parsed.deinit(); + + for (parsed.value.entries) |entry| { + const key = try allocator.dupe(u8, entry.fqn); + try db.entries.put(key, entry.uid); + if (entry.uid >= db.next_id) { + db.next_id = entry.uid + 1; + } + } + + return db; + } + + /// Save the database to `path` atomically. + pub fn save(db: *const UidDatabase, path: []const u8) !void { + const entries = try db.allocator.alloc(FileFormat.Entry, db.entries.count()); + defer db.allocator.free(entries); + + for (db.entries.keys(), db.entries.values(), 0..) |key, value, i| { + entries[i] = .{ .fqn = key, .uid = value }; + } + + const format: FileFormat = .{ .entries = entries }; + + var atomic_buffer: [4096]u8 = undefined; + var atomic_file = try std.fs.cwd().atomicFile(path, .{ .write_buffer = &atomic_buffer }); + defer atomic_file.deinit(); + + const writer = &atomic_file.file_writer.interface; + const options: std.json.Stringify.Options = .{ + .whitespace = .indent_2, + }; + try writer.print("{f}", .{std.json.fmt(format, options)}); + try writer.flush(); + + try atomic_file.finish(); + } +}; diff --git a/src/tools/abi-mapper/tests/doc_parser.zig b/src/tools/abi-mapper/tests/doc_parser.zig index cd11cafa..5530ad33 100644 --- a/src/tools/abi-mapper/tests/doc_parser.zig +++ b/src/tools/abi-mapper/tests/doc_parser.zig @@ -3,6 +3,13 @@ const abi_parser = @import("abi-parser"); const doc_comment_parser = abi_parser.doc_comment; const DocComment = abi_parser.model.DocComment; +// from json + +test "empty doc comment from json" { + var comment = try std.json.parseFromSlice(DocComment, std.testing.allocator, "{ \"sections\": [] }", .{}); + defer comment.deinit(); +} + // ── Empty / blank ──────────────────────────────────────────────────────────── test "empty input returns empty DocComment" { From 75b452df2ff0693a844d40e088b135f0c7854b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 2 Mar 2026 22:45:59 +0100 Subject: [PATCH 04/36] Fixes some bad assumptions for C compatibility --- src/abi/db/abi-id-db.json | 2420 +++++++++++++++++++++++++++ src/abi/utility/render_zig_code.zig | 11 +- src/tools/abi-mapper/src/model.zig | 5 + src/tools/abi-mapper/src/sema.zig | 10 +- 4 files changed, 2440 insertions(+), 6 deletions(-) create mode 100644 src/abi/db/abi-id-db.json diff --git a/src/abi/db/abi-id-db.json b/src/abi/db/abi-id-db.json new file mode 100644 index 00000000..8f0a0219 --- /dev/null +++ b/src/abi/db/abi-id-db.json @@ -0,0 +1,2420 @@ +{ + "entries": [ + { + "fqn": "SystemResource", + "uid": 1 + }, + { + "fqn": "resources.get_type.InvalidHandle", + "uid": 2 + }, + { + "fqn": "resources.get_type", + "uid": 3 + }, + { + "fqn": "resources.get_owners", + "uid": 4 + }, + { + "fqn": "resources.send_to_process.DeadProcess", + "uid": 5 + }, + { + "fqn": "resources.send_to_process.InvalidHandle", + "uid": 6 + }, + { + "fqn": "resources.send_to_process.SystemResources", + "uid": 7 + }, + { + "fqn": "resources.send_to_process", + "uid": 8 + }, + { + "fqn": "resources.release", + "uid": 9 + }, + { + "fqn": "resources.destroy", + "uid": 10 + }, + { + "fqn": "overlapped.ARC", + "uid": 11 + }, + { + "fqn": "overlapped.schedule.AlreadyScheduled", + "uid": 12 + }, + { + "fqn": "overlapped.schedule.SystemResources", + "uid": 13 + }, + { + "fqn": "overlapped.schedule", + "uid": 14 + }, + { + "fqn": "overlapped.await_completion.Unscheduled", + "uid": 15 + }, + { + "fqn": "overlapped.await_completion", + "uid": 16 + }, + { + "fqn": "overlapped.await_completion_of.InvalidOperation", + "uid": 17 + }, + { + "fqn": "overlapped.await_completion_of.Unscheduled", + "uid": 18 + }, + { + "fqn": "overlapped.await_completion_of", + "uid": 19 + }, + { + "fqn": "overlapped.cancel.Completed", + "uid": 20 + }, + { + "fqn": "overlapped.cancel.Unscheduled", + "uid": 21 + }, + { + "fqn": "overlapped.cancel", + "uid": 22 + }, + { + "fqn": "process.ExitCode", + "uid": 23 + }, + { + "fqn": "process.get_file_name.InvalidHandle", + "uid": 24 + }, + { + "fqn": "process.get_file_name", + "uid": 25 + }, + { + "fqn": "process.get_base_address.InvalidHandle", + "uid": 26 + }, + { + "fqn": "process.get_base_address", + "uid": 27 + }, + { + "fqn": "process.get_arguments.InvalidHandle", + "uid": 28 + }, + { + "fqn": "process.get_arguments", + "uid": 29 + }, + { + "fqn": "process.terminate", + "uid": 30 + }, + { + "fqn": "process.kill.InvalidHandle", + "uid": 31 + }, + { + "fqn": "process.kill", + "uid": 32 + }, + { + "fqn": "process.Spawn.BadExecutable", + "uid": 33 + }, + { + "fqn": "process.Spawn.DiskError", + "uid": 34 + }, + { + "fqn": "process.Spawn.FileNotFound", + "uid": 35 + }, + { + "fqn": "process.Spawn.InvalidHandle", + "uid": 36 + }, + { + "fqn": "process.Spawn.InvalidPath", + "uid": 37 + }, + { + "fqn": "process.Spawn.SystemResources", + "uid": 38 + }, + { + "fqn": "process.Spawn", + "uid": 39 + }, + { + "fqn": "process.thread.yield", + "uid": 40 + }, + { + "fqn": "process.thread.exit", + "uid": 41 + }, + { + "fqn": "process.thread.join.InvalidHandle", + "uid": 42 + }, + { + "fqn": "process.thread.join", + "uid": 43 + }, + { + "fqn": "process.thread.spawn.SystemResources", + "uid": 44 + }, + { + "fqn": "process.thread.spawn", + "uid": 45 + }, + { + "fqn": "process.thread.kill.InvalidHandle", + "uid": 46 + }, + { + "fqn": "process.thread.kill", + "uid": 47 + }, + { + "fqn": "process.debug.write_log", + "uid": 48 + }, + { + "fqn": "process.debug.breakpoint", + "uid": 49 + }, + { + "fqn": "process.memory.allocate.SystemResource", + "uid": 50 + }, + { + "fqn": "process.memory.allocate", + "uid": 51 + }, + { + "fqn": "process.memory.release", + "uid": 52 + }, + { + "fqn": "process.monitor.enumerate_processes", + "uid": 53 + }, + { + "fqn": "process.monitor.query_owned_resources.InvalidHandle", + "uid": 54 + }, + { + "fqn": "process.monitor.query_owned_resources", + "uid": 55 + }, + { + "fqn": "process.monitor.query_total_memory_usage.InvalidHandle", + "uid": 56 + }, + { + "fqn": "process.monitor.query_total_memory_usage", + "uid": 57 + }, + { + "fqn": "process.monitor.query_dynamic_memory_usage.InvalidHandle", + "uid": 58 + }, + { + "fqn": "process.monitor.query_dynamic_memory_usage", + "uid": 59 + }, + { + "fqn": "process.monitor.query_active_allocation_count.InvalidHandle", + "uid": 60 + }, + { + "fqn": "process.monitor.query_active_allocation_count", + "uid": 61 + }, + { + "fqn": "clock.monotonic", + "uid": 62 + }, + { + "fqn": "clock.Timer", + "uid": 63 + }, + { + "fqn": "datetime.now", + "uid": 64 + }, + { + "fqn": "datetime.Alarm", + "uid": 65 + }, + { + "fqn": "video.enumerate", + "uid": 66 + }, + { + "fqn": "video.acquire.NotAvailable", + "uid": 67 + }, + { + "fqn": "video.acquire.NotFound", + "uid": 68 + }, + { + "fqn": "video.acquire.SystemResources", + "uid": 69 + }, + { + "fqn": "video.acquire", + "uid": 70 + }, + { + "fqn": "video.get_resolution.InvalidHandle", + "uid": 71 + }, + { + "fqn": "video.get_resolution", + "uid": 72 + }, + { + "fqn": "video.get_video_memory.InvalidHandle", + "uid": 73 + }, + { + "fqn": "video.get_video_memory", + "uid": 74 + }, + { + "fqn": "video.WaitForVBlank.InvalidHandle", + "uid": 75 + }, + { + "fqn": "video.WaitForVBlank", + "uid": 76 + }, + { + "fqn": "random.get_soft_random", + "uid": 77 + }, + { + "fqn": "random.GetStrictRandom", + "uid": 78 + }, + { + "fqn": "input.GetEvent.InProgress", + "uid": 79 + }, + { + "fqn": "input.GetEvent.NonExclusiveAccess", + "uid": 80 + }, + { + "fqn": "input.GetEvent", + "uid": 81 + }, + { + "fqn": "network.IP_Type", + "uid": 82 + }, + { + "fqn": "network.IPv4", + "uid": 83 + }, + { + "fqn": "network.IPv6", + "uid": 84 + }, + { + "fqn": "network.IP.AnyAddr", + "uid": 85 + }, + { + "fqn": "network.IP", + "uid": 86 + }, + { + "fqn": "network.EndPoint", + "uid": 87 + }, + { + "fqn": "network.udp.create_socket.SystemResources", + "uid": 88 + }, + { + "fqn": "network.udp.create_socket", + "uid": 89 + }, + { + "fqn": "network.udp.Bind.IllegalArgument", + "uid": 90 + }, + { + "fqn": "network.udp.Bind.AddressInUse", + "uid": 91 + }, + { + "fqn": "network.udp.Bind.IllegalValue", + "uid": 92 + }, + { + "fqn": "network.udp.Bind.InvalidHandle", + "uid": 93 + }, + { + "fqn": "network.udp.Bind.SystemResources", + "uid": 94 + }, + { + "fqn": "network.udp.Bind", + "uid": 95 + }, + { + "fqn": "network.udp.Connect.AlreadyConnected", + "uid": 96 + }, + { + "fqn": "network.udp.Connect.AlreadyConnecting", + "uid": 97 + }, + { + "fqn": "network.udp.Connect.AddressInUse", + "uid": 98 + }, + { + "fqn": "network.udp.Connect.BufferError", + "uid": 99 + }, + { + "fqn": "network.udp.Connect.IllegalArgument", + "uid": 100 + }, + { + "fqn": "network.udp.Connect.IllegalValue", + "uid": 101 + }, + { + "fqn": "network.udp.Connect.InProgress", + "uid": 102 + }, + { + "fqn": "network.udp.Connect.InvalidHandle", + "uid": 103 + }, + { + "fqn": "network.udp.Connect.LowlevelInterfaceError", + "uid": 104 + }, + { + "fqn": "network.udp.Connect.OutOfMemory", + "uid": 105 + }, + { + "fqn": "network.udp.Connect.Routing", + "uid": 106 + }, + { + "fqn": "network.udp.Connect.SystemResources", + "uid": 107 + }, + { + "fqn": "network.udp.Connect.Timeout", + "uid": 108 + }, + { + "fqn": "network.udp.Connect", + "uid": 109 + }, + { + "fqn": "network.udp.Disconnect.InvalidHandle", + "uid": 110 + }, + { + "fqn": "network.udp.Disconnect.NotConnected", + "uid": 111 + }, + { + "fqn": "network.udp.Disconnect.SystemResources", + "uid": 112 + }, + { + "fqn": "network.udp.Disconnect", + "uid": 113 + }, + { + "fqn": "network.udp.Send.BufferError", + "uid": 114 + }, + { + "fqn": "network.udp.Send.IllegalArgument", + "uid": 115 + }, + { + "fqn": "network.udp.Send.IllegalValue", + "uid": 116 + }, + { + "fqn": "network.udp.Send.InProgress", + "uid": 117 + }, + { + "fqn": "network.udp.Send.InvalidHandle", + "uid": 118 + }, + { + "fqn": "network.udp.Send.LowlevelInterfaceError", + "uid": 119 + }, + { + "fqn": "network.udp.Send.NotConnected", + "uid": 120 + }, + { + "fqn": "network.udp.Send.OutOfMemory", + "uid": 121 + }, + { + "fqn": "network.udp.Send.Routing", + "uid": 122 + }, + { + "fqn": "network.udp.Send.SystemResources", + "uid": 123 + }, + { + "fqn": "network.udp.Send.Timeout", + "uid": 124 + }, + { + "fqn": "network.udp.Send", + "uid": 125 + }, + { + "fqn": "network.udp.SendTo.BufferError", + "uid": 126 + }, + { + "fqn": "network.udp.SendTo.IllegalArgument", + "uid": 127 + }, + { + "fqn": "network.udp.SendTo.IllegalValue", + "uid": 128 + }, + { + "fqn": "network.udp.SendTo.InProgress", + "uid": 129 + }, + { + "fqn": "network.udp.SendTo.InvalidHandle", + "uid": 130 + }, + { + "fqn": "network.udp.SendTo.LowlevelInterfaceError", + "uid": 131 + }, + { + "fqn": "network.udp.SendTo.OutOfMemory", + "uid": 132 + }, + { + "fqn": "network.udp.SendTo.Routing", + "uid": 133 + }, + { + "fqn": "network.udp.SendTo.SystemResources", + "uid": 134 + }, + { + "fqn": "network.udp.SendTo.Timeout", + "uid": 135 + }, + { + "fqn": "network.udp.SendTo", + "uid": 136 + }, + { + "fqn": "network.udp.ReceiveFrom.BufferError", + "uid": 137 + }, + { + "fqn": "network.udp.ReceiveFrom.IllegalArgument", + "uid": 138 + }, + { + "fqn": "network.udp.ReceiveFrom.IllegalValue", + "uid": 139 + }, + { + "fqn": "network.udp.ReceiveFrom.InProgress", + "uid": 140 + }, + { + "fqn": "network.udp.ReceiveFrom.InvalidHandle", + "uid": 141 + }, + { + "fqn": "network.udp.ReceiveFrom.LowlevelInterfaceError", + "uid": 142 + }, + { + "fqn": "network.udp.ReceiveFrom.OutOfMemory", + "uid": 143 + }, + { + "fqn": "network.udp.ReceiveFrom.Routing", + "uid": 144 + }, + { + "fqn": "network.udp.ReceiveFrom.SystemResources", + "uid": 145 + }, + { + "fqn": "network.udp.ReceiveFrom.Timeout", + "uid": 146 + }, + { + "fqn": "network.udp.ReceiveFrom", + "uid": 147 + }, + { + "fqn": "network.tcp.create_socket.SystemResources", + "uid": 148 + }, + { + "fqn": "network.tcp.create_socket", + "uid": 149 + }, + { + "fqn": "network.tcp.Bind.AddressInUse", + "uid": 150 + }, + { + "fqn": "network.tcp.Bind.IllegalValue", + "uid": 151 + }, + { + "fqn": "network.tcp.Bind.InvalidHandle", + "uid": 152 + }, + { + "fqn": "network.tcp.Bind.SystemResources", + "uid": 153 + }, + { + "fqn": "network.tcp.Bind", + "uid": 154 + }, + { + "fqn": "network.tcp.Connect.AlreadyConnected", + "uid": 155 + }, + { + "fqn": "network.tcp.Connect.AlreadyConnecting", + "uid": 156 + }, + { + "fqn": "network.tcp.Connect.BufferError", + "uid": 157 + }, + { + "fqn": "network.tcp.Connect.ConnectionAborted", + "uid": 158 + }, + { + "fqn": "network.tcp.Connect.ConnectionClosed", + "uid": 159 + }, + { + "fqn": "network.tcp.Connect.ConnectionReset", + "uid": 160 + }, + { + "fqn": "network.tcp.Connect.IllegalArgument", + "uid": 161 + }, + { + "fqn": "network.tcp.Connect.IllegalValue", + "uid": 162 + }, + { + "fqn": "network.tcp.Connect.InProgress", + "uid": 163 + }, + { + "fqn": "network.tcp.Connect.InvalidHandle", + "uid": 164 + }, + { + "fqn": "network.tcp.Connect.LowlevelInterfaceError", + "uid": 165 + }, + { + "fqn": "network.tcp.Connect.OutOfMemory", + "uid": 166 + }, + { + "fqn": "network.tcp.Connect.Routing", + "uid": 167 + }, + { + "fqn": "network.tcp.Connect.SystemResources", + "uid": 168 + }, + { + "fqn": "network.tcp.Connect.Timeout", + "uid": 169 + }, + { + "fqn": "network.tcp.Connect", + "uid": 170 + }, + { + "fqn": "network.tcp.Send.BufferError", + "uid": 171 + }, + { + "fqn": "network.tcp.Send.ConnectionAborted", + "uid": 172 + }, + { + "fqn": "network.tcp.Send.ConnectionClosed", + "uid": 173 + }, + { + "fqn": "network.tcp.Send.ConnectionReset", + "uid": 174 + }, + { + "fqn": "network.tcp.Send.IllegalArgument", + "uid": 175 + }, + { + "fqn": "network.tcp.Send.IllegalValue", + "uid": 176 + }, + { + "fqn": "network.tcp.Send.InProgress", + "uid": 177 + }, + { + "fqn": "network.tcp.Send.InvalidHandle", + "uid": 178 + }, + { + "fqn": "network.tcp.Send.LowlevelInterfaceError", + "uid": 179 + }, + { + "fqn": "network.tcp.Send.NotConnected", + "uid": 180 + }, + { + "fqn": "network.tcp.Send.OutOfMemory", + "uid": 181 + }, + { + "fqn": "network.tcp.Send.Routing", + "uid": 182 + }, + { + "fqn": "network.tcp.Send.SystemResources", + "uid": 183 + }, + { + "fqn": "network.tcp.Send.Timeout", + "uid": 184 + }, + { + "fqn": "network.tcp.Send", + "uid": 185 + }, + { + "fqn": "network.tcp.Receive.AlreadyConnected", + "uid": 186 + }, + { + "fqn": "network.tcp.Receive.AlreadyConnecting", + "uid": 187 + }, + { + "fqn": "network.tcp.Receive.BufferError", + "uid": 188 + }, + { + "fqn": "network.tcp.Receive.ConnectionAborted", + "uid": 189 + }, + { + "fqn": "network.tcp.Receive.ConnectionClosed", + "uid": 190 + }, + { + "fqn": "network.tcp.Receive.ConnectionReset", + "uid": 191 + }, + { + "fqn": "network.tcp.Receive.IllegalArgument", + "uid": 192 + }, + { + "fqn": "network.tcp.Receive.IllegalValue", + "uid": 193 + }, + { + "fqn": "network.tcp.Receive.InProgress", + "uid": 194 + }, + { + "fqn": "network.tcp.Receive.InvalidHandle", + "uid": 195 + }, + { + "fqn": "network.tcp.Receive.LowlevelInterfaceError", + "uid": 196 + }, + { + "fqn": "network.tcp.Receive.NotConnected", + "uid": 197 + }, + { + "fqn": "network.tcp.Receive.OutOfMemory", + "uid": 198 + }, + { + "fqn": "network.tcp.Receive.Routing", + "uid": 199 + }, + { + "fqn": "network.tcp.Receive.SystemResources", + "uid": 200 + }, + { + "fqn": "network.tcp.Receive.Timeout", + "uid": 201 + }, + { + "fqn": "network.tcp.Receive", + "uid": 202 + }, + { + "fqn": "fs.find_filesystem", + "uid": 203 + }, + { + "fqn": "fs.Sync.DiskError", + "uid": 204 + }, + { + "fqn": "fs.Sync", + "uid": 205 + }, + { + "fqn": "fs.GetFilesystemInfo.DiskError", + "uid": 206 + }, + { + "fqn": "fs.GetFilesystemInfo.InvalidFileSystem", + "uid": 207 + }, + { + "fqn": "fs.GetFilesystemInfo", + "uid": 208 + }, + { + "fqn": "fs.OpenDrive.DiskError", + "uid": 209 + }, + { + "fqn": "fs.OpenDrive.FileNotFound", + "uid": 210 + }, + { + "fqn": "fs.OpenDrive.InvalidFileSystem", + "uid": 211 + }, + { + "fqn": "fs.OpenDrive.InvalidPath", + "uid": 212 + }, + { + "fqn": "fs.OpenDrive.NotADir", + "uid": 213 + }, + { + "fqn": "fs.OpenDrive.SystemFdQuotaExceeded", + "uid": 214 + }, + { + "fqn": "fs.OpenDrive.SystemResources", + "uid": 215 + }, + { + "fqn": "fs.OpenDrive", + "uid": 216 + }, + { + "fqn": "fs.OpenDir.DiskError", + "uid": 217 + }, + { + "fqn": "fs.OpenDir.FileNotFound", + "uid": 218 + }, + { + "fqn": "fs.OpenDir.InvalidHandle", + "uid": 219 + }, + { + "fqn": "fs.OpenDir.InvalidPath", + "uid": 220 + }, + { + "fqn": "fs.OpenDir.NotADir", + "uid": 221 + }, + { + "fqn": "fs.OpenDir.SystemFdQuotaExceeded", + "uid": 222 + }, + { + "fqn": "fs.OpenDir.SystemResources", + "uid": 223 + }, + { + "fqn": "fs.OpenDir", + "uid": 224 + }, + { + "fqn": "fs.CloseDir.InvalidHandle", + "uid": 225 + }, + { + "fqn": "fs.CloseDir", + "uid": 226 + }, + { + "fqn": "fs.ResetDirEnumeration.DiskError", + "uid": 227 + }, + { + "fqn": "fs.ResetDirEnumeration.InvalidHandle", + "uid": 228 + }, + { + "fqn": "fs.ResetDirEnumeration.SystemResources", + "uid": 229 + }, + { + "fqn": "fs.ResetDirEnumeration", + "uid": 230 + }, + { + "fqn": "fs.EnumerateDir.DiskError", + "uid": 231 + }, + { + "fqn": "fs.EnumerateDir.InvalidHandle", + "uid": 232 + }, + { + "fqn": "fs.EnumerateDir.SystemResources", + "uid": 233 + }, + { + "fqn": "fs.EnumerateDir", + "uid": 234 + }, + { + "fqn": "fs.Delete.DiskError", + "uid": 235 + }, + { + "fqn": "fs.Delete.FileNotFound", + "uid": 236 + }, + { + "fqn": "fs.Delete.InvalidHandle", + "uid": 237 + }, + { + "fqn": "fs.Delete.InvalidPath", + "uid": 238 + }, + { + "fqn": "fs.Delete", + "uid": 239 + }, + { + "fqn": "fs.MkDir.DiskError", + "uid": 240 + }, + { + "fqn": "fs.MkDir.Exists", + "uid": 241 + }, + { + "fqn": "fs.MkDir.InvalidHandle", + "uid": 242 + }, + { + "fqn": "fs.MkDir.InvalidPath", + "uid": 243 + }, + { + "fqn": "fs.MkDir", + "uid": 244 + }, + { + "fqn": "fs.StatEntry.DiskError", + "uid": 245 + }, + { + "fqn": "fs.StatEntry.FileNotFound", + "uid": 246 + }, + { + "fqn": "fs.StatEntry.InvalidHandle", + "uid": 247 + }, + { + "fqn": "fs.StatEntry.InvalidPath", + "uid": 248 + }, + { + "fqn": "fs.StatEntry", + "uid": 249 + }, + { + "fqn": "fs.NearMove.DiskError", + "uid": 250 + }, + { + "fqn": "fs.NearMove.Exists", + "uid": 251 + }, + { + "fqn": "fs.NearMove.FileNotFound", + "uid": 252 + }, + { + "fqn": "fs.NearMove.InvalidHandle", + "uid": 253 + }, + { + "fqn": "fs.NearMove.InvalidPath", + "uid": 254 + }, + { + "fqn": "fs.NearMove", + "uid": 255 + }, + { + "fqn": "fs.FarMove.DiskError", + "uid": 256 + }, + { + "fqn": "fs.FarMove.Exists", + "uid": 257 + }, + { + "fqn": "fs.FarMove.FileNotFound", + "uid": 258 + }, + { + "fqn": "fs.FarMove.InvalidHandle", + "uid": 259 + }, + { + "fqn": "fs.FarMove.InvalidPath", + "uid": 260 + }, + { + "fqn": "fs.FarMove.NoSpaceLeft", + "uid": 261 + }, + { + "fqn": "fs.FarMove", + "uid": 262 + }, + { + "fqn": "fs.Copy.DiskError", + "uid": 263 + }, + { + "fqn": "fs.Copy.Exists", + "uid": 264 + }, + { + "fqn": "fs.Copy.FileNotFound", + "uid": 265 + }, + { + "fqn": "fs.Copy.InvalidHandle", + "uid": 266 + }, + { + "fqn": "fs.Copy.InvalidPath", + "uid": 267 + }, + { + "fqn": "fs.Copy.NoSpaceLeft", + "uid": 268 + }, + { + "fqn": "fs.Copy", + "uid": 269 + }, + { + "fqn": "fs.OpenFile.DiskError", + "uid": 270 + }, + { + "fqn": "fs.OpenFile.Exists", + "uid": 271 + }, + { + "fqn": "fs.OpenFile.FileAlreadyExists", + "uid": 272 + }, + { + "fqn": "fs.OpenFile.FileNotFound", + "uid": 273 + }, + { + "fqn": "fs.OpenFile.InvalidHandle", + "uid": 274 + }, + { + "fqn": "fs.OpenFile.InvalidPath", + "uid": 275 + }, + { + "fqn": "fs.OpenFile.NoSpaceLeft", + "uid": 276 + }, + { + "fqn": "fs.OpenFile.SystemFdQuotaExceeded", + "uid": 277 + }, + { + "fqn": "fs.OpenFile.SystemResources", + "uid": 278 + }, + { + "fqn": "fs.OpenFile.WriteProtected", + "uid": 279 + }, + { + "fqn": "fs.OpenFile", + "uid": 280 + }, + { + "fqn": "fs.CloseFile.DiskError", + "uid": 281 + }, + { + "fqn": "fs.CloseFile.InvalidHandle", + "uid": 282 + }, + { + "fqn": "fs.CloseFile.SystemResources", + "uid": 283 + }, + { + "fqn": "fs.CloseFile", + "uid": 284 + }, + { + "fqn": "fs.FlushFile.DiskError", + "uid": 285 + }, + { + "fqn": "fs.FlushFile.InvalidHandle", + "uid": 286 + }, + { + "fqn": "fs.FlushFile.SystemResources", + "uid": 287 + }, + { + "fqn": "fs.FlushFile", + "uid": 288 + }, + { + "fqn": "fs.Read.DiskError", + "uid": 289 + }, + { + "fqn": "fs.Read.InvalidHandle", + "uid": 290 + }, + { + "fqn": "fs.Read.SystemResources", + "uid": 291 + }, + { + "fqn": "fs.Read", + "uid": 292 + }, + { + "fqn": "fs.Write.DiskError", + "uid": 293 + }, + { + "fqn": "fs.Write.InvalidHandle", + "uid": 294 + }, + { + "fqn": "fs.Write.NoSpaceLeft", + "uid": 295 + }, + { + "fqn": "fs.Write.SystemResources", + "uid": 296 + }, + { + "fqn": "fs.Write.WriteProtected", + "uid": 297 + }, + { + "fqn": "fs.Write", + "uid": 298 + }, + { + "fqn": "fs.StatFile.DiskError", + "uid": 299 + }, + { + "fqn": "fs.StatFile.InvalidHandle", + "uid": 300 + }, + { + "fqn": "fs.StatFile.SystemResources", + "uid": 301 + }, + { + "fqn": "fs.StatFile", + "uid": 302 + }, + { + "fqn": "fs.Resize.DiskError", + "uid": 303 + }, + { + "fqn": "fs.Resize.InvalidHandle", + "uid": 304 + }, + { + "fqn": "fs.Resize.NoSpaceLeft", + "uid": 305 + }, + { + "fqn": "fs.Resize.SystemResources", + "uid": 306 + }, + { + "fqn": "fs.Resize", + "uid": 307 + }, + { + "fqn": "shm.create.SystemResources", + "uid": 308 + }, + { + "fqn": "shm.create", + "uid": 309 + }, + { + "fqn": "shm.get_length.InvalidHandle", + "uid": 310 + }, + { + "fqn": "shm.get_length", + "uid": 311 + }, + { + "fqn": "shm.get_pointer.InvalidHandle", + "uid": 312 + }, + { + "fqn": "shm.get_pointer", + "uid": 313 + }, + { + "fqn": "pipe.create.SystemResources", + "uid": 314 + }, + { + "fqn": "pipe.create", + "uid": 315 + }, + { + "fqn": "pipe.get_fifo_length.InvalidHandle", + "uid": 316 + }, + { + "fqn": "pipe.get_fifo_length", + "uid": 317 + }, + { + "fqn": "pipe.get_object_size.InvalidHandle", + "uid": 318 + }, + { + "fqn": "pipe.get_object_size", + "uid": 319 + }, + { + "fqn": "pipe.Write", + "uid": 320 + }, + { + "fqn": "pipe.Read", + "uid": 321 + }, + { + "fqn": "sync.create_event.SystemResources", + "uid": 322 + }, + { + "fqn": "sync.create_event", + "uid": 323 + }, + { + "fqn": "sync.notify_one.InvalidHandle", + "uid": 324 + }, + { + "fqn": "sync.notify_one", + "uid": 325 + }, + { + "fqn": "sync.notify_all.InvalidHandle", + "uid": 326 + }, + { + "fqn": "sync.notify_all", + "uid": 327 + }, + { + "fqn": "sync.WaitForEvent.InvalidHandle", + "uid": 328 + }, + { + "fqn": "sync.WaitForEvent", + "uid": 329 + }, + { + "fqn": "sync.create_mutex.SystemResources", + "uid": 330 + }, + { + "fqn": "sync.create_mutex", + "uid": 331 + }, + { + "fqn": "sync.try_lock.InvalidHandle", + "uid": 332 + }, + { + "fqn": "sync.try_lock", + "uid": 333 + }, + { + "fqn": "sync.unlock.InvalidHandle", + "uid": 334 + }, + { + "fqn": "sync.unlock", + "uid": 335 + }, + { + "fqn": "sync.Lock.InvalidHandle", + "uid": 336 + }, + { + "fqn": "sync.Lock", + "uid": 337 + }, + { + "fqn": "draw.get_system_font.FileNotFound", + "uid": 338 + }, + { + "fqn": "draw.get_system_font.SystemResources", + "uid": 339 + }, + { + "fqn": "draw.get_system_font", + "uid": 340 + }, + { + "fqn": "draw.create_font.InvalidData", + "uid": 341 + }, + { + "fqn": "draw.create_font.SystemResources", + "uid": 342 + }, + { + "fqn": "draw.create_font", + "uid": 343 + }, + { + "fqn": "draw.is_system_font.InvalidHandle", + "uid": 344 + }, + { + "fqn": "draw.is_system_font", + "uid": 345 + }, + { + "fqn": "draw.measure_text_size.InvalidHandle", + "uid": 346 + }, + { + "fqn": "draw.measure_text_size", + "uid": 347 + }, + { + "fqn": "draw.create_memory_framebuffer.SystemResources", + "uid": 348 + }, + { + "fqn": "draw.create_memory_framebuffer", + "uid": 349 + }, + { + "fqn": "draw.create_video_framebuffer.InvalidHandle", + "uid": 350 + }, + { + "fqn": "draw.create_video_framebuffer.SystemResources", + "uid": 351 + }, + { + "fqn": "draw.create_video_framebuffer", + "uid": 352 + }, + { + "fqn": "draw.create_window_framebuffer.InvalidHandle", + "uid": 353 + }, + { + "fqn": "draw.create_window_framebuffer.SystemResources", + "uid": 354 + }, + { + "fqn": "draw.create_window_framebuffer", + "uid": 355 + }, + { + "fqn": "draw.create_widget_framebuffer.InvalidHandle", + "uid": 356 + }, + { + "fqn": "draw.create_widget_framebuffer.SystemResources", + "uid": 357 + }, + { + "fqn": "draw.create_widget_framebuffer", + "uid": 358 + }, + { + "fqn": "draw.get_framebuffer_type.InvalidHandle", + "uid": 359 + }, + { + "fqn": "draw.get_framebuffer_type", + "uid": 360 + }, + { + "fqn": "draw.get_framebuffer_size.InvalidHandle", + "uid": 361 + }, + { + "fqn": "draw.get_framebuffer_size", + "uid": 362 + }, + { + "fqn": "draw.get_framebuffer_memory.InvalidHandle", + "uid": 363 + }, + { + "fqn": "draw.get_framebuffer_memory.Unsupported", + "uid": 364 + }, + { + "fqn": "draw.get_framebuffer_memory", + "uid": 365 + }, + { + "fqn": "draw.invalidate_framebuffer", + "uid": 366 + }, + { + "fqn": "draw.Render.BadCode", + "uid": 367 + }, + { + "fqn": "draw.Render.InvalidHandle", + "uid": 368 + }, + { + "fqn": "draw.Render", + "uid": 369 + }, + { + "fqn": "gui.register_widget_type.AlreadyRegistered", + "uid": 370 + }, + { + "fqn": "gui.register_widget_type.SystemResources", + "uid": 371 + }, + { + "fqn": "gui.register_widget_type", + "uid": 372 + }, + { + "fqn": "gui.ShowMessageBox", + "uid": 373 + }, + { + "fqn": "gui.create_window.InvalidDimensions", + "uid": 374 + }, + { + "fqn": "gui.create_window.InvalidHandle", + "uid": 375 + }, + { + "fqn": "gui.create_window.SystemResources", + "uid": 376 + }, + { + "fqn": "gui.create_window", + "uid": 377 + }, + { + "fqn": "gui.get_window_title.InvalidHandle", + "uid": 378 + }, + { + "fqn": "gui.get_window_title", + "uid": 379 + }, + { + "fqn": "gui.get_window_size.InvalidHandle", + "uid": 380 + }, + { + "fqn": "gui.get_window_size", + "uid": 381 + }, + { + "fqn": "gui.get_window_min_size.InvalidHandle", + "uid": 382 + }, + { + "fqn": "gui.get_window_min_size", + "uid": 383 + }, + { + "fqn": "gui.get_window_max_size.InvalidHandle", + "uid": 384 + }, + { + "fqn": "gui.get_window_max_size", + "uid": 385 + }, + { + "fqn": "gui.get_window_flags.InvalidHandle", + "uid": 386 + }, + { + "fqn": "gui.get_window_flags", + "uid": 387 + }, + { + "fqn": "gui.set_window_size.InvalidHandle", + "uid": 388 + }, + { + "fqn": "gui.set_window_size", + "uid": 389 + }, + { + "fqn": "gui.resize_window.InvalidHandle", + "uid": 390 + }, + { + "fqn": "gui.resize_window", + "uid": 391 + }, + { + "fqn": "gui.set_window_title.InvalidHandle", + "uid": 392 + }, + { + "fqn": "gui.set_window_title", + "uid": 393 + }, + { + "fqn": "gui.mark_window_urgent.InvalidHandle", + "uid": 394 + }, + { + "fqn": "gui.mark_window_urgent", + "uid": 395 + }, + { + "fqn": "gui.GetWindowEvent.Cancelled", + "uid": 396 + }, + { + "fqn": "gui.GetWindowEvent.InProgress", + "uid": 397 + }, + { + "fqn": "gui.GetWindowEvent.InvalidHandle", + "uid": 398 + }, + { + "fqn": "gui.GetWindowEvent", + "uid": 399 + }, + { + "fqn": "gui.create_widget.SystemResources", + "uid": 400 + }, + { + "fqn": "gui.create_widget.WidgetNotFound", + "uid": 401 + }, + { + "fqn": "gui.create_widget.InvalidHandle", + "uid": 402 + }, + { + "fqn": "gui.create_widget", + "uid": 403 + }, + { + "fqn": "gui.place_widget.InvalidHandle", + "uid": 404 + }, + { + "fqn": "gui.place_widget", + "uid": 405 + }, + { + "fqn": "gui.WidgetControlID", + "uid": 406 + }, + { + "fqn": "gui.control_widget.SystemResources", + "uid": 407 + }, + { + "fqn": "gui.control_widget.InvalidHandle", + "uid": 408 + }, + { + "fqn": "gui.control_widget", + "uid": 409 + }, + { + "fqn": "gui.WidgetNotifyID", + "uid": 410 + }, + { + "fqn": "gui.notify_owner.SystemResources", + "uid": 411 + }, + { + "fqn": "gui.notify_owner.InvalidHandle", + "uid": 412 + }, + { + "fqn": "gui.notify_owner", + "uid": 413 + }, + { + "fqn": "gui.get_widget_data.InvalidHandle", + "uid": 414 + }, + { + "fqn": "gui.get_widget_data", + "uid": 415 + }, + { + "fqn": "gui.get_widget_bounds.InvalidHandle", + "uid": 416 + }, + { + "fqn": "gui.get_widget_bounds", + "uid": 417 + }, + { + "fqn": "gui.create_desktop.SystemResources", + "uid": 418 + }, + { + "fqn": "gui.create_desktop", + "uid": 419 + }, + { + "fqn": "gui.get_desktop_name.InvalidHandle", + "uid": 420 + }, + { + "fqn": "gui.get_desktop_name", + "uid": 421 + }, + { + "fqn": "gui.enumerate_desktops", + "uid": 422 + }, + { + "fqn": "gui.enumerate_desktop_windows.InvalidHandle", + "uid": 423 + }, + { + "fqn": "gui.enumerate_desktop_windows", + "uid": 424 + }, + { + "fqn": "gui.get_desktop_data.InvalidHandle", + "uid": 425 + }, + { + "fqn": "gui.get_desktop_data", + "uid": 426 + }, + { + "fqn": "gui.notify_message_box.BadRequestId", + "uid": 427 + }, + { + "fqn": "gui.notify_message_box.InvalidHandle", + "uid": 428 + }, + { + "fqn": "gui.notify_message_box", + "uid": 429 + }, + { + "fqn": "gui.post_window_event.SystemResources", + "uid": 430 + }, + { + "fqn": "gui.post_window_event.InvalidHandle", + "uid": 431 + }, + { + "fqn": "gui.post_window_event", + "uid": 432 + }, + { + "fqn": "gui.send_notification.SystemResources", + "uid": 433 + }, + { + "fqn": "gui.send_notification.InvalidHandle", + "uid": 434 + }, + { + "fqn": "gui.send_notification", + "uid": 435 + }, + { + "fqn": "gui.clipboard.set.SystemResources", + "uid": 436 + }, + { + "fqn": "gui.clipboard.set", + "uid": 437 + }, + { + "fqn": "gui.clipboard.get_type.InvalidHandle", + "uid": 438 + }, + { + "fqn": "gui.clipboard.get_type", + "uid": 439 + }, + { + "fqn": "gui.clipboard.get_value.InvalidHandle", + "uid": 440 + }, + { + "fqn": "gui.clipboard.get_value.SystemResources", + "uid": 441 + }, + { + "fqn": "gui.clipboard.get_value.ConversionFailed", + "uid": 442 + }, + { + "fqn": "gui.clipboard.get_value.ClipboardEmpty", + "uid": 443 + }, + { + "fqn": "gui.clipboard.get_value", + "uid": 444 + }, + { + "fqn": "service.create.AlreadyRegistered", + "uid": 445 + }, + { + "fqn": "service.create.SystemResources", + "uid": 446 + }, + { + "fqn": "service.create", + "uid": 447 + }, + { + "fqn": "service.enumerate", + "uid": 448 + }, + { + "fqn": "service.get_name.InvalidHandle", + "uid": 449 + }, + { + "fqn": "service.get_name", + "uid": 450 + }, + { + "fqn": "service.get_process.InvalidHandle", + "uid": 451 + }, + { + "fqn": "service.get_process", + "uid": 452 + }, + { + "fqn": "service.get_functions.InvalidHandle", + "uid": 453 + }, + { + "fqn": "service.get_functions", + "uid": 454 + }, + { + "fqn": "Service", + "uid": 455 + }, + { + "fqn": "SharedMemory", + "uid": 456 + }, + { + "fqn": "Pipe", + "uid": 457 + }, + { + "fqn": "Process", + "uid": 458 + }, + { + "fqn": "Thread", + "uid": 459 + }, + { + "fqn": "TcpSocket", + "uid": 460 + }, + { + "fqn": "UdpSocket", + "uid": 461 + }, + { + "fqn": "File", + "uid": 462 + }, + { + "fqn": "Directory", + "uid": 463 + }, + { + "fqn": "VideoOutput", + "uid": 464 + }, + { + "fqn": "Font", + "uid": 465 + }, + { + "fqn": "Framebuffer", + "uid": 466 + }, + { + "fqn": "Window", + "uid": 467 + }, + { + "fqn": "Widget", + "uid": 468 + }, + { + "fqn": "Desktop", + "uid": 469 + }, + { + "fqn": "WidgetType", + "uid": 470 + }, + { + "fqn": "SyncEvent", + "uid": 471 + }, + { + "fqn": "Mutex", + "uid": 472 + }, + { + "fqn": "max_fs_name_len", + "uid": 473 + }, + { + "fqn": "max_fs_type_len", + "uid": 474 + }, + { + "fqn": "max_file_name_len", + "uid": 475 + }, + { + "fqn": "UUID", + "uid": 476 + }, + { + "fqn": "DateTime", + "uid": 477 + }, + { + "fqn": "Absolute", + "uid": 478 + }, + { + "fqn": "Duration", + "uid": 479 + }, + { + "fqn": "PipeMode", + "uid": 480 + }, + { + "fqn": "NotificationSeverity", + "uid": 481 + }, + { + "fqn": "Await_Options.Thread_Affinity", + "uid": 482 + }, + { + "fqn": "Await_Options.Wait", + "uid": 483 + }, + { + "fqn": "Await_Options", + "uid": 484 + }, + { + "fqn": "VideoOutputID", + "uid": 485 + }, + { + "fqn": "FontType", + "uid": 486 + }, + { + "fqn": "FramebufferType", + "uid": 487 + }, + { + "fqn": "MessageBoxIcon", + "uid": 488 + }, + { + "fqn": "LogLevel", + "uid": 489 + }, + { + "fqn": "FileSystemId", + "uid": 490 + }, + { + "fqn": "FileAttributes", + "uid": 491 + }, + { + "fqn": "FileAccess", + "uid": 492 + }, + { + "fqn": "FileMode", + "uid": 493 + }, + { + "fqn": "KeyUsageCode", + "uid": 494 + }, + { + "fqn": "MouseButton", + "uid": 495 + }, + { + "fqn": "SpawnProcessArg.Type", + "uid": 496 + }, + { + "fqn": "SpawnProcessArg.Value", + "uid": 497 + }, + { + "fqn": "SpawnProcessArg.String", + "uid": 498 + }, + { + "fqn": "SpawnProcessArg", + "uid": 499 + }, + { + "fqn": "WindowFlags", + "uid": 500 + }, + { + "fqn": "CreateWindowFlags", + "uid": 501 + }, + { + "fqn": "WidgetDescriptor.Flags", + "uid": 502 + }, + { + "fqn": "WidgetDescriptor", + "uid": 503 + }, + { + "fqn": "WidgetControlMessage", + "uid": 504 + }, + { + "fqn": "WidgetNotifyEvent", + "uid": 505 + }, + { + "fqn": "MessageBoxResult", + "uid": 506 + }, + { + "fqn": "MessageBoxButtons.ok", + "uid": 507 + }, + { + "fqn": "MessageBoxButtons.ok_cancel", + "uid": 508 + }, + { + "fqn": "MessageBoxButtons.yes_no", + "uid": 509 + }, + { + "fqn": "MessageBoxButtons.yes_no_cancel", + "uid": 510 + }, + { + "fqn": "MessageBoxButtons.retry_cancel", + "uid": 511 + }, + { + "fqn": "MessageBoxButtons.abort_retry_ignore", + "uid": 512 + }, + { + "fqn": "MessageBoxButtons", + "uid": 513 + }, + { + "fqn": "DesktopDescriptor", + "uid": 514 + }, + { + "fqn": "DesktopEvent.Type", + "uid": 515 + }, + { + "fqn": "DesktopEvent", + "uid": 516 + }, + { + "fqn": "DesktopWindowEvent", + "uid": 517 + }, + { + "fqn": "DesktopWindowInvalidateEvent", + "uid": 518 + }, + { + "fqn": "DesktopNotificationEvent", + "uid": 519 + }, + { + "fqn": "MessageBoxEvent.RequestID", + "uid": 520 + }, + { + "fqn": "MessageBoxEvent", + "uid": 521 + }, + { + "fqn": "Color.black", + "uid": 522 + }, + { + "fqn": "Color.white", + "uid": 523 + }, + { + "fqn": "Color.red", + "uid": 524 + }, + { + "fqn": "Color.yellow", + "uid": 525 + }, + { + "fqn": "Color.lime", + "uid": 526 + }, + { + "fqn": "Color.green", + "uid": 527 + }, + { + "fqn": "Color.cyan", + "uid": 528 + }, + { + "fqn": "Color.blue", + "uid": 529 + }, + { + "fqn": "Color.purple", + "uid": 530 + }, + { + "fqn": "Color.magenta", + "uid": 531 + }, + { + "fqn": "Color.RGB888", + "uid": 532 + }, + { + "fqn": "Color.ARGB8888", + "uid": 533 + }, + { + "fqn": "Color.ABGR8888", + "uid": 534 + }, + { + "fqn": "Color", + "uid": 535 + }, + { + "fqn": "InputEvent.Type", + "uid": 536 + }, + { + "fqn": "InputEvent", + "uid": 537 + }, + { + "fqn": "WidgetEvent.Type", + "uid": 538 + }, + { + "fqn": "WidgetEvent", + "uid": 539 + }, + { + "fqn": "WindowEvent.Type", + "uid": 540 + }, + { + "fqn": "WindowEvent", + "uid": 541 + }, + { + "fqn": "SharedEventType", + "uid": 542 + }, + { + "fqn": "MouseEvent", + "uid": 543 + }, + { + "fqn": "KeyboardEvent", + "uid": 544 + }, + { + "fqn": "KeyboardModifiers", + "uid": 545 + }, + { + "fqn": "Point.zero", + "uid": 546 + }, + { + "fqn": "Point", + "uid": 547 + }, + { + "fqn": "Size.empty", + "uid": 548 + }, + { + "fqn": "Size.max", + "uid": 549 + }, + { + "fqn": "Size", + "uid": 550 + }, + { + "fqn": "Rectangle", + "uid": 551 + }, + { + "fqn": "VideoMemory", + "uid": 552 + }, + { + "fqn": "FileSystemInfo.Flags", + "uid": 553 + }, + { + "fqn": "FileSystemInfo", + "uid": 554 + }, + { + "fqn": "FileInfo", + "uid": 555 + }, + { + "fqn": "io.serial.SerialPortID", + "uid": 556 + }, + { + "fqn": "io.serial.SerialPort", + "uid": 557 + }, + { + "fqn": "io.serial.configure.InvalidHandle", + "uid": 558 + }, + { + "fqn": "io.serial.configure.ImpreciseBaudRate", + "uid": 559 + }, + { + "fqn": "io.serial.configure.UnsupportedDataBits", + "uid": 560 + }, + { + "fqn": "io.serial.configure.UnsupportedStopBits", + "uid": 561 + }, + { + "fqn": "io.serial.configure.UnsupportedParity", + "uid": 562 + }, + { + "fqn": "io.serial.configure.UnsupportedControlFlow", + "uid": 563 + }, + { + "fqn": "io.serial.configure", + "uid": 564 + }, + { + "fqn": "io.serial.control.InvalidHandle", + "uid": 565 + }, + { + "fqn": "io.serial.control.Unsupported", + "uid": 566 + }, + { + "fqn": "io.serial.control.ControlFlowActive", + "uid": 567 + }, + { + "fqn": "io.serial.control", + "uid": 568 + }, + { + "fqn": "io.serial.query_control.InvalidHandle", + "uid": 569 + }, + { + "fqn": "io.serial.query_control.Unsupported", + "uid": 570 + }, + { + "fqn": "io.serial.query_control", + "uid": 571 + }, + { + "fqn": "io.serial.write.InvalidHandle", + "uid": 572 + }, + { + "fqn": "io.serial.write.WordSizeMismatch", + "uid": 573 + }, + { + "fqn": "io.serial.write", + "uid": 574 + }, + { + "fqn": "io.serial.read.InvalidHandle", + "uid": 575 + }, + { + "fqn": "io.serial.read.WordSizeMismatch", + "uid": 576 + }, + { + "fqn": "io.serial.read", + "uid": 577 + }, + { + "fqn": "io.serial.break.InvalidHandle", + "uid": 578 + }, + { + "fqn": "io.serial.break.Unsupported", + "uid": 579 + }, + { + "fqn": "io.serial.break", + "uid": 580 + }, + { + "fqn": "io.serial.SerialPortError", + "uid": 581 + }, + { + "fqn": "io.serial.StopBits", + "uid": 582 + }, + { + "fqn": "io.serial.Parity", + "uid": 583 + }, + { + "fqn": "io.serial.ControlFlow", + "uid": 584 + }, + { + "fqn": "io.serial.SoftwareControlFlow", + "uid": 585 + }, + { + "fqn": "io.i2c.BusID", + "uid": 586 + }, + { + "fqn": "io.i2c.enumerate", + "uid": 587 + }, + { + "fqn": "io.i2c.query_metadata.NotFound", + "uid": 588 + }, + { + "fqn": "io.i2c.query_metadata", + "uid": 589 + }, + { + "fqn": "io.i2c.Bus", + "uid": 590 + }, + { + "fqn": "io.i2c.open.NotFound", + "uid": 591 + }, + { + "fqn": "io.i2c.open.SystemResources", + "uid": 592 + }, + { + "fqn": "io.i2c.open", + "uid": 593 + }, + { + "fqn": "io.i2c.Execute.InvalidHandle", + "uid": 594 + }, + { + "fqn": "io.i2c.Execute.InvalidAddress", + "uid": 595 + }, + { + "fqn": "io.i2c.Execute.EmptyOperation", + "uid": 596 + }, + { + "fqn": "io.i2c.Execute.ExecutionFailed", + "uid": 597 + }, + { + "fqn": "io.i2c.Execute", + "uid": 598 + }, + { + "fqn": "io.i2c.Operation.Type", + "uid": 599 + }, + { + "fqn": "io.i2c.Operation.Error", + "uid": 600 + }, + { + "fqn": "io.i2c.Operation", + "uid": 601 + }, + { + "fqn": "Syscall_ID", + "uid": 602 + }, + { + "fqn": "SystemResource.Type", + "uid": 603 + }, + { + "fqn": "overlapped.ARC.Type", + "uid": 604 + } + ] +} \ No newline at end of file diff --git a/src/abi/utility/render_zig_code.zig b/src/abi/utility/render_zig_code.zig index ecc7d7fa..94ccbaed 100644 --- a/src/abi/utility/render_zig_code.zig +++ b/src/abi/utility/render_zig_code.zig @@ -1018,10 +1018,13 @@ const ZigRenderer = struct { }); } - fn render_docs(zr: *ZigRenderer, docs: model.DocString) !void { - for (docs) |line| { - try zr.writer.println("/// {s}", .{line}); - } + fn render_docs(zr: *ZigRenderer, docs: model.DocComment) !void { + _=zr; + _=docs; + // TODO: COnsider if it's worth to include the doc strings inside the generated zig code + // for (docs) |line| { + // try zr.writer.println("/// {s}", .{line}); + // } } fn fmt_type(zr: *ZigRenderer, type_id: model.TypeIndex) ZigTypeFmt { diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index 4500f337..867714a0 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -425,6 +425,11 @@ pub const Type = union(enum) { .one, .unknown => true, .slice => false, }, + .resource => true, // resources are also represented as nullable pointers in C-ABI + .well_known => |id| switch(id) { + .anyptr, .anyfnptr => true, + else => false, + }, else => false, }; }, diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index f19c9694..502f7ab2 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -1714,7 +1714,12 @@ const Analyzer = struct { } for (sc.native_inputs) |inp| { - std.debug.assert(ana.get_resolved_type(inp.type).is_c_abi_compatible(ana.types.items)); + if (!ana.get_resolved_type(inp.type).is_c_abi_compatible(ana.types.items)) { + std.debug.panic("unexpected non-compatible type for native input {s}.{s}", .{ + sc.full_qualified_name[sc.full_qualified_name.len - 1], + inp.name, + }); + } // inputs cannot be the error role std.debug.assert(inp.role != .@"error"); @@ -1829,7 +1834,8 @@ const Analyzer = struct { switch (fld_type) { .well_known => |id| std.debug.assert(id.size_in_bits() != null), .@"enum", .bitstruct => {}, - else => unreachable, + .uint, .int => {}, + else => std.debug.panic("Unsupported bit type: {t}", .{fld_type}), } std.debug.assert(fld.bit_count != null); From 03406f954165e4268e78b41ddec21d0a51214ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 09:13:31 +0100 Subject: [PATCH 05/36] Adds abi-mapper to be installed from main zig file --- build.zig | 4 ++++ build.zig.zon | 3 +++ 2 files changed, 7 insertions(+) diff --git a/build.zig b/build.zig index 442943d6..8ff587b4 100644 --- a/build.zig +++ b/build.zig @@ -50,6 +50,10 @@ const installed_tools: []const ToolDep = &.{ .dependency = "gui_designer", .artifacts = &.{ "gui-editor", "gui-compiler" }, }, + .{ + .dependency = "abi_mapper", + .artifacts = &.{"abi-parser"}, + }, // .{ // .dependency = "agp_tester", // .artifacts = &.{"agp-tester"}, diff --git a/build.zig.zon b/build.zig.zon index 2a05e448..ac548676 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -40,6 +40,9 @@ .agp_tester = .{ .path = "src/tools/agp-tester", }, + .abi_mapper = .{ + .path = "src/tools/abi-mapper", + }, // .wikitool = .{ // .path = "src/tools/wikitool", // }, From 094759db1faf9b1668bd3c632db1d5595cc9abc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 09:32:54 +0100 Subject: [PATCH 06/36] Starts adjusting website to abi-mapper DocComment type --- src/tools/abi-mapper/src/model.zig | 10 +- src/website/src/syscalls-gen.zig | 166 ++++++++++++++--------------- 2 files changed, 90 insertions(+), 86 deletions(-) diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index 867714a0..525348f2 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -29,6 +29,10 @@ pub const FQN = []const []const u8; pub const DocComment = struct { sections: []const Section, + pub fn is_empty(docs: DocComment) bool { + return docs.sections.len == 0; + } + pub const empty: DocComment = .{ .sections = &.{} }; pub const Section = struct { @@ -426,9 +430,9 @@ pub const Type = union(enum) { .slice => false, }, .resource => true, // resources are also represented as nullable pointers in C-ABI - .well_known => |id| switch(id) { - .anyptr, .anyfnptr => true, - else => false, + .well_known => |id| switch (id) { + .anyptr, .anyfnptr => true, + else => false, }, else => false, }; diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index 399e7072..c66a8593 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -41,9 +41,7 @@ pub fn render(output_dir: std.fs.Dir, schema: abi_parser.model.Document, allocat defer tree.stack.deinit(allocator); try tree.render_declaration(syscalls_dst_dir, "ashet", 0, .{ - .docs = &.{ - // TODO: can we add top-level namespace docs?! - }, + .docs = .empty, // TODO: can we add top-level namespace docs?! .children = schema.root, .full_qualified_name = &.{}, .data = .namespace, @@ -123,7 +121,7 @@ const PageRenderer = struct { }, ); - if (decl.docs.len > 0) { + if (!decl.docs.is_empty()) { try html.writer.writeAll("
\n"); try html.writer.print("

Documentation

\n", .{}); try html.writer.print("{f}\n", .{fmt_docs(decl.docs)}); @@ -152,7 +150,7 @@ const PageRenderer = struct { try html.writer.writeAll(""); - if (field.docs.len > 0) { + if (!field.docs.is_empty()) { try html.writer.print("
{f}
\n", .{fmt_docs(field.docs)}); } @@ -229,7 +227,7 @@ const PageRenderer = struct { try html.writer.writeAll(""); - if (field.docs.len > 0) { + if (!field.docs.is_empty()) { try html.writer.print("
{f}
\n", .{fmt_docs(field.docs)}); } @@ -543,7 +541,7 @@ const PageRenderer = struct { } try html.writer.writeAll(""); - if (child.docs.len > 0) { + if (!child.docs.is_empty()) { try html.writer.print( \\
{f}
\\ @@ -738,85 +736,87 @@ fn format_fqn(fqn: []const []const u8, writer: *std.Io.Writer) !void { } } -fn fmt_docs(docs: []const []const u8) std.fmt.Alt([]const []const u8, format_docs) { +fn fmt_docs(docs: model.DocComment) std.fmt.Alt(model.DocComment, format_docs) { return .{ .data = docs }; } -fn format_docs(docs: []const []const u8, writer: *std.Io.Writer) !void { - if (docs.len == 0) +fn format_docs(docs: model.DocComment, writer: *std.Io.Writer) !void { + if (docs.is_empty()) return; - const BlockType = enum { note, lore, relates }; - - var last_was_empty = false; - - try writer.writeAll("

"); - - for (docs) |line| { - if (line.len == 0) { - last_was_empty = true; - continue; - } - - var requires_new_paragraph = false; - - if (last_was_empty) { - requires_new_paragraph = true; - last_was_empty = false; - } - - var out_line = std.mem.trim(u8, line, " "); - var change_block_type: ?BlockType = null; - - if (std.mem.startsWith(u8, out_line, "NOTE:")) { - change_block_type = .note; - requires_new_paragraph = true; - out_line = out_line[5..]; - } else if (std.mem.startsWith(u8, out_line, "LORE:")) { - change_block_type = .lore; - requires_new_paragraph = true; - out_line = out_line[5..]; - } else if (std.mem.startsWith(u8, out_line, "RELATES:")) { - change_block_type = .relates; - requires_new_paragraph = true; - out_line = out_line[8..]; - } - - if (change_block_type) |block_type| { - std.debug.assert(requires_new_paragraph == true); - - try writer.writeAll("

"); - - try writer.print("

{s}

", .{ - @tagName(block_type), - switch (block_type) { - .lore => "Lore:", - .note => "Note:", - .relates => "Related Elements:", - }, - }); - } else if (requires_new_paragraph) { - try writer.writeAll("

\n

"); - } - - out_line = std.mem.trimRight(u8, out_line, " \t\r\n"); - - var in_code = false; - for (out_line) |char| { - switch (char) { - '`' => { - in_code = !in_code; - if (in_code) { - try writer.writeAll(""); - } else { - try writer.writeAll(""); - } - }, - else => try writer.writeByte(char), - } - } - - try writer.writeAll("\n"); - } - try writer.writeAll("

"); + _ = writer; + + // const BlockType = enum { note, lore, relates }; + + // var last_was_empty = false; + + // try writer.writeAll("

"); + + // for (docs) |line| { + // if (line.len == 0) { + // last_was_empty = true; + // continue; + // } + + // var requires_new_paragraph = false; + + // if (last_was_empty) { + // requires_new_paragraph = true; + // last_was_empty = false; + // } + + // var out_line = std.mem.trim(u8, line, " "); + // var change_block_type: ?BlockType = null; + + // if (std.mem.startsWith(u8, out_line, "NOTE:")) { + // change_block_type = .note; + // requires_new_paragraph = true; + // out_line = out_line[5..]; + // } else if (std.mem.startsWith(u8, out_line, "LORE:")) { + // change_block_type = .lore; + // requires_new_paragraph = true; + // out_line = out_line[5..]; + // } else if (std.mem.startsWith(u8, out_line, "RELATES:")) { + // change_block_type = .relates; + // requires_new_paragraph = true; + // out_line = out_line[8..]; + // } + + // if (change_block_type) |block_type| { + // std.debug.assert(requires_new_paragraph == true); + + // try writer.writeAll("

"); + + // try writer.print("

{s}

", .{ + // @tagName(block_type), + // switch (block_type) { + // .lore => "Lore:", + // .note => "Note:", + // .relates => "Related Elements:", + // }, + // }); + // } else if (requires_new_paragraph) { + // try writer.writeAll("

\n

"); + // } + + // out_line = std.mem.trimRight(u8, out_line, " \t\r\n"); + + // var in_code = false; + // for (out_line) |char| { + // switch (char) { + // '`' => { + // in_code = !in_code; + // if (in_code) { + // try writer.writeAll(""); + // } else { + // try writer.writeAll(""); + // } + // }, + // else => try writer.writeByte(char), + // } + // } + + // try writer.writeAll("\n"); + // } + // try writer.writeAll("

"); } From 98587c05ae0df193432aecf03a3d7c2c49bef0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 21:33:45 +0100 Subject: [PATCH 07/36] Implements rendering of new doccomment scheme --- src/abi/src/ashet.abi | 22 ++--- src/website/build.zig | 2 +- src/website/src/syscalls-gen.zig | 142 +++++++++++++++---------------- src/website/www/theme.css | 20 +++++ 4 files changed, 99 insertions(+), 87 deletions(-) diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index 5af9d46a..e0254c13 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -84,15 +84,15 @@ namespace overlapped { } /// Awaits one or more scheduled asynchronous operations and returns the - /// number of `completed` elements. + /// number of @`completed` elements. /// - /// The kernel will fill out `completed` up to the returned number of elements. + /// The kernel will fill out @`completed` up to the returned number of elements. /// All other values are undefined. /// /// NOTE: For blocking operations, this function will suspend the current /// thread until the request has been completed. /// - /// RELATES: @ref await_completion_of + /// RELATES: @`await_completion_of` syscall await_completion { in completed: []*ARC; in options: Await_Options; @@ -101,26 +101,26 @@ namespace overlapped { } /// Awaits one or more explicit asynchronous operations and returns the - /// number of `events` elements. + /// number of @`events` elements. /// - /// The kernel will only await elements provided in `events` and all of those events must - /// not be awaited by another `await_completion_of`. + /// The kernel will only await elements provided in @`events` and all of those events must + /// not be awaited by another @`await_completion_of`. /// - /// When the function returns, `events` will have all completed events unchanged, and all + /// When the function returns, @`events` will have all completed events unchanged, and all /// unfinished events set to `null`. This way, a simple check via index can be done instead of - /// the need for iteration of `events` to find what was finished. + /// the need for iteration of @`events` to find what was finished. /// /// NOTE: This syscall will always return as soon as a single event has finished. /// - /// NOTE: It is invalid to await the same operation with two concurrent calls to `await_completion_of`. + /// NOTE: It is invalid to await the same operation with two concurrent calls to @`await_completion_of`. /// /// NOTE: Elements awaited with this function will be guaranteed to not be returned by - /// another concurrent call to `await_completion`. + /// another concurrent call to @`await_completion`. /// /// NOTE: For blocking operations, this function will suspend the current /// thread until the request has been completed. /// - /// RELATES: @ref await_completion + /// RELATES: @`await_completion` syscall await_completion_of { in events: []?*ARC; error InvalidOperation; diff --git a/src/website/build.zig b/src/website/build.zig index 887dc0e1..6cfbec72 100644 --- a/src/website/build.zig +++ b/src/website/build.zig @@ -4,7 +4,7 @@ const kernel_package = @import("kernel"); const Machine = kernel_package.Machine; -pub fn build(b: *std.Build) void { +pub fn build(b: *std.Build) void { // $ls root_id 1 const install_step = b.getInstallStep(); const os_dep = b.dependency("os", .{ diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index c66a8593..a8d5b2be 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -9,6 +9,7 @@ const model = abi_parser.model; const render_page_file = website_gen.render_page_file; const fmt_html = website_gen.fmt_html; const fmt_url = website_gen.fmt_url; +const fmt_attr = website_gen.fmt_attr; const Context = struct { root_dir: std.fs.Dir, @@ -744,79 +745,70 @@ fn format_docs(docs: model.DocComment, writer: *std.Io.Writer) !void { if (docs.is_empty()) return; - _ = writer; - - // const BlockType = enum { note, lore, relates }; - - // var last_was_empty = false; - - // try writer.writeAll("

"); - - // for (docs) |line| { - // if (line.len == 0) { - // last_was_empty = true; - // continue; - // } - - // var requires_new_paragraph = false; - - // if (last_was_empty) { - // requires_new_paragraph = true; - // last_was_empty = false; - // } - - // var out_line = std.mem.trim(u8, line, " "); - // var change_block_type: ?BlockType = null; - - // if (std.mem.startsWith(u8, out_line, "NOTE:")) { - // change_block_type = .note; - // requires_new_paragraph = true; - // out_line = out_line[5..]; - // } else if (std.mem.startsWith(u8, out_line, "LORE:")) { - // change_block_type = .lore; - // requires_new_paragraph = true; - // out_line = out_line[5..]; - // } else if (std.mem.startsWith(u8, out_line, "RELATES:")) { - // change_block_type = .relates; - // requires_new_paragraph = true; - // out_line = out_line[8..]; - // } - - // if (change_block_type) |block_type| { - // std.debug.assert(requires_new_paragraph == true); - - // try writer.writeAll("

"); - - // try writer.print("

{s}

", .{ - // @tagName(block_type), - // switch (block_type) { - // .lore => "Lore:", - // .note => "Note:", - // .relates => "Related Elements:", - // }, - // }); - // } else if (requires_new_paragraph) { - // try writer.writeAll("

\n

"); - // } - - // out_line = std.mem.trimRight(u8, out_line, " \t\r\n"); - - // var in_code = false; - // for (out_line) |char| { - // switch (char) { - // '`' => { - // in_code = !in_code; - // if (in_code) { - // try writer.writeAll(""); - // } else { - // try writer.writeAll(""); - // } - // }, - // else => try writer.writeByte(char), - // } - // } - - // try writer.writeAll("\n"); - // } - // try writer.writeAll("

"); + for (docs.sections) |section| { + try writer.print("
\n", .{section.kind}); + + for (section.blocks) |block| { + switch (block) { + .paragraph => |p| { + try writer.writeAll("

\n"); + try format_inlines(p.content, writer); + try writer.writeAll("

\n"); + }, + + .ordered_list => |list| { + try writer.writeAll("
    \n"); + for (list.items) |item| { + try writer.writeAll("
  1. \n"); + try format_inlines(item, writer); + try writer.writeAll("
  2. \n"); + } + try writer.writeAll("
\n"); + }, + .unordered_list => |list| { + try writer.writeAll("
    \n"); + for (list.items) |item| { + try writer.writeAll("
  • \n"); + try format_inlines(item, writer); + try writer.writeAll("
  • \n"); + } + try writer.writeAll("
\n"); + }, + + .code_block => |code| { + try writer.writeAll("
");
+                    try writer.print("{f}", .{fmt_attr(code.content)});
+                    try writer.writeAll("
\n"); + }, + } + } + + try writer.writeAll("
\n"); + } +} + +fn format_inlines(inlines: []const model.DocComment.Inline, writer: *std.Io.Writer) !void { + for (inlines) |span| { + switch (span) { + .text => |text| try writer.writeAll(text.value), + .code => |code| try writer.print("{s}", .{code.value}), + .emphasis => |emphasis| { + try writer.writeAll(""); + try format_inlines(emphasis.content, writer); + try writer.writeAll(""); + }, + .ref => |ref| try writer.print("[[{s}]]", .{ref.fqn}), + .link => |link| { + try writer.print("", .{fmt_attr(link.url)}); + try format_inlines(link.content, writer); + try writer.writeAll(""); + }, + } + } } diff --git a/src/website/www/theme.css b/src/website/www/theme.css index 15dfb85f..6f1318fe 100644 --- a/src/website/www/theme.css +++ b/src/website/www/theme.css @@ -248,6 +248,26 @@ div.flex-spacer { margin-bottom: 0.5em; } +.docs-container .doc-section { + margin-block: 0.75em; + line-height: 125%; +} + + +.docs-container .doc-section>:is(p,li,ul,pre) { + margin-block: 0.75em; + margin-left: 2em; +} + +.docs-container .doc-section-note::before { + content: 'NOTE:'; + font-weight: bold; +} + +.docs-container .doc-section-main>:is(p,li,ul,pre) { + margin-left: 0; +} + /************************************************* * Syntax Highlighting * *************************************************/ From f951772e099c8314beb6876a03cb09f7ca5dfd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 21:37:49 +0100 Subject: [PATCH 08/36] Adds missing code admonitions to the theme.css --- src/website/www/theme.css | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/website/www/theme.css b/src/website/www/theme.css index 6f1318fe..b41c1b12 100644 --- a/src/website/www/theme.css +++ b/src/website/www/theme.css @@ -264,6 +264,32 @@ div.flex-spacer { font-weight: bold; } +.docs-container .doc-section-warning::before { + content: 'WARNING:'; + font-weight: bold; +} + +.docs-container .doc-section-lore::before { + content: 'LORE:'; + font-weight: bold; +} + +.docs-container .doc-section-example::before { + content: 'EXAMPLE:'; + font-weight: bold; +} + +.docs-container .doc-section-deprecated::before { + content: 'DEPRECATED:'; + font-weight: bold; +} + +.docs-container .doc-section-decision::before { + content: 'DECISION:'; + font-weight: bold; +} + + .docs-container .doc-section-main>:is(p,li,ul,pre) { margin-left: 0; } From 047f3a1384738494c5392f17dd23fc4b201419d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 22:17:28 +0100 Subject: [PATCH 09/36] Introduces LEARN admonition into code docs, fixes all references inside ashet.abi, adds broken links for code references. --- src/abi/src/ashet.abi | 182 +++++++++--------- src/tools/abi-mapper/rework/abi-doc-format.md | 8 +- src/tools/abi-mapper/src/doc_comment.zig | 1 + src/tools/abi-mapper/src/model.zig | 1 + src/website/src/syscalls-gen.zig | 8 +- src/website/www/theme.css | 10 +- 6 files changed, 115 insertions(+), 95 deletions(-) diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index e0254c13..8007cd16 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -132,7 +132,7 @@ namespace overlapped { /// /// NOTE: If the operation has already completed, an error will be returned saying so. /// - /// NOTE: The cancelled operation will not be returned by `await_completion` or `await_completion_of` anymore. + /// NOTE: The cancelled operation will not be returned by @`await_completion` or @`await_completion_of` anymore. syscall cancel { in aop: *ARC; error Completed; @@ -165,7 +165,7 @@ namespace process { error InvalidHandle; } - /// Returns the arguments that were passed to this process in `Spawn`. + /// Returns the arguments that were passed to this process in @`Spawn`. syscall get_arguments { in target: ?Process; in argv: ?[]SpawnProcessArg; @@ -188,12 +188,12 @@ namespace process { /// Spawns a new process async_call Spawn { - /// Relative base directory for `path`. + /// Relative base directory for @`path`. in dir: Directory; - /// File name of the executable relative to `dir`. + /// File name of the executable relative to @`dir`. in path: str; /// The arguments passed to the process. - /// If a `SystemResource` is passed, it will receive the created process as a owning process. + /// If a @`SystemResource` is passed, it will receive the created process as a owning process. /// It is safe to release the resource in this process as soon as this operation returns. in argv: []const SpawnProcessArg; /// Handle to the spawned process. @@ -226,12 +226,12 @@ namespace process { } /// Defines the signature of a thread entry point. - /// The parameter is the `arg` value passed to `spawn`. + /// The parameter is the @`arg` value passed to @`spawn`. /// The return value is the exit code of the thread. typedef ThreadFunction = fnptr (?anyptr) u32; - /// Spawns a new thread with `function` passing `arg` to it. - /// If `stack_size` is not 0, will create a stack with the given size. + /// Spawns a new thread with @`function` passing @`arg` to it. + /// If @`stack_size` is not 0, will create a stack with the given size. syscall spawn { in function: ThreadFunction; in arg: ?anyptr; @@ -240,7 +240,7 @@ namespace process { error SystemResources; } - /// Kills the given thread with `exit_code`. + /// Kills the given thread with @`exit_code`. syscall kill { in target: Thread; in exit_code: ExitCode; @@ -289,9 +289,9 @@ namespace process { /// The alignment in ptr_align: u8; - /// A non-`null` pointer that points to exactly @ref size bytes. + /// A non-`null` pointer that points to exactly @`size` bytes. /// - /// NOTE: In practise, this might point to more than @ref size bytes, + /// NOTE: In practise, this might point to more than @`size` bytes, /// but the code must not assume *any* excess bytes may exist. out memory: [*]u8; @@ -301,10 +301,10 @@ namespace process { /// Returns previously allocated memory back to the process heap. syscall release { - /// The complete chunk of memory previously allocated with @ref allocate. + /// The complete chunk of memory previously allocated with @`allocate`. in mem: []u8; - /// The alignment that was passed to @ref allocate.ptr_align previously. + /// The alignment that was passed to @`allocate.ptr_align` previously. in ptr_align: u8; } } @@ -354,7 +354,7 @@ namespace clock { out time: Absolute; } - /// Sleeps until `clock.monotonic()` returns at least `timeout`. + /// Sleeps until `clock.monotonic()` returns at least @`timeout`. async_call Timer { /// Monotonic timestamp in nanoseconds until the operation completes. in timeout: Absolute; @@ -370,7 +370,7 @@ namespace datetime { out datetime: DateTime; } - /// Sleeps until `datetime.now()` returns a point in time that comes after `when`. + /// Sleeps until `datetime.now()` returns a point in time that comes after @`when`. async_call Alarm { /// Earliest possible date time of when the alarm triggers. in when: DateTime; @@ -381,7 +381,7 @@ namespace datetime { namespace video { /// Returns a list of all video outputs. /// - /// If `ids` is `null`, the total number of available outputs is returned; + /// If @`ids` is `null`, the total number of available outputs is returned; /// otherwise, up to `ids.len` elements are written into the provided array /// and the number of written elements is returned. syscall enumerate { @@ -701,7 +701,7 @@ namespace fs { } /// Gets information about a file system. - /// Also returns a `next` id that can be used to iterate over all filesystems. + /// Also returns a @`next` id that can be used to iterate over all filesystems. /// The `system` filesystem is guaranteed to be the first one. async_call GetFilesystemInfo { in fs_id: FileSystemId; @@ -774,7 +774,7 @@ namespace fs { error InvalidPath; } - /// creates a new directory relative to dir. If `path` contains subdirectories, all + /// creates a new directory relative to dir. If @`path` contains subdirectories, all /// directories are created. async_call MkDir { in dir: Directory; @@ -921,7 +921,7 @@ namespace fs { } namespace shm { - /// Constructs a new shared memory object with `size` bytes of memory. + /// Constructs a new shared memory object with @`size` bytes of memory. /// Shared memory can be written by all processes without any memory protection. syscall create { in size: usize; @@ -946,9 +946,9 @@ namespace shm { } namespace pipe { - /// Spawns a new pipe with `fifo_length` elements of `object_size` bytes. - /// If `fifo_length` is 0, the pipe is synchronous and can only send data - /// if a `read` call is active. Otherwise, up to `fifo_length` elements can be + /// Spawns a new pipe with @`fifo_length` elements of @`object_size` bytes. + /// If @`fifo_length` is 0, the pipe is synchronous and can only send data + /// if a @`Read` call is active. Otherwise, up to @`fifo_length` elements can be /// stored in a FIFO. syscall create { in object_size: usize; @@ -971,14 +971,14 @@ namespace pipe { error InvalidHandle; } - /// Writes elements from `data` into the given pipe. + /// Writes elements from @`data` into the given pipe. async_call Write { in handle: Pipe; /// Pointer to the first element. Length defines how many elements are to be transferred. in data: bytestr; - /// Distance between each element in `data`. Can be different from the pipes element size + /// Distance between each element in @`data`. Can be different from the pipes element size /// to allow sparse data to be transferred. - /// If `0`, it will use the `object_size` property of the pipe. + /// If `0`, it will use the @`object_size` property of the pipe. in stride: usize; /// Defines how the write should operate. in mode: PipeMode; @@ -986,14 +986,14 @@ namespace pipe { out count: usize; } - /// Reads elements from a pipe into `buffer`. + /// Reads elements from a pipe into @`buffer`. async_call Read { in handle: Pipe; /// Points to the first element to be received. in buffer: bytebuf; - /// Distance between each element in `buffer`. Can be different from the pipes element size + /// Distance between each element in @`buffer`. Can be different from the pipes element size /// to allow sparse data to be transferred. - /// If `0`, it will use the `object_size` property of the pipe. + /// If `0`, it will use the @`object_size` property of the pipe. in stride: usize; /// Defines how the read should operate. in mode: PipeMode; @@ -1004,26 +1004,26 @@ namespace pipe { } namespace sync { - /// Creates a new `SyncEvent` object that can be used to synchronize + /// Creates a new @`SyncEvent` object that can be used to synchronize /// different processes. syscall create_event { out event: SyncEvent; error SystemResources; } - /// Completes one `WaitForEvent` IOP waiting for the given event. + /// Completes one @`WaitForEvent` IOP waiting for the given event. syscall notify_one { in event: SyncEvent; error InvalidHandle; } - /// Completes all `WaitForEvent` IOP waiting for the given event. + /// Completes all @`WaitForEvent` IOP waiting for the given event. syscall notify_all { in event: SyncEvent; error InvalidHandle; } - /// Waits for the given `SyncEvent` to be notified. + /// Waits for the given @`SyncEvent` to be notified. async_call WaitForEvent { in event: SyncEvent; error InvalidHandle; @@ -1042,7 +1042,7 @@ namespace sync { error InvalidHandle; } - /// Unlocks a mutual exclusion. Completes a single `Lock` IOP if it exists. + /// Unlocks a mutual exclusion. Completes a single @`Lock` IOP if it exists. syscall unlock { in mutex: Mutex; error InvalidHandle; @@ -1153,7 +1153,7 @@ namespace draw { in area: Rectangle; } - /// Renders the provided Ashet Graphics Protocol `sequence` into `target` framebuffer. + /// Renders the provided Ashet Graphics Protocol @`sequence` into @`target` framebuffer. /// /// The function will run asynchronously and will return as soon as the rendering is done. /// @@ -1238,7 +1238,7 @@ namespace gui { error InvalidHandle; } - /// Sets the `size` of `window` and returns the new actual size. + /// Sets the @`size` of @`window` and returns the new actual size. /// NOTE: This event is meant to be used from desktop APIs and will not automatically /// notify the window of the resize event. syscall set_window_size { @@ -1269,7 +1269,7 @@ namespace gui { error InvalidHandle; } - /// Waits for an event on the given `Window`, completing as soon as + /// Waits for an event on the given @`Window`, completing as soon as /// an event arrived. async_call GetWindowEvent { in window: Window; @@ -1279,8 +1279,8 @@ namespace gui { error InvalidHandle; } - /// Create a new widget identified by `uuid` on the given `window`. - /// Position and size of the widget are undetermined at start and a call to `place_widget` should be performed on success. + /// Create a new widget identified by @`uuid` on the given @`window`. + /// Position and size of the widget are undetermined at start and a call to @`place_widget` should be performed on success. syscall create_widget { in window: Window; in uuid: *const UUID; @@ -1308,7 +1308,7 @@ namespace gui { enum WidgetControlID : u32 { ... } - /// Triggers the `control` event of the widget with the given `message` as a payload. + /// Triggers the @`WidgetEvent.Type.control` event of the widget with the given @`message` as a payload. syscall control_widget { in widget: Widget; in message: WidgetControlMessage; @@ -1318,8 +1318,8 @@ namespace gui { enum WidgetNotifyID : u32 { ... } - /// Puts a `widget_notify` event into the event queue of the `Window` that owns `widget`. - /// The parameters are passed as a `WidgetNotifyEvent` to the event queue. + /// Puts a @`WindowEvent.Type.widget_notify` event into the event queue of the @`Window` that owns @`widget`. + /// The parameters are passed as a @`WidgetNotifyEvent` to the event queue. syscall notify_owner { in widget: Widget; in type: WidgetNotifyID; @@ -1394,11 +1394,11 @@ namespace gui { /// /// NOTE: This function is meant to be implemented by a desktop server. /// Regular GUI applications should not use this function as they have no - /// access to a `MessageBoxEvent.RequestID`. + /// access to a @`MessageBoxEvent.RequestID`. syscall notify_message_box { /// The desktop that completed the message box. in source: Desktop; - /// The request id that was passed in `MessageBoxEvent`. + /// The request id that was passed in @`MessageBoxEvent`. in request_id: MessageBoxEvent.RequestID; /// The resulting button which the user clicked. in result: MessageBoxResult; @@ -1415,7 +1415,7 @@ namespace gui { error InvalidHandle; } - /// Sends a notification to the provided `desktop`. + /// Sends a notification to the provided @`desktop`. syscall send_notification { /// Where to show the notification? in desktop: Desktop; @@ -1447,7 +1447,7 @@ namespace gui { /// Returns the current clipboard value as the provided mime type. /// The os provides a conversion *if possible*, otherwise returns an error. - /// The returned memory for `value` is owned by the process and must be freed with `ashet.process.memory.release`. + /// The returned memory for @`value` is owned by the process and must be freed with @`process.memory.release`. syscall get_value { in desktop: Desktop; in mime: str; @@ -1461,7 +1461,7 @@ namespace gui { } namespace service { - /// Registers a new service `uuid` in the system. + /// Registers a new service @`uuid` in the system. /// Takes an array of function pointers that will be provided for IPC and a service name to be advertised. syscall create { in uuid: *const UUID; @@ -1653,7 +1653,7 @@ struct Await_Options { /// or /// b) the result array is full /// - /// NOTE: If `thread_affinity` is `.all_threads`, other threads can still + /// NOTE: If @`thread_affinity` is `.all_threads`, other threads can still /// schedule more operations and make this function block longer. item wait_all = 2; @@ -2614,7 +2614,7 @@ struct WidgetDescriptor { field uuid: UUID; /// Number of bytes allocated in a Widget for this widget type. - /// See @ref gui.get_widget_data function for further information. + /// See @`gui.get_widget_data` function for further information. field data_size: usize; field flags: Flags; @@ -2705,7 +2705,7 @@ typedef DesktopEventHandler = fnptr (Desktop, *const DesktopEvent) void; struct DesktopDescriptor { /// Number of bytes allocated in a Window for this desktop. - /// See @ref gui.get_desktop_data function for further information. + /// See @`gui.get_desktop_data` function for further information. field window_data_size: usize; /// A function pointer to the event handler of a desktop. @@ -2737,11 +2737,11 @@ union DesktopEvent { //? user interaction: - /// `send_notification` was called and the desktop user should display + /// @`send_notification` was called and the desktop user should display /// a notification. item show_notification = 3; - /// `send_notification` was called and the desktop user should display + /// @`send_notification` was called and the desktop user should display /// a notification. item show_message_box = 4; @@ -2774,7 +2774,7 @@ struct MessageBoxEvent { field event_type: DesktopEvent.Type; /// The desktop-specific request id that must be passed into - /// `notify_message_box` to finish the message box request. + /// @`notify_message_box` to finish the message box request. field request_id: RequestID; /// Content of the message box. @@ -2808,11 +2808,11 @@ struct MessageBoxEvent { /// /// To address these two problems, the color scheme uses a modified mapping: /// -/// - `hue` is used without special interpretation. -/// - `value` maps to a range of `[1:8]` instead of `[0:7]`, allowing 8 different +/// - @`hue` is used without special interpretation. +/// - @`value` maps to a range of `[1:8]` instead of `[0:7]`, allowing 8 different /// values that are all not black. -/// - `saturation` is used without special interpretation except for zero: -/// If the `saturation` field is zero, `hue` and `value` are interpreted together as a 6 bit +/// - @`saturation` is used without special interpretation except for zero: +/// If the @`saturation` field is zero, @`hue` and @`value` are interpreted together as a 6 bit /// integer storing the brightness of gray. /// /// This yields a color space which has the following properties: @@ -2925,7 +2925,7 @@ union WidgetEvent { /// same widget. The hovered widget *may* change in between the mouse down and mouse up, /// but the click will still be recognized. /// NOTE: A click with the keyboard is valid when, and only when: - /// `key_press` and `key_release` happen without changing the focused widget, and only when + /// @`key_press` and @`key_release` happen without changing the focused widget, and only when /// the focus giving key (space, return, ...) was pressed without any other key interrupting. item click = 3; @@ -2941,43 +2941,43 @@ union WidgetEvent { /// The mouse was moved inside the rectangle of the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`hit_test_visible` was set /// in the widget creation flags. item mouse_enter = 6; /// The mouse was moved outside the rectangle of the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`hit_test_visible` was set /// in the widget creation flags. item mouse_leave = 7; /// The mouse stopped for some time over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`hit_test_visible` was set /// in the widget creation flags. item mouse_hover = 8; /// A mouse button was pressed over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`hit_test_visible` was set /// in the widget creation flags. item mouse_button_press = 9; /// A mouse button was released over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`hit_test_visible` was set /// in the widget creation flags. item mouse_button_release = 10; /// The mouse was moved over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`hit_test_visible` was set /// in the widget creation flags. item mouse_motion = 11; /// A vertical or horizontal scroll wheel was scrolled over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`hit_test_visible` was set /// in the widget creation flags. item scroll = 12; @@ -2985,25 +2985,25 @@ union WidgetEvent { /// The user dragged a payload into the rectangle of this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`allow_drop` was set in the /// widget creation flags. item drag_enter = 13; /// The user dragged a payload out of the rectangle of this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`allow_drop` was set in the /// widget type creation flags. item drag_leave = 14; /// The user dragged a payload over the rectangle of this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`allow_drop` was set in the /// widget type creation flags. item drag_over = 15; /// The user dropped a payload into this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`allow_drop` was set in the /// widget type creation flags. item drag_drop = 16; @@ -3011,26 +3011,26 @@ union WidgetEvent { /// The user requested a clipboard copy operation, usually by pressing 'Ctrl-C'. /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_copy = 17; /// The user requested a clipboard paste operation, usually by pressing 'Ctrl-V'. /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_paste = 18; /// The user requested a clipboard cut operation, usually by pressing 'Ctrl-X'. /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_cut = 19; //? widget specific: //? TODO: Implement ResizedArgs with "desired size, actual size" - /// The widget was resized with a call to `place_widget`. + /// The widget was resized with a call to @`place_widget`. /// /// NOTE: This event will not fire if the widget was only moved. item resized = 21; @@ -3081,15 +3081,19 @@ union WindowEvent { item window_restore = 10; /// The window is currently moving on the screen. Query `window.bounds` to get the new position. + /// + /// DEPRECATED: This event must be removed! item window_moving = 11; /// The window was moved on the screen. Query `window.bounds` to get the new position. + /// + /// DEPRECATED: This event must be removed! item window_moved = 12; - /// The window size is currently changing. Query `window.bounds` to get the new size. + /// The window size is currently changing. Query @`gui.get_window_size` to get the new size. item window_resizing = 13; - /// The window size changed. Query `window.bounds` to get the new size. + /// The window size changed. Query @`gui.get_window_size` to get the new size. item window_resized = 14; } } @@ -3185,10 +3189,10 @@ struct Rectangle { struct VideoMemory { /// Pointer to the first pixel of the first scanline. /// - /// Each scanline is `.stride` elements separated from - /// each other and contains `width` valid elements. + /// Each scanline is @`stride` elements separated from + /// each other and contains @`width` valid elements. /// - /// There are `height` total scanlines available. + /// There are @`height` total scanlines available. field base: [*]align(4) Color; /// Length of a scanline. @@ -3318,7 +3322,7 @@ namespace io { error InvalidHandle; - /// The actual baud rate diverges more than `acceptable_baud_error` from the requested baud rate. + /// The actual baud rate diverges more than @`acceptable_baud_error` from the requested baud rate. error ImpreciseBaudRate; /// The requested word size is not supported by this serial port. @@ -3589,7 +3593,7 @@ namespace io { /// As both a singular read and a singular write can be expressed as a batch of one operation, /// this is the only available function on the I²C bus. /// - /// **Example:** + /// *Example:* /// Typical I²C EEPROMs have an internally maintained "memory cursor" which is advanced for every /// read operation. All write operations will update the cursor also for read operations. /// @@ -3605,10 +3609,10 @@ namespace io { in bus: Bus; /// A mutable sequence of I²C operations. Will be processed first-to-last and - /// the pointed `Operation`s will be changed during their execution. + /// the pointed @`Operation`s will be changed during their execution. in sequence: []Operation; - /// The number of successfully processed elements from `sequence`. + /// The number of successfully processed elements from @`sequence`. /// /// On success, the call returns exactly the length of the sequence, otherwise /// it returns the index of the element that failed. @@ -3616,10 +3620,10 @@ namespace io { error InvalidHandle; - /// An operation in the `sequence` contained an invalid address. + /// An operation in the @`sequence` contained an invalid address. error InvalidAddress; - /// An operation that isn't `ping` was trying to process zero bytes. + /// An operation that isn't @`Operation.Type.ping` was trying to process zero bytes. error EmptyOperation; /// An error happened during processing. Read `sequence[].error` to see which operations failed. @@ -3637,12 +3641,12 @@ namespace io { /// The data which should either be written or read. /// - /// NOTE: If the operation is `Type.read`, the buffer will + /// NOTE: If the operation is @`Type.read`, the buffer will /// be overwritten by the OS. All other operations treat this /// buffer as immutable. field data: bytebuf; - /// The number of processed bytes inside `data`. + /// The number of processed bytes inside @`data`. /// /// NOTE: This field can be left uninitialized and will be overwritten by the OS /// with the result of the operation. @@ -3650,11 +3654,11 @@ namespace io { field processed: usize = 0; /// The error that happened when processing this batch item. On success, this will - /// be set to `Error.none`. + /// be set to @`Error.none`. /// /// NOTE: This field can be left uninitialized and will be overwritten by the OS /// with the result of the operation. - /// If the batch item wasn't scheduled, the resulting value will be `Error.aborted`. + /// If the batch item wasn't scheduled, the resulting value will be @`Error.aborted`. field @"error": Error; enum Type : u8 { @@ -3680,7 +3684,7 @@ namespace io { /// While writing the data, the device returned a NAK. /// - /// NOTE: The ACK/NAK during the addressing phase is handled by `device_not_found`. + /// NOTE: The ACK/NAK during the addressing phase is handled by @`device_not_found`. item no_acknowledge = 2; /// A previous batch item errored and this item wasn't executed at all. @@ -3697,4 +3701,4 @@ namespace io { } } } -} \ No newline at end of file +} diff --git a/src/tools/abi-mapper/rework/abi-doc-format.md b/src/tools/abi-mapper/rework/abi-doc-format.md index 2a552db1..3400bf1d 100644 --- a/src/tools/abi-mapper/rework/abi-doc-format.md +++ b/src/tools/abi-mapper/rework/abi-doc-format.md @@ -85,7 +85,8 @@ A parsed doc comment produces a **DocComment** value. The model is specified her "lore", "example", "deprecated", - "decision" + "decision", + "learn", ] }, "blocks": { @@ -251,7 +252,7 @@ DocComment Section ├─ kind: "main" | "note" | "warning" | "lore" | "example" - │ | "deprecated" | "decision" + │ | "deprecated" | "decision" | "learn" └─ blocks: Block[] (at least 1) Block = Paragraph | UnorderedList | OrderedList | CodeBlock @@ -337,6 +338,7 @@ Recognized tags (case-sensitive): | `EXAMPLE` | `example` | | `DEPRECATED` | `deprecated` | | `DECISION` | `decision` | +| `LEARN` | `learn` | A new admonition tag or a blank line followed by different content ends the current section and starts a new one. @@ -955,7 +957,7 @@ section = [ admonition_start ] , block , { blank_line , block } ; admonition_start = tag , ":" , ws , text_line ; tag = "NOTE" | "WARNING" | "LORE" | "EXAMPLE" - | "DEPRECATED" | "DECISION" ; + | "DEPRECATED" | "DECISION" | "LEARN"; block = code_block | unordered_list | ordered_list | paragraph ; diff --git a/src/tools/abi-mapper/src/doc_comment.zig b/src/tools/abi-mapper/src/doc_comment.zig index eb56b20e..9b8bdf88 100644 --- a/src/tools/abi-mapper/src/doc_comment.zig +++ b/src/tools/abi-mapper/src/doc_comment.zig @@ -387,6 +387,7 @@ fn parse_admonition(line: []const u8) ?AdmonitionResult { .{ .tag = "EXAMPLE", .kind = .example }, .{ .tag = "DEPRECATED", .kind = .deprecated }, .{ .tag = "DECISION", .kind = .decision }, + .{ .tag = "LEARN", .kind = .learn }, }; for (tags) |entry| { diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index 525348f2..23ea898f 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -47,6 +47,7 @@ pub const DocComment = struct { example, deprecated, decision, + learn, }; }; diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index a8d5b2be..8851bb2e 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -457,7 +457,7 @@ const PageRenderer = struct { try html.writer.print("

{s}

\n", .{title}); - try html.writer.writeAll("
    \n"); + try html.writer.writeAll("
      \n"); for (decl.children) |child| { if (!contains_tag(child.data, tags)) continue; @@ -803,7 +803,11 @@ fn format_inlines(inlines: []const model.DocComment.Inline, writer: *std.Io.Writ try format_inlines(emphasis.content, writer); try writer.writeAll(""); }, - .ref => |ref| try writer.print("[[{s}]]", .{ref.fqn}), + .ref => |ref| { + try writer.print("", .{fmt_attr("???")}); + try writer.print("{s}", .{ref.fqn}); + try writer.writeAll(""); + }, .link => |link| { try writer.print("", .{fmt_attr(link.url)}); try format_inlines(link.content, writer); diff --git a/src/website/www/theme.css b/src/website/www/theme.css index b41c1b12..703554f0 100644 --- a/src/website/www/theme.css +++ b/src/website/www/theme.css @@ -225,7 +225,7 @@ div.flex-spacer { margin-bottom: 0.5em; } -.docs-container ul { +.docs-container ul.basic-list { column-width: 15em; margin-left: 2em; } @@ -289,11 +289,19 @@ div.flex-spacer { font-weight: bold; } +.docs-container .doc-section-learn::before { + content: 'LEARN:'; + font-weight: bold; +} .docs-container .doc-section-main>:is(p,li,ul,pre) { margin-left: 0; } +.docs-container .doc-section :is(ul,ol) { + padding-left: 2em; +} + /************************************************* * Syntax Highlighting * *************************************************/ From 368063896c1814171f4ea64d46a32db776d08c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 22:51:23 +0100 Subject: [PATCH 10/36] Codex: Implements FQN resolution of doc comment refs --- src/tools/abi-mapper/src/doc_comment.zig | 31 +++++++++-- src/tools/abi-mapper/src/model.zig | 4 +- src/tools/abi-mapper/src/sema.zig | 65 +++++++++++++++++++++-- src/tools/abi-mapper/tests/doc_parser.zig | 50 +++++++++-------- 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/tools/abi-mapper/src/doc_comment.zig b/src/tools/abi-mapper/src/doc_comment.zig index 9b8bdf88..546a39af 100644 --- a/src/tools/abi-mapper/src/doc_comment.zig +++ b/src/tools/abi-mapper/src/doc_comment.zig @@ -3,6 +3,17 @@ const model = @import("model.zig"); const DocComment = model.DocComment; +pub const RefLookupFn = *const fn ( + context: ?*anyopaque, + allocator: std.mem.Allocator, + local_qn: []const u8, +) error{OutOfMemory}!?[]const u8; + +pub const ParseOptions = struct { + ref_lookup: ?RefLookupFn = null, + ref_lookup_context: ?*anyopaque = null, +}; + /// A parsed doc comment together with the arena that owns its memory. /// Call deinit() when the DocComment is no longer needed. pub const ParsedDocComment = struct { @@ -18,13 +29,13 @@ pub const ParsedDocComment = struct { /// The caller owns the result and must call deinit() to release memory. /// /// Each raw line should be `token.text[3..]` where token.text starts with `///`. -pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8) !ParsedDocComment { +pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8, options: ParseOptions) !ParsedDocComment { var result: ParsedDocComment = .{ .arena = std.heap.ArenaAllocator.init(backing_allocator), .comment = undefined, }; errdefer result.arena.deinit(); - result.comment = try parse_into_arena(&result.arena, raw_lines); + result.comment = try parse_into_arena(&result.arena, raw_lines, options); return result; } @@ -32,10 +43,14 @@ pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8 /// The caller owns the arena and is responsible for its lifetime. /// /// Each raw line should be `token.text[3..]` where token.text starts with `///`. -pub fn parse_into_arena(arena: *std.heap.ArenaAllocator, raw_lines: []const []const u8) !DocComment { +pub fn parse_into_arena(arena: *std.heap.ArenaAllocator, raw_lines: []const []const u8, options: ParseOptions) !DocComment { if (raw_lines.len == 0) return .empty; - var ctx: ParseContext = .{ .allocator = arena.allocator() }; + var ctx: ParseContext = .{ + .allocator = arena.allocator(), + .ref_lookup = options.ref_lookup, + .ref_lookup_context = options.ref_lookup_context, + }; return ctx.parse_doc(raw_lines); } @@ -43,6 +58,8 @@ const AccKind = enum { none, paragraph, unordered_list, ordered_list }; const ParseContext = struct { allocator: std.mem.Allocator, + ref_lookup: ?RefLookupFn, + ref_lookup_context: ?*anyopaque, fn parse_doc(ctx: *ParseContext, raw_lines: []const []const u8) !DocComment { // Normalize lines: strip one optional leading space (the /// separator), right-trim. @@ -251,7 +268,11 @@ const ParseContext = struct { } const ref_start = i + 2; if (std.mem.indexOfScalar(u8, text[ref_start..], '`')) |rel_end| { - const fqn = text[ref_start .. ref_start + rel_end]; + const local_qn = text[ref_start .. ref_start + rel_end]; + const fqn = if (ctx.ref_lookup) |lookup| + (try lookup(ctx.ref_lookup_context, ctx.allocator, local_qn)) orelse local_qn + else + local_qn; try result.append(ctx.allocator, .{ .ref = .{ .fqn = fqn } }); i = ref_start + rel_end + 1; text_start = i; diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index 23ea898f..5e6f09da 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -123,7 +123,9 @@ pub const DocComment = struct { pub const Text = struct { value: []const u8 }; pub const Code = struct { value: []const u8 }; pub const Emphasis = struct { content: []const Inline }; - pub const Ref = struct { fqn: []const u8 }; + pub const Ref = struct { + fqn: []const u8, + }; pub const Link = struct { url: []const u8, content: []const Inline }; pub fn jsonStringify(inl: Inline, jws: anytype) !void { diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index 502f7ab2..c4175d83 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -986,7 +986,66 @@ const Analyzer = struct { /// Parses a raw doc comment into a structured DocComment. fn map_doc_comment(ana: *Analyzer, raw_lines: []const []const u8) !model.DocComment { var arena = std.heap.ArenaAllocator.init(ana.allocator); - return doc_comment_parser.parse_into_arena(&arena, raw_lines); + return doc_comment_parser.parse_into_arena(&arena, raw_lines, .{ + .ref_lookup = lookup_doc_comment_ref, + .ref_lookup_context = @ptrCast(ana), + }); + } + + fn lookup_doc_comment_ref(context: ?*anyopaque, allocator: std.mem.Allocator, local_qn: []const u8) error{OutOfMemory}!?[]const u8 { + const raw = context orelse return null; + const ana: *Analyzer = @ptrCast(@alignCast(raw)); + return ana.resolve_doc_comment_ref(allocator, local_qn); + } + + fn resolve_doc_comment_ref(ana: *Analyzer, allocator: std.mem.Allocator, local_qn: []const u8) error{OutOfMemory}!?[]const u8 { + var local_parts: std.ArrayList([]const u8) = .empty; + defer local_parts.deinit(allocator); + + var iter = std.mem.splitScalar(u8, local_qn, '.'); + while (iter.next()) |part| { + if (part.len == 0) return null; + try local_parts.append(allocator, part); + } + + if (local_parts.items.len == 0) return null; + + const resolved_scope = ana.resolve_scope_path(ana.current_scope_name(), local_parts.items) orelse return null; + return @as(?[]const u8, try ana.scope_to_fqn_string(allocator, resolved_scope)); + } + + fn resolve_scope_path(ana: *Analyzer, declared_scope: []const []const u8, local_parts: []const []const u8) ?*Scope { + var search_scope: ?*Scope = ana.scope_map.get(declared_scope) orelse return null; + + while (search_scope) |base_scope| : (search_scope = base_scope.parent) { + var resolved_scope: ?*Scope = base_scope; + for (local_parts) |part| { + resolved_scope = if (resolved_scope) |scope| scope.children.get(part) else null; + if (resolved_scope == null) break; + } + + if (resolved_scope) |scope| { + return scope; + } + } + + return null; + } + + fn scope_to_fqn_string(ana: *Analyzer, allocator: std.mem.Allocator, scope: *const Scope) error{OutOfMemory}![]const u8 { + _ = ana; + + var segments: std.ArrayList([]const u8) = .empty; + defer segments.deinit(allocator); + + var current: ?*const Scope = scope; + while (current) |node| : (current = node.parent) { + if (node.parent == null) break; + try segments.append(allocator, node.name); + } + + std.mem.reverse([]const u8, segments.items); + return std.mem.join(allocator, ".", segments.items); } /// Creates a synthetic one-paragraph DocComment from a plain text string. @@ -1006,8 +1065,6 @@ const Analyzer = struct { const full_name, const scope = try ana.push_scope(decl.name, convert_enum(Scope.Type, decl.type)); defer ana.pop_scope(); - const doc_comment = try ana.map_doc_comment(node.doc_comment); - var children: std.ArrayList(model.Declaration) = .empty; defer children.deinit(ana.allocator); @@ -1023,6 +1080,8 @@ const Analyzer = struct { } } + const doc_comment = try ana.map_doc_comment(node.doc_comment); + const needs_subtype = switch (decl.type) { .@"enum", .bitstruct => true, .@"struct", .@"union", .async_call, .syscall, .namespace, .resource => false, diff --git a/src/tools/abi-mapper/tests/doc_parser.zig b/src/tools/abi-mapper/tests/doc_parser.zig index 5530ad33..52bb0cd5 100644 --- a/src/tools/abi-mapper/tests/doc_parser.zig +++ b/src/tools/abi-mapper/tests/doc_parser.zig @@ -3,6 +3,10 @@ const abi_parser = @import("abi-parser"); const doc_comment_parser = abi_parser.doc_comment; const DocComment = abi_parser.model.DocComment; +fn parse_doc(raw_lines: []const []const u8) !doc_comment_parser.ParsedDocComment { + return doc_comment_parser.parse(std.testing.allocator, raw_lines, .{}); +} + // from json test "empty doc comment from json" { @@ -13,14 +17,14 @@ test "empty doc comment from json" { // ── Empty / blank ──────────────────────────────────────────────────────────── test "empty input returns empty DocComment" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{}); + var parsed = try parse_doc(&.{}); defer parsed.deinit(); try std.testing.expectEqual(@as(usize, 0), parsed.comment.sections.len); } test "only blank lines returns empty DocComment" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ "", "", "" }); + var parsed = try parse_doc(&.{ "", "", "" }); defer parsed.deinit(); try std.testing.expectEqual(@as(usize, 0), parsed.comment.sections.len); @@ -29,7 +33,7 @@ test "only blank lines returns empty DocComment" { // ── Paragraphs ─────────────────────────────────────────────────────────────── test "simple paragraph" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Hello, world!"}); + var parsed = try parse_doc(&.{" Hello, world!"}); defer parsed.deinit(); try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections.len); @@ -44,7 +48,7 @@ test "simple paragraph" { } test "multi-line paragraph joined with space" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " First line", " second line", " third line", @@ -63,7 +67,7 @@ test "multi-line paragraph joined with space" { } test "blank line separates paragraphs" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " First paragraph.", "", " Second paragraph.", @@ -87,7 +91,7 @@ test "blank line separates paragraphs" { // ── Inline elements ────────────────────────────────────────────────────────── test "inline code span" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Call `foo()` now."}); + var parsed = try parse_doc(&.{" Call `foo()` now."}); defer parsed.deinit(); const content = parsed.comment.sections[0].blocks[0].paragraph.content; @@ -101,7 +105,7 @@ test "inline code span" { } test "cross-reference @`fqn`" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" See @`foo.bar.Baz` for details."}); + var parsed = try parse_doc(&.{" See @`foo.bar.Baz` for details."}); defer parsed.deinit(); const content = parsed.comment.sections[0].blocks[0].paragraph.content; @@ -115,7 +119,7 @@ test "cross-reference @`fqn`" { } test "emphasis *text*" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" This is *important* text."}); + var parsed = try parse_doc(&.{" This is *important* text."}); defer parsed.deinit(); const content = parsed.comment.sections[0].blocks[0].paragraph.content; @@ -130,7 +134,7 @@ test "emphasis *text*" { } test "escape sequences suppress special syntax" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Escape: \\`not code\\`."}); + var parsed = try parse_doc(&.{" Escape: \\`not code\\`."}); defer parsed.deinit(); const content = parsed.comment.sections[0].blocks[0].paragraph.content; @@ -147,7 +151,7 @@ test "escape sequences suppress special syntax" { } test "titled link [display](url)" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" See [the docs](https://example.com/docs)."}); + var parsed = try parse_doc(&.{" See [the docs](https://example.com/docs)."}); defer parsed.deinit(); const content = parsed.comment.sections[0].blocks[0].paragraph.content; @@ -163,7 +167,7 @@ test "titled link [display](url)" { } test "autolink " { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Visit ."}); + var parsed = try parse_doc(&.{" Visit ."}); defer parsed.deinit(); const content = parsed.comment.sections[0].blocks[0].paragraph.content; @@ -179,7 +183,7 @@ test "autolink " { } test "autolink " { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{" Mail us."}); + var parsed = try parse_doc(&.{" Mail us."}); defer parsed.deinit(); const content = parsed.comment.sections[0].blocks[0].paragraph.content; @@ -191,7 +195,7 @@ test "autolink " { // ── Admonitions ────────────────────────────────────────────────────────────── test "NOTE admonition starts new section" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " Main text.", "", " NOTE: This is a note.", @@ -221,7 +225,7 @@ test "all admonition kinds are recognized" { var buf: [64]u8 = undefined; const line = try std.fmt.bufPrint(&buf, " {s}: test text", .{c.tag}); - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{line}); + var parsed = try parse_doc(&.{line}); defer parsed.deinit(); try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections.len); @@ -230,7 +234,7 @@ test "all admonition kinds are recognized" { } test "admonition with empty body starts section" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " NOTE:", " Text on the next line.", }); @@ -246,7 +250,7 @@ test "admonition with empty body starts section" { } test "multiple admonition sections" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " Main description.", "", " NOTE: Important note.", @@ -264,7 +268,7 @@ test "multiple admonition sections" { // ── Lists ──────────────────────────────────────────────────────────────────── test "unordered list" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " - First item", " - Second item", " - Third item", @@ -282,7 +286,7 @@ test "unordered list" { } test "ordered list" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " 1. First", " 2. Second", " 3. Third", @@ -300,7 +304,7 @@ test "ordered list" { } test "list item continuation line" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " - First item", " continues here", " - Second item", @@ -319,7 +323,7 @@ test "list item continuation line" { } test "paragraph after list" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " - Item one", " - Item two", "", @@ -335,7 +339,7 @@ test "paragraph after list" { // ── Code fences ────────────────────────────────────────────────────────────── test "code fence without syntax hint" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " ```", " some code", " more code", @@ -352,7 +356,7 @@ test "code fence without syntax hint" { } test "code fence with syntax hint" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " ```zig", " const x = 42;", " ```", @@ -367,7 +371,7 @@ test "code fence with syntax hint" { } test "code fence preceded and followed by text" { - var parsed = try doc_comment_parser.parse(std.testing.allocator, &.{ + var parsed = try parse_doc(&.{ " Before.", "", " ```", From 4551176fa3cc319f095070b01bccb2a83f868018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 23:04:52 +0100 Subject: [PATCH 11/36] Codex: Implements abi-mapper errors for invalid code refs. --- src/tools/abi-mapper/src/sema.zig | 237 ++++++++++++++++++++++++++++-- 1 file changed, 224 insertions(+), 13 deletions(-) diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index c4175d83..2f98d14b 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -30,6 +30,7 @@ pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document, uid_data try analyzer.scope_map.put(&.{}, &analyzer.root_scope); try analyzer.map(document); + try analyzer.resolve_doc_comment_refs(); try analyzer.resolve_named_types(); @@ -995,37 +996,98 @@ const Analyzer = struct { fn lookup_doc_comment_ref(context: ?*anyopaque, allocator: std.mem.Allocator, local_qn: []const u8) error{OutOfMemory}!?[]const u8 { const raw = context orelse return null; const ana: *Analyzer = @ptrCast(@alignCast(raw)); - return ana.resolve_doc_comment_ref(allocator, local_qn); + return ana.resolve_doc_comment_ref_with_scope( + allocator, + ana.current_scope_name(), + local_qn, + false, + ); } - fn resolve_doc_comment_ref(ana: *Analyzer, allocator: std.mem.Allocator, local_qn: []const u8) error{OutOfMemory}!?[]const u8 { + fn resolve_doc_comment_ref_with_scope( + ana: *Analyzer, + allocator: std.mem.Allocator, + declared_scope: []const []const u8, + local_qn: []const u8, + report_errors: bool, + ) error{OutOfMemory}!?[]const u8 { var local_parts: std.ArrayList([]const u8) = .empty; defer local_parts.deinit(allocator); var iter = std.mem.splitScalar(u8, local_qn, '.'); while (iter.next()) |part| { - if (part.len == 0) return null; + if (part.len == 0) { + if (report_errors) { + try ana.emit_error(Location.empty, "invalid doc reference '@`{s}`' in scope '{f}'", .{ + local_qn, + dotJoin(declared_scope), + }); + } + return null; + } try local_parts.append(allocator, part); } - if (local_parts.items.len == 0) return null; + if (local_parts.items.len == 0) { + if (report_errors) { + try ana.emit_error(Location.empty, "invalid doc reference '@`{s}`' in scope '{f}'", .{ + local_qn, + dotJoin(declared_scope), + }); + } + return null; + } - const resolved_scope = ana.resolve_scope_path(ana.current_scope_name(), local_parts.items) orelse return null; - return @as(?[]const u8, try ana.scope_to_fqn_string(allocator, resolved_scope)); + const resolved = ana.resolve_scope_prefix(declared_scope, local_parts.items) orelse { + if (local_parts.items.len == 1) { + return null; + } + if (report_errors) { + try ana.emit_error(Location.empty, "unknown doc reference '@`{s}`' in scope '{f}'", .{ + local_qn, + dotJoin(declared_scope), + }); + } + return null; + }; + + const resolved_fqn = try ana.scope_to_fqn_string(allocator, resolved.scope); + if (resolved.matched_parts == local_parts.items.len) { + return @as(?[]const u8, resolved_fqn); + } + + var full: std.ArrayList(u8) = .empty; + defer full.deinit(allocator); + try full.appendSlice(allocator, resolved_fqn); + for (local_parts.items[resolved.matched_parts..]) |part| { + try full.append(allocator, '.'); + try full.appendSlice(allocator, part); + } + return @as(?[]const u8, try full.toOwnedSlice(allocator)); } - fn resolve_scope_path(ana: *Analyzer, declared_scope: []const []const u8, local_parts: []const []const u8) ?*Scope { + const ScopePrefixMatch = struct { + scope: *Scope, + matched_parts: usize, + }; + + fn resolve_scope_prefix(ana: *Analyzer, declared_scope: []const []const u8, local_parts: []const []const u8) ?ScopePrefixMatch { var search_scope: ?*Scope = ana.scope_map.get(declared_scope) orelse return null; while (search_scope) |base_scope| : (search_scope = base_scope.parent) { - var resolved_scope: ?*Scope = base_scope; - for (local_parts) |part| { - resolved_scope = if (resolved_scope) |scope| scope.children.get(part) else null; - if (resolved_scope == null) break; + var resolved_scope: *Scope = base_scope; + var matched_parts: usize = 0; + + while (matched_parts < local_parts.len) : (matched_parts += 1) { + const next_scope = resolved_scope.children.get(local_parts[matched_parts]) orelse break; + resolved_scope = next_scope; } - if (resolved_scope) |scope| { - return scope; + if (matched_parts > 0) { + return .{ + .scope = resolved_scope, + .matched_parts = matched_parts, + }; } } @@ -1048,6 +1110,155 @@ const Analyzer = struct { return std.mem.join(allocator, ".", segments.items); } + fn resolve_doc_comment_refs(ana: *Analyzer) !void { + try ana.resolve_namespace_doc_comment_refs(ana.root.items); + + for (ana.structs.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + for (@constCast(item.native_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + } + + for (ana.unions.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + for (@constCast(item.native_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + } + + for (ana.enums.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.items)) |*enum_item| { + try ana.resolve_doc_comment_in_scope(&enum_item.docs, item.full_qualified_name); + } + } + + for (ana.bitstructs.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + } + + for (ana.syscalls.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.logic_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.errors)) |*api_error| { + try ana.resolve_doc_comment_in_scope(&api_error.docs, item.full_qualified_name); + } + } + + for (ana.async_calls.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.logic_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.errors)) |*api_error| { + try ana.resolve_doc_comment_in_scope(&api_error.docs, item.full_qualified_name); + } + } + + for (ana.resources.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + } + + for (ana.constants.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + } + + for (ana.types.items) |*item| { + switch (item.*) { + .typedef => |*typedef| { + try ana.resolve_doc_comment_in_scope(&typedef.docs, typedef.full_qualified_name); + }, + else => {}, + } + } + } + + fn resolve_namespace_doc_comment_refs(ana: *Analyzer, declarations: []const model.Declaration) !void { + for (@constCast(declarations)) |*decl| { + if (decl.data == .namespace) { + try ana.resolve_doc_comment_in_scope(&decl.docs, decl.full_qualified_name); + } + try ana.resolve_namespace_doc_comment_refs(decl.children); + } + } + + fn resolve_doc_comment_in_scope(ana: *Analyzer, docs: *model.DocComment, declared_scope: []const []const u8) !void { + for (@constCast(docs.sections)) |*section| { + for (@constCast(section.blocks)) |*block| { + switch (block.*) { + .paragraph => |*paragraph| { + try ana.resolve_inline_refs(@constCast(paragraph.content), declared_scope); + }, + .unordered_list => |*list| { + for (@constCast(list.items)) |*item| { + try ana.resolve_inline_refs(@constCast(item.*), declared_scope); + } + }, + .ordered_list => |*list| { + for (@constCast(list.items)) |*item| { + try ana.resolve_inline_refs(@constCast(item.*), declared_scope); + } + }, + .code_block => {}, + } + } + } + } + + fn resolve_inline_refs(ana: *Analyzer, inlines: []model.DocComment.Inline, declared_scope: []const []const u8) !void { + for (inlines) |*inl| { + switch (inl.*) { + .ref => |*ref_data| { + if (try ana.resolve_doc_comment_ref_with_scope( + ana.allocator, + declared_scope, + ref_data.fqn, + true, + )) |resolved| { + ref_data.fqn = resolved; + } + }, + .emphasis => |*emphasis| { + try ana.resolve_inline_refs(@constCast(emphasis.content), declared_scope); + }, + .link => |*link| { + try ana.resolve_inline_refs(@constCast(link.content), declared_scope); + }, + else => {}, + } + } + } + /// Creates a synthetic one-paragraph DocComment from a plain text string. fn synthetic_doc(ana: *Analyzer, text: []const u8) !model.DocComment { const inlines = try ana.allocator.alloc(model.DocComment.Inline, 1); From d69edf6e931729e9c1e9270af385e085c7fcdd86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 23:27:21 +0100 Subject: [PATCH 12/36] Codex: Implements improved validation of bad references, adds proper resolution of FQNs --- src/tools/abi-mapper/build.zig | 11 ++ src/tools/abi-mapper/src/abi-parser.zig | 2 +- src/tools/abi-mapper/src/sema.zig | 130 +++++++++++++++--- .../abi-mapper/tests/doc_ref_resolution.abi | 27 ++++ .../abi-mapper/tests/doc_ref_resolution.zig | 125 +++++++++++++++++ 5 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 src/tools/abi-mapper/tests/doc_ref_resolution.abi create mode 100644 src/tools/abi-mapper/tests/doc_ref_resolution.zig diff --git a/src/tools/abi-mapper/build.zig b/src/tools/abi-mapper/build.zig index 82e62df8..a16875db 100644 --- a/src/tools/abi-mapper/build.zig +++ b/src/tools/abi-mapper/build.zig @@ -46,6 +46,17 @@ pub fn build(b: *std.Build) void { }); const doc_parser_tests = b.addTest(.{ .root_module = doc_parser_mod }); test_step.dependOn(&b.addRunArtifact(doc_parser_tests).step); + + const doc_ref_resolution_mod = b.createModule(.{ + .root_source_file = b.path("tests/doc_ref_resolution.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "abi-parser", .module = abi_parser_mod }, + }, + }); + const doc_ref_resolution_tests = b.addTest(.{ .root_module = doc_ref_resolution_mod }); + test_step.dependOn(&b.addRunArtifact(doc_ref_resolution_tests).step); } pub const Converter = struct { diff --git a/src/tools/abi-mapper/src/abi-parser.zig b/src/tools/abi-mapper/src/abi-parser.zig index e873169a..e0f0d3c8 100644 --- a/src/tools/abi-mapper/src/abi-parser.zig +++ b/src/tools/abi-mapper/src/abi-parser.zig @@ -24,7 +24,7 @@ pub fn main() !u8 { return 1; } - if (args.options.output.len == 1) { + if (args.options.output.len == 0) { return 1; } diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index 2f98d14b..c1231472 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -1038,32 +1038,39 @@ const Analyzer = struct { return null; } - const resolved = ana.resolve_scope_prefix(declared_scope, local_parts.items) orelse { - if (local_parts.items.len == 1) { - return null; + if (ana.resolve_scope_prefix(declared_scope, local_parts.items)) |resolved| { + const resolved_fqn = try ana.scope_to_fqn_string(allocator, resolved.scope); + if (resolved.matched_parts == local_parts.items.len) { + return @as(?[]const u8, resolved_fqn); } - if (report_errors) { - try ana.emit_error(Location.empty, "unknown doc reference '@`{s}`' in scope '{f}'", .{ - local_qn, - dotJoin(declared_scope), - }); + + var full: std.ArrayList(u8) = .empty; + defer full.deinit(allocator); + try full.appendSlice(allocator, resolved_fqn); + for (local_parts.items[resolved.matched_parts..]) |part| { + try full.append(allocator, '.'); + try full.appendSlice(allocator, part); } - return null; - }; + return @as(?[]const u8, try full.toOwnedSlice(allocator)); + } - const resolved_fqn = try ana.scope_to_fqn_string(allocator, resolved.scope); - if (resolved.matched_parts == local_parts.items.len) { - return @as(?[]const u8, resolved_fqn); + if (local_parts.items.len == 1) { + if (try ana.resolve_contained_doc_reference( + allocator, + declared_scope, + local_parts.items[0], + )) |resolved| { + return resolved; + } } - var full: std.ArrayList(u8) = .empty; - defer full.deinit(allocator); - try full.appendSlice(allocator, resolved_fqn); - for (local_parts.items[resolved.matched_parts..]) |part| { - try full.append(allocator, '.'); - try full.appendSlice(allocator, part); + if (report_errors) { + try ana.emit_error(Location.empty, "unknown doc reference '@`{s}`' in scope '{f}'", .{ + local_qn, + dotJoin(declared_scope), + }); } - return @as(?[]const u8, try full.toOwnedSlice(allocator)); + return null; } const ScopePrefixMatch = struct { @@ -1094,6 +1101,66 @@ const Analyzer = struct { return null; } + fn resolve_contained_doc_reference( + ana: *Analyzer, + allocator: std.mem.Allocator, + declared_scope: []const []const u8, + local_name: []const u8, + ) error{OutOfMemory}!?[]const u8 { + const scope = ana.scope_map.get(declared_scope) orelse return null; + const link = scope.link orelse return null; + if (!ana.link_contains_doc_reference_target(link, local_name)) { + return null; + } + + var full: std.ArrayList(u8) = .empty; + defer full.deinit(allocator); + for (declared_scope, 0..) |part, i| { + if (i > 0) try full.append(allocator, '.'); + try full.appendSlice(allocator, part); + } + if (declared_scope.len > 0) { + try full.append(allocator, '.'); + } + try full.appendSlice(allocator, local_name); + return @as(?[]const u8, try full.toOwnedSlice(allocator)); + } + + fn link_contains_doc_reference_target(ana: *Analyzer, link: Scope.Link, local_name: []const u8) bool { + return switch (link) { + .namespace, + .resource, + .constant, + .typedef, + => false, + + .@"struct" => |idx| blk: { + const item = ana.structs.get(idx); + break :blk find_field_by_name(item.logic_fields, local_name) != null or + find_field_by_name(item.native_fields, local_name) != null; + }, + .@"union" => |idx| blk: { + const item = ana.unions.get(idx); + break :blk find_field_by_name(item.logic_fields, local_name) != null or + find_field_by_name(item.native_fields, local_name) != null; + }, + .@"enum" => |idx| has_enum_item_by_name(ana.enums.get(idx).items, local_name), + .bitstruct => |idx| has_bitstruct_field_by_name(ana.bitstructs.get(idx).fields, local_name), + .syscall => |idx| blk: { + const call = ana.syscalls.get(idx); + break :blk find_param_by_name(call.logic_inputs, local_name) != null or + find_param_by_name(call.logic_outputs, local_name) != null or + has_error_by_name(call.errors, local_name); + }, + .async_call => |idx| blk: { + const call = ana.async_calls.get(idx); + break :blk find_param_by_name(call.logic_inputs, local_name) != null or + find_param_by_name(call.logic_outputs, local_name) != null or + has_error_by_name(call.errors, local_name); + }, + }; + } + fn scope_to_fqn_string(ana: *Analyzer, allocator: std.mem.Allocator, scope: *const Scope) error{OutOfMemory}![]const u8 { _ = ana; @@ -1951,6 +2018,29 @@ const Analyzer = struct { return null; } + fn has_error_by_name(errs: []const model.Error, name: []const u8) bool { + for (errs) |err_item| { + if (std.mem.eql(u8, err_item.name, name)) return true; + } + return false; + } + + fn has_enum_item_by_name(items: []const model.EnumItem, name: []const u8) bool { + for (items) |item| { + if (std.mem.eql(u8, item.name, name)) return true; + } + return false; + } + + fn has_bitstruct_field_by_name(fields: []const model.BitStructField, name: []const u8) bool { + for (fields) |field| { + if (field.name) |field_name| { + if (std.mem.eql(u8, field_name, name)) return true; + } + } + return false; + } + fn find_field_by_name(fields: []const model.StructField, name: []const u8) ?model.StructField { for (fields) |f| { if (std.mem.eql(u8, f.name, name)) return f; diff --git a/src/tools/abi-mapper/tests/doc_ref_resolution.abi b/src/tools/abi-mapper/tests/doc_ref_resolution.abi new file mode 100644 index 00000000..9980e7bc --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_ref_resolution.abi @@ -0,0 +1,27 @@ +namespace overlapped { + /// Handle to an asynchronously running operation. + struct ARC { + field tag: usize; + } + + /// Awaits one or more scheduled asynchronous operations and returns the + /// number of @`completed` elements. + /// + /// The kernel will fill out @`completed` up to @`completed_count` elements. + /// + /// RELATES: @`await_completion_of` + syscall await_completion { + in completed: []*ARC; + out completed_count: usize; + error Unscheduled; + } + + /// Awaits one or more explicit asynchronous operations. + /// + /// NOTE: Use @`await_completion` when ordering of @`events` is irrelevant. + syscall await_completion_of { + in events: []?*ARC; + out completed_count: usize; + error Unscheduled; + } +} diff --git a/src/tools/abi-mapper/tests/doc_ref_resolution.zig b/src/tools/abi-mapper/tests/doc_ref_resolution.zig new file mode 100644 index 00000000..419e7634 --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_ref_resolution.zig @@ -0,0 +1,125 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); +const model = abi_parser.model; + +test "doc references resolve to contained syscall elements" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const abi_source = try std.fs.cwd().readFileAlloc( + allocator, + "tests/doc_ref_resolution.abi", + 1 << 20, + ); + + var tokenizer: abi_parser.syntax.Tokenizer = .init(abi_source, "tests/doc_ref_resolution.abi"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast_document = try parser.accept_document(); + const analyzed_document = try abi_parser.sema.analyze(allocator, ast_document, null); + + const await_completion = find_syscall_by_fqn( + analyzed_document.syscalls, + "overlapped.await_completion", + ) orelse return error.TestUnexpectedResult; + + try std.testing.expect(has_ref_fqn( + await_completion.docs, + "overlapped.await_completion.completed", + )); + try std.testing.expect(has_ref_fqn( + await_completion.docs, + "overlapped.await_completion.completed_count", + )); + try std.testing.expect(has_ref_fqn( + await_completion.docs, + "overlapped.await_completion_of", + )); + try std.testing.expect(!has_ref_fqn(await_completion.docs, "completed")); + + const await_completion_of = find_syscall_by_fqn( + analyzed_document.syscalls, + "overlapped.await_completion_of", + ) orelse return error.TestUnexpectedResult; + + try std.testing.expect(has_ref_fqn( + await_completion_of.docs, + "overlapped.await_completion", + )); + try std.testing.expect(has_ref_fqn( + await_completion_of.docs, + "overlapped.await_completion_of.events", + )); +} + +fn find_syscall_by_fqn( + syscalls: []const model.GenericCall, + expected: []const u8, +) ?model.GenericCall { + for (syscalls) |syscall| { + if (fqn_equals(syscall.full_qualified_name, expected)) { + return syscall; + } + } + return null; +} + +fn fqn_equals(fqn: model.FQN, expected: []const u8) bool { + var parts = std.mem.splitScalar(u8, expected, '.'); + var index: usize = 0; + while (parts.next()) |part| { + if (part.len == 0 or index >= fqn.len) { + return false; + } + if (!std.mem.eql(u8, fqn[index], part)) { + return false; + } + index += 1; + } + return index == fqn.len; +} + +fn has_ref_fqn(docs: model.DocComment, expected: []const u8) bool { + for (docs.sections) |section| { + for (section.blocks) |block| { + switch (block) { + .paragraph => |paragraph| { + if (inlines_have_ref_fqn(paragraph.content, expected)) return true; + }, + .unordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .ordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .code_block => {}, + } + } + } + return false; +} + +fn inlines_have_ref_fqn(inlines: []const model.DocComment.Inline, expected: []const u8) bool { + for (inlines) |inl| { + switch (inl) { + .ref => |r| { + if (std.mem.eql(u8, r.fqn, expected)) return true; + }, + .emphasis => |e| { + if (inlines_have_ref_fqn(e.content, expected)) return true; + }, + .link => |l| { + if (inlines_have_ref_fqn(l.content, expected)) return true; + }, + .text, .code => {}, + } + } + return false; +} From ec4222c5e526b9bc743b42666d6161d7a6a8b20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 23:33:13 +0100 Subject: [PATCH 13/36] Fixes all bad references in ashet.abi that are actually bad. --- src/abi/src/ashet.abi | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index 8007cd16..12f1d50b 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -226,7 +226,7 @@ namespace process { } /// Defines the signature of a thread entry point. - /// The parameter is the @`arg` value passed to @`spawn`. + /// The parameter is the @`spawn.arg` value passed to @`spawn`. /// The return value is the exit code of the thread. typedef ThreadFunction = fnptr (?anyptr) u32; @@ -978,7 +978,7 @@ namespace pipe { in data: bytestr; /// Distance between each element in @`data`. Can be different from the pipes element size /// to allow sparse data to be transferred. - /// If `0`, it will use the @`object_size` property of the pipe. + /// If `0`, it will use the @`create.object_size` property of the pipe. in stride: usize; /// Defines how the write should operate. in mode: PipeMode; @@ -993,7 +993,7 @@ namespace pipe { in buffer: bytebuf; /// Distance between each element in @`buffer`. Can be different from the pipes element size /// to allow sparse data to be transferred. - /// If `0`, it will use the @`object_size` property of the pipe. + /// If `0`, it will use the @`create.object_size` property of the pipe. in stride: usize; /// Defines how the read should operate. in mode: PipeMode; @@ -1653,7 +1653,7 @@ struct Await_Options { /// or /// b) the result array is full /// - /// NOTE: If @`thread_affinity` is `.all_threads`, other threads can still + /// NOTE: If @`thread_affinity` is @`Thread_Affinity.all_threads`, other threads can still /// schedule more operations and make this function block longer. item wait_all = 2; @@ -2737,11 +2737,11 @@ union DesktopEvent { //? user interaction: - /// @`send_notification` was called and the desktop user should display + /// @`gui.send_notification` was called and the desktop user should display /// a notification. item show_notification = 3; - /// @`send_notification` was called and the desktop user should display + /// @`gui.ShowMessageBox` was called and the desktop user should display /// a notification. item show_message_box = 4; @@ -2774,7 +2774,7 @@ struct MessageBoxEvent { field event_type: DesktopEvent.Type; /// The desktop-specific request id that must be passed into - /// @`notify_message_box` to finish the message box request. + /// @`gui.notify_message_box` to finish the message box request. field request_id: RequestID; /// Content of the message box. @@ -2941,43 +2941,43 @@ union WidgetEvent { /// The mouse was moved inside the rectangle of the widget. /// - /// NOTE: This event can only happen when @`hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_enter = 6; /// The mouse was moved outside the rectangle of the widget. /// - /// NOTE: This event can only happen when @`hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_leave = 7; /// The mouse stopped for some time over the widget. /// - /// NOTE: This event can only happen when @`hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_hover = 8; /// A mouse button was pressed over the widget. /// - /// NOTE: This event can only happen when @`hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_button_press = 9; /// A mouse button was released over the widget. /// - /// NOTE: This event can only happen when @`hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_button_release = 10; /// The mouse was moved over the widget. /// - /// NOTE: This event can only happen when @`hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_motion = 11; /// A vertical or horizontal scroll wheel was scrolled over the widget. /// - /// NOTE: This event can only happen when @`hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item scroll = 12; @@ -2985,25 +2985,25 @@ union WidgetEvent { /// The user dragged a payload into the rectangle of this widget. /// - /// NOTE: This event can only happen when @`allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget creation flags. item drag_enter = 13; /// The user dragged a payload out of the rectangle of this widget. /// - /// NOTE: This event can only happen when @`allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget type creation flags. item drag_leave = 14; /// The user dragged a payload over the rectangle of this widget. /// - /// NOTE: This event can only happen when @`allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget type creation flags. item drag_over = 15; /// The user dropped a payload into this widget. /// - /// NOTE: This event can only happen when @`allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget type creation flags. item drag_drop = 16; @@ -3011,26 +3011,26 @@ union WidgetEvent { /// The user requested a clipboard copy operation, usually by pressing 'Ctrl-C'. /// - /// NOTE: This event can only happen when @`clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_copy = 17; /// The user requested a clipboard paste operation, usually by pressing 'Ctrl-V'. /// - /// NOTE: This event can only happen when @`clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_paste = 18; /// The user requested a clipboard cut operation, usually by pressing 'Ctrl-X'. /// - /// NOTE: This event can only happen when @`clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_cut = 19; //? widget specific: //? TODO: Implement ResizedArgs with "desired size, actual size" - /// The widget was resized with a call to @`place_widget`. + /// The widget was resized with a call to @`gui.place_widget`. /// /// NOTE: This event will not fire if the widget was only moved. item resized = 21; From 288740d0f9497855f2394297d4717df038d77e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 3 Mar 2026 23:37:59 +0100 Subject: [PATCH 14/36] Codex: Fixes bug that field references could not be found --- src/tools/abi-mapper/src/sema.zig | 69 ++++++++++++------- .../abi-mapper/tests/doc_ref_resolution.abi | 17 +++++ .../abi-mapper/tests/doc_ref_resolution.zig | 40 +++++++++++ 3 files changed, 103 insertions(+), 23 deletions(-) diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index c1231472..f05f4ab0 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -1054,14 +1054,12 @@ const Analyzer = struct { return @as(?[]const u8, try full.toOwnedSlice(allocator)); } - if (local_parts.items.len == 1) { - if (try ana.resolve_contained_doc_reference( - allocator, - declared_scope, - local_parts.items[0], - )) |resolved| { - return resolved; - } + if (try ana.resolve_contained_doc_reference( + allocator, + declared_scope, + local_parts.items, + )) |resolved| { + return resolved; } if (report_errors) { @@ -1079,7 +1077,7 @@ const Analyzer = struct { }; fn resolve_scope_prefix(ana: *Analyzer, declared_scope: []const []const u8, local_parts: []const []const u8) ?ScopePrefixMatch { - var search_scope: ?*Scope = ana.scope_map.get(declared_scope) orelse return null; + var search_scope: ?*Scope = ana.resolve_declared_scope_or_parent(declared_scope); while (search_scope) |base_scope| : (search_scope = base_scope.parent) { var resolved_scope: *Scope = base_scope; @@ -1101,29 +1099,54 @@ const Analyzer = struct { return null; } + fn resolve_declared_scope_or_parent(ana: *Analyzer, declared_scope: []const []const u8) ?*Scope { + var scope_len = declared_scope.len; + while (true) { + if (ana.scope_map.get(declared_scope[0..scope_len])) |scope| { + return scope; + } + if (scope_len == 0) { + break; + } + scope_len -= 1; + } + return null; + } + fn resolve_contained_doc_reference( ana: *Analyzer, allocator: std.mem.Allocator, declared_scope: []const []const u8, - local_name: []const u8, + local_parts: []const []const u8, ) error{OutOfMemory}!?[]const u8 { - const scope = ana.scope_map.get(declared_scope) orelse return null; - const link = scope.link orelse return null; - if (!ana.link_contains_doc_reference_target(link, local_name)) { + if (local_parts.len == 0) { return null; } - var full: std.ArrayList(u8) = .empty; - defer full.deinit(allocator); - for (declared_scope, 0..) |part, i| { - if (i > 0) try full.append(allocator, '.'); - try full.appendSlice(allocator, part); - } - if (declared_scope.len > 0) { - try full.append(allocator, '.'); + var search_scope: ?*Scope = ana.resolve_declared_scope_or_parent(declared_scope); + while (search_scope) |scope| : (search_scope = scope.parent) { + const link = scope.link orelse continue; + if (!ana.link_contains_doc_reference_target(link, local_parts[0])) { + continue; + } + + const scope_fqn = try ana.scope_to_fqn_string(allocator, scope); + defer allocator.free(scope_fqn); + var full: std.ArrayList(u8) = .empty; + defer full.deinit(allocator); + if (scope_fqn.len > 0) { + try full.appendSlice(allocator, scope_fqn); + try full.append(allocator, '.'); + } + try full.appendSlice(allocator, local_parts[0]); + for (local_parts[1..]) |part| { + try full.append(allocator, '.'); + try full.appendSlice(allocator, part); + } + return @as(?[]const u8, try full.toOwnedSlice(allocator)); } - try full.appendSlice(allocator, local_name); - return @as(?[]const u8, try full.toOwnedSlice(allocator)); + + return null; } fn link_contains_doc_reference_target(ana: *Analyzer, link: Scope.Link, local_name: []const u8) bool { diff --git a/src/tools/abi-mapper/tests/doc_ref_resolution.abi b/src/tools/abi-mapper/tests/doc_ref_resolution.abi index 9980e7bc..87ca8a58 100644 --- a/src/tools/abi-mapper/tests/doc_ref_resolution.abi +++ b/src/tools/abi-mapper/tests/doc_ref_resolution.abi @@ -24,4 +24,21 @@ namespace overlapped { out completed_count: usize; error Unscheduled; } + + struct Await_Options { + field wait: Wait; + field thread_affinity: Thread_Affinity; + + enum Thread_Affinity : u8 { + item all_threads; + item this_thread; + } + + enum Wait : u8 { + item wait_one = 1; + + /// NOTE: If @`thread_affinity` is @`Thread_Affinity.all_threads`, waiting can take longer. + item wait_all = 2; + } + } } diff --git a/src/tools/abi-mapper/tests/doc_ref_resolution.zig b/src/tools/abi-mapper/tests/doc_ref_resolution.zig index 419e7634..c5fb7eca 100644 --- a/src/tools/abi-mapper/tests/doc_ref_resolution.zig +++ b/src/tools/abi-mapper/tests/doc_ref_resolution.zig @@ -53,6 +53,25 @@ test "doc references resolve to contained syscall elements" { await_completion_of.docs, "overlapped.await_completion_of.events", )); + + const wait_enum = find_enum_by_fqn( + analyzed_document.enums, + "overlapped.Await_Options.Wait", + ) orelse return error.TestUnexpectedResult; + const wait_all_item = find_enum_item_by_name( + wait_enum.items, + "wait_all", + ) orelse return error.TestUnexpectedResult; + + try std.testing.expect(has_ref_fqn( + wait_all_item.docs, + "overlapped.Await_Options.thread_affinity", + )); + try std.testing.expect(has_ref_fqn( + wait_all_item.docs, + "overlapped.Await_Options.Thread_Affinity.all_threads", + )); + try std.testing.expect(!has_ref_fqn(wait_all_item.docs, "thread_affinity")); } fn find_syscall_by_fqn( @@ -67,6 +86,27 @@ fn find_syscall_by_fqn( return null; } +fn find_enum_by_fqn( + enums: []const model.Enumeration, + expected: []const u8, +) ?model.Enumeration { + for (enums) |item| { + if (fqn_equals(item.full_qualified_name, expected)) { + return item; + } + } + return null; +} + +fn find_enum_item_by_name(items: []const model.EnumItem, name: []const u8) ?model.EnumItem { + for (items) |item| { + if (std.mem.eql(u8, item.name, name)) { + return item; + } + } + return null; +} + fn fqn_equals(fqn: model.FQN, expected: []const u8) bool { var parts = std.mem.splitScalar(u8, expected, '.'); var index: usize = 0; From 56681e8c6af9e5da37fcc5b8db35ffe81492a5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 00:04:33 +0100 Subject: [PATCH 15/36] Implements hyperlinking doc comment refs --- src/website/src/syscalls-gen.zig | 99 +++++++++++++++++++++++--------- src/website/www/theme.css | 4 ++ 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index 8851bb2e..c9ce0abe 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -125,7 +125,7 @@ const PageRenderer = struct { if (!decl.docs.is_empty()) { try html.writer.writeAll("
      \n"); try html.writer.print("

      Documentation

      \n", .{}); - try html.writer.print("{f}\n", .{fmt_docs(decl.docs)}); + try html.writer.print("{f}\n", .{fmt_docs(decl.docs, html.scope_fqn.len - 1)}); try html.writer.writeAll("
      \n"); } @@ -138,7 +138,10 @@ const PageRenderer = struct { try html.begin_dl(); for (item.logic_fields) |field| { - try html.writer.writeAll("
      \n"); + try html.writer.print("
      \n", .{ + fmt_fqn(item.full_qualified_name), + field.name, + }); try html.writer.print("
      {s}: {f}", .{ field.name, @@ -152,7 +155,7 @@ const PageRenderer = struct { try html.writer.writeAll("
      "); if (!field.docs.is_empty()) { - try html.writer.print("
      {f}
      \n", .{fmt_docs(field.docs)}); + try html.writer.print("
      {f}
      \n", .{fmt_docs(field.docs, html.scope_fqn.len - 1)}); } try html.writer.writeAll("
      \n"); @@ -167,12 +170,14 @@ const PageRenderer = struct { try html.begin_dl(); for (item.logic_fields) |field| { try html.dl_item( + item.full_qualified_name, + field.name, "{s}: {f}", "{f}", .{ field.name, html.fmt_type(field.type), - fmt_docs(field.docs), + fmt_docs(field.docs, html.scope_fqn.len - 1), }, ); } @@ -188,18 +193,22 @@ const PageRenderer = struct { try html.begin_dl(); for (enumeration.items) |item| { try html.dl_item( + enumeration.full_qualified_name, + item.name, "{s} = {}", "{f}", .{ item.name, item.value, - fmt_docs(item.docs), + fmt_docs(item.docs, html.scope_fqn.len - 1), }, ); } switch (enumeration.kind) { .open => try html.dl_item( + enumeration.full_qualified_name, + "...", "...", "

      This enumeration is non-exhaustive and may assume all values a {s} can represent.

      ", .{@tagName(enumeration.backing_type)}, @@ -215,7 +224,10 @@ const PageRenderer = struct { try html.begin_dl(); for (item.fields) |field| { - try html.writer.writeAll("
      \n"); + try html.writer.print("
      \n", .{ + fmt_fqn(item.full_qualified_name), + field.name orelse "", + }); try html.writer.print("
      {s}: {f}", .{ field.name orelse "reserved", @@ -229,7 +241,7 @@ const PageRenderer = struct { try html.writer.writeAll("
      "); if (!field.docs.is_empty()) { - try html.writer.print("
      {f}
      \n", .{fmt_docs(field.docs)}); + try html.writer.print("
      {f}
      \n", .{fmt_docs(field.docs, html.scope_fqn.len - 1)}); } try html.writer.writeAll("
      \n"); @@ -246,12 +258,14 @@ const PageRenderer = struct { for (item.logic_inputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + fmt_docs(param.docs, html.scope_fqn.len - 1), }, ); } @@ -265,12 +279,14 @@ const PageRenderer = struct { for (item.logic_outputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + fmt_docs(param.docs, html.scope_fqn.len - 1), }, ); } @@ -283,11 +299,13 @@ const PageRenderer = struct { try html.begin_dl(); for (item.errors) |err| { try html.dl_item( + item.full_qualified_name, + err.name, "{f}", "{f}", .{ std.zig.fmtId(err.name), - fmt_docs(err.docs), + fmt_docs(err.docs, html.scope_fqn.len - 1), }, ); } @@ -304,12 +322,14 @@ const PageRenderer = struct { for (item.logic_inputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + fmt_docs(param.docs, html.scope_fqn.len - 1), }, ); } @@ -323,12 +343,14 @@ const PageRenderer = struct { for (item.logic_outputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + fmt_docs(param.docs, html.scope_fqn.len - 1), }, ); } @@ -341,11 +363,13 @@ const PageRenderer = struct { try html.begin_dl(); for (item.errors) |err| { try html.dl_item( + item.full_qualified_name, + err.name, "{f}", "{f}", .{ std.zig.fmtId(err.name), - fmt_docs(err.docs), + fmt_docs(err.docs, html.scope_fqn.len - 1), }, ); } @@ -428,9 +452,15 @@ const PageRenderer = struct { try html.writer.writeAll("\n"); } - fn dl_item(html: *PageRenderer, comptime dt_fmt: []const u8, comptime dd_fmt: []const u8, args: anytype) !void { + fn dl_item(html: *PageRenderer, fqn: []const []const u8, local_name: ?[]const u8, comptime dt_fmt: []const u8, comptime dd_fmt: []const u8, args: anytype) !void { + if (local_name) |name| { + try html.writer.print("
      ", .{ fmt_fqn(fqn), name }); + } else { + try html.writer.print("
      ", .{fmt_fqn(fqn)}); + } + try html.writer.print( - "
      \n
      " ++ dt_fmt ++ "
      " ++ dd_fmt ++ "
      \n", + "
      " ++ dt_fmt ++ "
      " ++ dd_fmt ++ "
      \n", args, ); } @@ -493,7 +523,7 @@ const PageRenderer = struct { if (child.data != tag) continue; - try html.writer.writeAll("
      \n"); + try html.writer.print("
      \n", .{fmt_fqn(child.full_qualified_name)}); try html.writer.print( \\
      {s} {s}( @@ -547,7 +577,7 @@ const PageRenderer = struct { \\
      {f}
      \\ , .{ - fmt_docs(child.docs), + fmt_docs(child.docs, html.scope_fqn.len - 1), }); } @@ -737,11 +767,12 @@ fn format_fqn(fqn: []const []const u8, writer: *std.Io.Writer) !void { } } -fn fmt_docs(docs: model.DocComment) std.fmt.Alt(model.DocComment, format_docs) { - return .{ .data = docs }; +fn fmt_docs(docs: model.DocComment, url_nesting: usize) std.fmt.Alt(struct { model.DocComment, usize }, format_docs) { + return .{ .data = .{ docs, url_nesting } }; } -fn format_docs(docs: model.DocComment, writer: *std.Io.Writer) !void { +fn format_docs(_params: struct { model.DocComment, usize }, writer: *std.Io.Writer) !void { + const docs: model.DocComment, const url_nesting: usize = _params; if (docs.is_empty()) return; @@ -752,7 +783,7 @@ fn format_docs(docs: model.DocComment, writer: *std.Io.Writer) !void { switch (block) { .paragraph => |p| { try writer.writeAll("

      \n"); - try format_inlines(p.content, writer); + try format_inlines(p.content, url_nesting, writer); try writer.writeAll("

      \n"); }, @@ -760,7 +791,7 @@ fn format_docs(docs: model.DocComment, writer: *std.Io.Writer) !void { try writer.writeAll("
        \n"); for (list.items) |item| { try writer.writeAll("
      1. \n"); - try format_inlines(item, writer); + try format_inlines(item, url_nesting, writer); try writer.writeAll("
      2. \n"); } try writer.writeAll("
      \n"); @@ -769,7 +800,7 @@ fn format_docs(docs: model.DocComment, writer: *std.Io.Writer) !void { try writer.writeAll("
        \n"); for (list.items) |item| { try writer.writeAll("
      • \n"); - try format_inlines(item, writer); + try format_inlines(item, url_nesting, writer); try writer.writeAll("
      • \n"); } try writer.writeAll("
      \n"); @@ -793,24 +824,38 @@ fn format_docs(docs: model.DocComment, writer: *std.Io.Writer) !void { } } -fn format_inlines(inlines: []const model.DocComment.Inline, writer: *std.Io.Writer) !void { +fn format_inlines(inlines: []const model.DocComment.Inline, url_nesting: usize, writer: *std.Io.Writer) !void { for (inlines) |span| { switch (span) { .text => |text| try writer.writeAll(text.value), .code => |code| try writer.print("{s}", .{code.value}), .emphasis => |emphasis| { try writer.writeAll(""); - try format_inlines(emphasis.content, writer); + try format_inlines(emphasis.content, url_nesting, writer); try writer.writeAll(""); }, .ref => |ref| { - try writer.print("", .{fmt_attr("???")}); + var url_buffer: [512]u8 = undefined; + var url_writer: std.Io.Writer = .fixed(&url_buffer); + + try url_writer.splatBytesAll("../", url_nesting); + + var pos: usize = 0; + while (pos < ref.fqn.len) { + const split = std.mem.indexOfScalarPos(u8, ref.fqn, pos, '.') orelse break; + try url_writer.print("{s}/", .{ref.fqn[pos..split]}); + pos = split + 1; + } + + try url_writer.print("index.html#{s}", .{ref.fqn}); + + try writer.print("", .{fmt_url(url_writer.buffered(), 0)}); try writer.print("{s}", .{ref.fqn}); try writer.writeAll(""); }, .link => |link| { try writer.print("", .{fmt_attr(link.url)}); - try format_inlines(link.content, writer); + try format_inlines(link.content, url_nesting, writer); try writer.writeAll(""); }, } diff --git a/src/website/www/theme.css b/src/website/www/theme.css index 703554f0..3d57e719 100644 --- a/src/website/www/theme.css +++ b/src/website/www/theme.css @@ -242,6 +242,10 @@ div.flex-spacer { border-color: #373737; } +.docs-container dl>div:target { + background-color: #e0d9ea; +} + .docs-container dl dd p { margin-left: 2rem; margin-top: 0.5em; From 17ef98771b63159c6c488994df69d815a768968a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 12:38:45 +0100 Subject: [PATCH 16/36] Claude Code: Implements rendering of local qualified names derived from context + fqn inside the docs --- src/website/src/syscalls-gen.zig | 206 +++++++++++++++++-------------- 1 file changed, 114 insertions(+), 92 deletions(-) diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index c9ce0abe..19ec4919 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -125,7 +125,7 @@ const PageRenderer = struct { if (!decl.docs.is_empty()) { try html.writer.writeAll("
      \n"); try html.writer.print("

      Documentation

      \n", .{}); - try html.writer.print("{f}\n", .{fmt_docs(decl.docs, html.scope_fqn.len - 1)}); + try html.writer.print("{f}\n", .{html.fmt_docs(decl.docs)}); try html.writer.writeAll("
      \n"); } @@ -155,7 +155,7 @@ const PageRenderer = struct { try html.writer.writeAll("
      "); if (!field.docs.is_empty()) { - try html.writer.print("
      {f}
      \n", .{fmt_docs(field.docs, html.scope_fqn.len - 1)}); + try html.writer.print("
      {f}
      \n", .{html.fmt_docs(field.docs)}); } try html.writer.writeAll("
      \n"); @@ -177,7 +177,7 @@ const PageRenderer = struct { .{ field.name, html.fmt_type(field.type), - fmt_docs(field.docs, html.scope_fqn.len - 1), + html.fmt_docs(field.docs), }, ); } @@ -200,7 +200,7 @@ const PageRenderer = struct { .{ item.name, item.value, - fmt_docs(item.docs, html.scope_fqn.len - 1), + html.fmt_docs(item.docs), }, ); } @@ -241,7 +241,7 @@ const PageRenderer = struct { try html.writer.writeAll(""); if (!field.docs.is_empty()) { - try html.writer.print("
      {f}
      \n", .{fmt_docs(field.docs, html.scope_fqn.len - 1)}); + try html.writer.print("
      {f}
      \n", .{html.fmt_docs(field.docs)}); } try html.writer.writeAll("
      \n"); @@ -265,7 +265,7 @@ const PageRenderer = struct { .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs, html.scope_fqn.len - 1), + html.fmt_docs(param.docs), }, ); } @@ -286,7 +286,7 @@ const PageRenderer = struct { .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs, html.scope_fqn.len - 1), + html.fmt_docs(param.docs), }, ); } @@ -305,7 +305,7 @@ const PageRenderer = struct { "{f}", .{ std.zig.fmtId(err.name), - fmt_docs(err.docs, html.scope_fqn.len - 1), + html.fmt_docs(err.docs), }, ); } @@ -329,7 +329,7 @@ const PageRenderer = struct { .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs, html.scope_fqn.len - 1), + html.fmt_docs(param.docs), }, ); } @@ -350,7 +350,7 @@ const PageRenderer = struct { .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs, html.scope_fqn.len - 1), + html.fmt_docs(param.docs), }, ); } @@ -369,7 +369,7 @@ const PageRenderer = struct { "{f}", .{ std.zig.fmtId(err.name), - fmt_docs(err.docs, html.scope_fqn.len - 1), + html.fmt_docs(err.docs), }, ); } @@ -577,7 +577,7 @@ const PageRenderer = struct { \\
      {f}
      \\ , .{ - fmt_docs(child.docs, html.scope_fqn.len - 1), + html.fmt_docs(child.docs), }); } @@ -713,6 +713,10 @@ const PageRenderer = struct { } try writer.writeAll("index.html"); } + + fn fmt_docs(html: *PageRenderer, docs: model.DocComment) DocFmt { + return .{ .docs = docs, .url_nesting = html.scope_fqn.len - 1, .scope_fqn = html.scope_fqn }; + } }; fn render_page_header(writer: *std.Io.Writer, namespace_fqn: abi_parser.model.FQN, nesting: usize) !void { @@ -767,97 +771,115 @@ fn format_fqn(fqn: []const []const u8, writer: *std.Io.Writer) !void { } } -fn fmt_docs(docs: model.DocComment, url_nesting: usize) std.fmt.Alt(struct { model.DocComment, usize }, format_docs) { - return .{ .data = .{ docs, url_nesting } }; -} +const DocFmt = struct { + docs: model.DocComment, + url_nesting: usize, + scope_fqn: model.FQN, -fn format_docs(_params: struct { model.DocComment, usize }, writer: *std.Io.Writer) !void { - const docs: model.DocComment, const url_nesting: usize = _params; - if (docs.is_empty()) - return; + pub fn format(self: DocFmt, writer: *std.Io.Writer) !void { + if (self.docs.is_empty()) + return; - for (docs.sections) |section| { - try writer.print("
      \n", .{section.kind}); + for (self.docs.sections) |section| { + try writer.print("
      \n", .{section.kind}); + + for (section.blocks) |block| { + switch (block) { + .paragraph => |p| { + try writer.writeAll("

      \n"); + try self.format_inlines(p.content, writer); + try writer.writeAll("

      \n"); + }, + + .ordered_list => |list| { + try writer.writeAll("
        \n"); + for (list.items) |item| { + try writer.writeAll("
      1. \n"); + try self.format_inlines(item, writer); + try writer.writeAll("
      2. \n"); + } + try writer.writeAll("
      \n"); + }, + .unordered_list => |list| { + try writer.writeAll("
        \n"); + for (list.items) |item| { + try writer.writeAll("
      • \n"); + try self.format_inlines(item, writer); + try writer.writeAll("
      • \n"); + } + try writer.writeAll("
      \n"); + }, + + .code_block => |code| { + try writer.writeAll("
      ");
      +                        try writer.print("{f}", .{fmt_attr(code.content)});
      +                        try writer.writeAll("
      \n"); + }, + } + } - for (section.blocks) |block| { - switch (block) { - .paragraph => |p| { - try writer.writeAll("

      \n"); - try format_inlines(p.content, url_nesting, writer); - try writer.writeAll("

      \n"); - }, + try writer.writeAll("
      \n"); + } + } - .ordered_list => |list| { - try writer.writeAll("
        \n"); - for (list.items) |item| { - try writer.writeAll("
      1. \n"); - try format_inlines(item, url_nesting, writer); - try writer.writeAll("
      2. \n"); - } - try writer.writeAll("
      \n"); - }, - .unordered_list => |list| { - try writer.writeAll("
        \n"); - for (list.items) |item| { - try writer.writeAll("
      • \n"); - try format_inlines(item, url_nesting, writer); - try writer.writeAll("
      • \n"); - } - try writer.writeAll("
      \n"); + fn format_inlines(self: DocFmt, inlines: []const model.DocComment.Inline, writer: *std.Io.Writer) !void { + for (inlines) |span| { + switch (span) { + .text => |text| try writer.writeAll(text.value), + .code => |code| try writer.print("{s}", .{code.value}), + .emphasis => |emphasis| { + try writer.writeAll(""); + try self.format_inlines(emphasis.content, writer); + try writer.writeAll(""); }, + .ref => |ref| { + var url_buffer: [512]u8 = undefined; + var url_writer: std.Io.Writer = .fixed(&url_buffer); - .code_block => |code| { - try writer.writeAll("
      ");
      -                    try writer.print("{f}", .{fmt_attr(code.content)});
      -                    try writer.writeAll("
      \n"); + + try url_writer.print("index.html#{s}", .{ref.fqn}); + + try writer.print("", .{fmt_url(url_writer.buffered(), 0)}); + try writer.print("{s}", .{self.local_ref_display(ref.fqn)}); + try writer.writeAll(""); + }, + .link => |link| { + try writer.print("", .{fmt_attr(link.url)}); + try self.format_inlines(link.content, writer); + try writer.writeAll(""); }, } } - - try writer.writeAll("
      \n"); } -} -fn format_inlines(inlines: []const model.DocComment.Inline, url_nesting: usize, writer: *std.Io.Writer) !void { - for (inlines) |span| { - switch (span) { - .text => |text| try writer.writeAll(text.value), - .code => |code| try writer.print("{s}", .{code.value}), - .emphasis => |emphasis| { - try writer.writeAll(""); - try format_inlines(emphasis.content, url_nesting, writer); - try writer.writeAll(""); - }, - .ref => |ref| { - var url_buffer: [512]u8 = undefined; - var url_writer: std.Io.Writer = .fixed(&url_buffer); - - try url_writer.splatBytesAll("../", url_nesting); - - var pos: usize = 0; - while (pos < ref.fqn.len) { - const split = std.mem.indexOfScalarPos(u8, ref.fqn, pos, '.') orelse break; - try url_writer.print("{s}/", .{ref.fqn[pos..split]}); - pos = split + 1; - } - - try url_writer.print("index.html#{s}", .{ref.fqn}); - - try writer.print("", .{fmt_url(url_writer.buffered(), 0)}); - try writer.print("{s}", .{ref.fqn}); - try writer.writeAll(""); - }, - .link => |link| { - try writer.print("", .{fmt_attr(link.url)}); - try format_inlines(link.content, url_nesting, writer); - try writer.writeAll(""); - }, + /// Returns the locally qualified display name for a ref FQN relative to + /// this scope. Strips the scope prefix (everything after the root "ashet" + /// component) when the ref shares it, so refs within the same namespace + /// are shown without redundant qualification. + fn local_ref_display(self: DocFmt, ref_fqn: []const u8) []const u8 { + var pos: usize = 0; + // scope_fqn[0] is the root "ashet"; refs don't include it, so start at [1] + for (self.scope_fqn[1..]) |part| { + if (!std.mem.startsWith(u8, ref_fqn[pos..], part)) break; + const next = pos + part.len; + if (next >= ref_fqn.len or ref_fqn[next] != '.') break; + pos = next + 1; } + return ref_fqn[pos..]; } -} +}; + From 90107d978885768f9d5fd4a8a87fb53a0dd60587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 12:49:11 +0100 Subject: [PATCH 17/36] Claude Code: Updates syscall renderer such that type refs to known types are now also rendered as locally qualified names to simplify display. --- src/website/src/syscalls-gen.zig | 40 ++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index 19ec4919..eb7f8e50 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -666,10 +666,14 @@ const PageRenderer = struct { fn fmt_known_type(html: *PageRenderer, writer: *std.Io.Writer, known_type: anytype) !void { try writer.print("{f}", .{ html.fmt_page_url(known_type.full_qualified_name), - fmt_fqn(known_type.full_qualified_name), + html.fmt_lqn(known_type.full_qualified_name), }); } + fn fmt_lqn(html: *PageRenderer, fqn: model.FQN) LqnFmt { + return .{ .html = html, .fqn = fqn }; + } + fn format_value(value: model.Value, writer: *std.Io.Writer) !void { return format_value_inner(value, writer, 1); } @@ -715,7 +719,25 @@ const PageRenderer = struct { } fn fmt_docs(html: *PageRenderer, docs: model.DocComment) DocFmt { - return .{ .docs = docs, .url_nesting = html.scope_fqn.len - 1, .scope_fqn = html.scope_fqn }; + return .{ .docs = docs, .html = html }; + } +}; + +const LqnFmt = struct { + html: *PageRenderer, + fqn: model.FQN, + + pub fn format(self: LqnFmt, writer: *std.Io.Writer) !void { + // Count how many leading components of fqn match scope_fqn[1..] (skip root "ashet") + const scope = self.html.scope_fqn[1..]; + var skip: usize = 0; + while (skip < scope.len and skip < self.fqn.len and + std.mem.eql(u8, scope[skip], self.fqn[skip])) : (skip += 1) + {} + for (self.fqn[skip..], 0..) |part, i| { + if (i > 0) try writer.writeAll("."); + try writer.writeAll(part); + } } }; @@ -773,8 +795,7 @@ fn format_fqn(fqn: []const []const u8, writer: *std.Io.Writer) !void { const DocFmt = struct { docs: model.DocComment, - url_nesting: usize, - scope_fqn: model.FQN, + html: *PageRenderer, pub fn format(self: DocFmt, writer: *std.Io.Writer) !void { if (self.docs.is_empty()) @@ -842,7 +863,7 @@ const DocFmt = struct { var url_buffer: [512]u8 = undefined; var url_writer: std.Io.Writer = .fixed(&url_buffer); - try url_writer.splatBytesAll("../", self.url_nesting); + try url_writer.splatBytesAll("../", self.html.scope_fqn.len - 1); var pos: usize = 0; while (pos < ref.fqn.len) { @@ -866,14 +887,13 @@ const DocFmt = struct { } } - /// Returns the locally qualified display name for a ref FQN relative to - /// this scope. Strips the scope prefix (everything after the root "ashet" - /// component) when the ref shares it, so refs within the same namespace - /// are shown without redundant qualification. + /// Returns the locally qualified display name for a dot-joined ref FQN + /// relative to the current scope. Strips the scope prefix (everything + /// after the root "ashet" component) when the ref shares it. fn local_ref_display(self: DocFmt, ref_fqn: []const u8) []const u8 { var pos: usize = 0; // scope_fqn[0] is the root "ashet"; refs don't include it, so start at [1] - for (self.scope_fqn[1..]) |part| { + for (self.html.scope_fqn[1..]) |part| { if (!std.mem.startsWith(u8, ref_fqn[pos..], part)) break; const next = pos + part.len; if (next >= ref_fqn.len or ref_fqn[next] != '.') break; From ca41605372ccdb9d32d6b9202f8509b4e3a51995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 13:02:38 +0100 Subject: [PATCH 18/36] Claude Code: Fixes linking for different kind of link targets based off the declaration. --- src/website/src/syscalls-gen.zig | 42 ++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index eb7f8e50..54ec1a9d 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -721,6 +721,23 @@ const PageRenderer = struct { fn fmt_docs(html: *PageRenderer, docs: model.DocComment) DocFmt { return .{ .docs = docs, .html = html }; } + + fn find_declaration(html: *PageRenderer, fqn_str: []const u8) ?model.Declaration { + var current: []const model.Declaration = html.schema.root; + var pos: usize = 0; + while (pos <= fqn_str.len) { + const end = std.mem.indexOfScalarPos(u8, fqn_str, pos, '.') orelse fqn_str.len; + const part = fqn_str[pos..end]; + const found = for (current) |decl| { + if (std.mem.eql(u8, decl.full_qualified_name[decl.full_qualified_name.len - 1], part)) + break decl; + } else return null; + if (end == fqn_str.len) return found; + current = found.children; + pos = end + 1; + } + return null; + } }; const LqnFmt = struct { @@ -865,15 +882,26 @@ const DocFmt = struct { try url_writer.splatBytesAll("../", self.html.scope_fqn.len - 1); - var pos: usize = 0; - while (pos < ref.fqn.len) { - const split = std.mem.indexOfScalarPos(u8, ref.fqn, pos, '.') orelse break; - try url_writer.print("{s}/", .{ref.fqn[pos..split]}); - pos = split + 1; + if (self.html.find_declaration(ref.fqn) != null) { + // Declaration: all parts become path segments, links to its own index.html + var pos: usize = 0; + while (pos < ref.fqn.len) { + const end = std.mem.indexOfScalarPos(u8, ref.fqn, pos, '.') orelse ref.fqn.len; + try url_writer.print("{s}/", .{ref.fqn[pos..end]}); + pos = end + 1; + } + try url_writer.writeAll("index.html"); + } else { + // Sub-item: parent path segments + fragment anchor + var pos: usize = 0; + while (pos < ref.fqn.len) { + const split = std.mem.indexOfScalarPos(u8, ref.fqn, pos, '.') orelse break; + try url_writer.print("{s}/", .{ref.fqn[pos..split]}); + pos = split + 1; + } + try url_writer.print("index.html#{s}", .{ref.fqn}); } - try url_writer.print("index.html#{s}", .{ref.fqn}); - try writer.print("", .{fmt_url(url_writer.buffered(), 0)}); try writer.print("{s}", .{self.local_ref_display(ref.fqn)}); try writer.writeAll(""); From 16c6e6ddb0d44b43f34d692bd87e1bdb8fe9103a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 13:27:01 +0100 Subject: [PATCH 19/36] Adds current working draft for 1.0 ABI file, to serve as a stress test for the abi-parser. --- .../abi-mapper/tests/stress/ashet-1.0.abi | 9226 +++++++++++++++++ 1 file changed, 9226 insertions(+) create mode 100644 src/tools/abi-mapper/tests/stress/ashet-1.0.abi diff --git a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi new file mode 100644 index 00000000..4c795bda --- /dev/null +++ b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi @@ -0,0 +1,9226 @@ + +/// Enumeration of all syscall numbers. +typedef Syscall_ID = <>; + +/// The base type for all system resources. +enum SystemResource : usize +{ + ... + + typedef Type = <>; +} + +/// All syscalls related to generic resource management. +/// +/// - Resources are created through various calls in the kernel api, but their +/// lifetime and availability is managed through calls inside this namespace. +/// - After creation, a resource is strongly bound to the process that created the +/// resource. +/// - When a resource is destroyed, it becomes unusable from userland. +/// - As long as a resource is strongly bound to at least a single process, it is +/// not automatically destroyed. +/// - As soon as a resource has no strong bindings anymore, it is destroyed by the kernel. +/// - A process can only access the resources bound to the process. +/// - In addition to strong bindings, weak bindings also exist. +/// - Weak bindings allow a process access to a resource, but don't keep the resource alive. +/// - This allows processes to access resources they don't own. +/// - Resources can be tethered to other resources. +/// - If a tethered resource is destroyed, the associated resource is also destroyed. +/// +/// NOTE: Every kernel object the userland can interact with is a resource. +/// +/// NOTE: If a resource is destroyed by any means (zero strong bindings, manual destruction, tethering), +/// it destroys all resources tethered to it (i.e. where it is the `source`). +/// This may lead to a cascade called "tether chain". +/// +/// NOTE: Tethering can form cycles. This allows resources to be tightly bound together and if one +/// resource dies, the other one also dies. +/// +/// NOTE: The order in which tether destruction is executed is unspecified. Implementors must not assume +/// any order of destruction. +/// +/// NOTE: Tethering does not affect resource lifetimes. A tether will never keep +/// other resources alive. Tethers are removed if the `target` resource is destroyed +/// and all of its tether effects are resolved. +/// +/// LORE: Originally, Ashet OS had no concept of bindings, but only of ownership. +/// But this quickly lead to problems like "the desktop server also owns the window +/// so even if the application releases the window, it is not destroyed". +/// Cycles like this could only be resolved by an explicit call to `destroy` instead +/// of `release`. But this yields brittle code that expects correct application shutdown. +/// In cases of crashes, the resource would only be released, but not destroyed. +/// The idea of allowing a process to access a resource, but not keeping it alive solves +/// this problem completely and also allows some other patterns to work well. +/// +/// LORE: The idea of resource tethering came after the idea of bindings. +/// Tethering allows resolving a problem most other operating systems have, which is: +/// What happens if a thread dies unexpectedly. +/// In operating systems without tethering, the application has to monitor the threads +/// and if a thread dies, it has to manually clean up the resources of that thread +/// assuming it has properly registered the resources in a global management structure. +/// With tethering, we can tether the lifetime of a file handle to the lifetime of its +/// owning thread, meaning: If the thread dies, the file is closed. +/// As this is a very useful property, i've decided to implement it as a broader general +/// concept instead of tying it to threads only. +namespace resources { + /// Returns the type of the system resource. + syscall get_type { + in @"resource": SystemResource; + out type: SystemResource.Type; + error InvalidHandle; + } + + /// Immediately destroys the resource and releases its memory. + /// + /// NOTE: This will *always* destroy the resource, even if it's + /// still strongly bound by a process. + /// + /// NOTE: This immediately triggers tether chains and destroys + /// all tethered resources as well. + /// + /// NOTE: `destroy` always succeeds; destroying an invalid or already-destroyed handle is a no-op. + syscall destroy { + in @"resource": SystemResource; + } + + /// Defines the possible kinds of bind operations we have. + enum BindOperation : u8 + { + /// The resource shall be unbound from the process. + item unbind = 0; + + /// The resource will be bound strongly to the process. + item strong = 1; + + /// The resource will be bound weakly to the process. + item weak = 2; + + /// This operation ensures that the binding is at least `weak`, + /// but may never be downgraded from `strong`. + /// + /// This gives the ability to ensure a process can definitely access a resource + /// without force-downgrading it to a `weak` binding if the process already has + /// a `strong` binding. + item at_least_weak = 3; + } + + /// Binds a resource to a process. + /// + /// The success of this operation allows `target` to access `resource`, and optionally + /// gain/lose a strong binding of the resource. + /// + /// NOTE: This function can be used to up/downgrade the binding of a resource. + /// + /// NOTE: If `binding` is `unbind`, the resource is instead unbound from the process and + /// may be released. + /// + /// If all strong bindings of a resource are removed, the resource will be destroyed. + /// + /// NOTE: The following operations are idempotent: + /// - `binding=weak` when already bound weak. + /// - `binding=strong` when already bound strong. + /// - `binding=unbind` when not bound. + /// - `binding=at_least_weak` when already bound weak or strong. + syscall bind { + in @"resource": SystemResource; + + /// The process which the resource should be bound to. If `null`, uses the current process. + in target: ?process.Process; + + /// The type of binding operation that shall be performed. + in binding: BindOperation; + + /// The resource or process handle was invalid. + error InvalidHandle; + + /// The `target` process is dead, but still has an alive handle. + /// NOTE: Cannot happen when `binding` is `unbind`. + error ZombieProcess; + + /// The system ran out of resources when handling the request. + /// NOTE: Cannot happen when `binding` is `unbind`. + error SystemResources; + } + + + /// Defines the possible kinds of binding a resource can have. + enum Binding : u8 + { + /// The resource is not bound to the process. + /// This means the process cannot access the resource and also does not keep + /// the resource alive. + item unbound = 0; + + /// The resource is strongly bound to the process. + /// As long as a single strong binding exists, the resource is + /// valid. + item strong = 1; + + /// The resource is weakly bound to the process. + /// This means the process can access the resource, but does not + /// keep the resource alive. + item weak = 2; + } + + /// Returns the binding for a resource on a process. + syscall get_binding { + in @"resource": SystemResource; + + /// The process for which the resource binding shall be queried. If `null`, uses the current process. + in target: ?process.Process; + + /// The kind of binding the resource has on `target`. + out binding: Binding; + + /// The resource or process handle was invalid. + error InvalidHandle; + } + + /// Returns the current bindings of a resource. + /// + /// NOTE: The order in which processes and bindings are returned are not guaranteed to be + /// stable between two calls. + /// + /// NOTE: When `processes` is `null`, `bindings` can be used to count strong vs weak bindings. + /// + /// NOTE: When `resource` is the currently executing process, that process is always returned + /// in `processes`. + syscall get_bindings { + /// The resource which should be queried. + in @"resource": SystemResource; + + /// If not `null`, will receive the process handles that have a binding + /// on `resource`. + /// + /// NOTE: The process handles will be bound to the calling process with `BindOperation.at_least_weak` + /// to ensure resource access. + in processes: ?[]process.Process; + + /// If not `null`, will receive the binding types of all bindings on `resource`. + /// If `processes` is also provided, `bindings[i]` corresponds to `processes[i]`. + /// Otherwise, the order is unspecified. + /// + /// NOTE: No value written in this will ever be `unbound`. + in bindings: ?[]Binding; + + /// The number of bindings for the resource if `processes` and `bindings` is `null`. + /// Otherwise the number of elements written to `processes` and/or `bindings`. + /// + /// NOTE: If `processes` and `bindings` have different lengths, `min(processes.len, bindings.len)` is chosen. + out count: usize; + + /// The resource or process handle was invalid. + error InvalidHandle; + + /// The system ran out of resources when handling the request. + error SystemResources; + } + + /// Defines the possible ways of how a resource is tethered to another resource. + enum TetherMode : u8 + { + /// The resources are not tethered. + item untethered = 0; + + /// If the source resource is destroyed, the target resource will be destroyed as well. + item strong = 1; + } + + /// Binds the lifetime of the 'target' resource to the lifetime of the 'source' resource. + /// + /// If `mode` is `strong` and the `source` resource is destroyed, the `target` resource + /// will implicitly be destroyed as well. + syscall tether { + /// The resource which destruction will trigger the tether event. + in source: SystemResource; + + /// The resource that will receive the tether event. + in target: SystemResource; + + /// The kind of tethering performed. + in mode: TetherMode; + + /// One of the resource handles was invalid. + error InvalidHandle; + + /// The system ran out of resources when handling the request. + error SystemResources; + } + + /// Queries the tethering between two resources. + syscall get_tether { + /// The resource which destruction will trigger the tether event. + in source: SystemResource; + + /// The resource that will receive the tether event. + in target: SystemResource; + + /// The kind of tethering performed between the two resources. + out mode: TetherMode; + + /// One of the resource handles was invalid. + error InvalidHandle; + } + + //? TODO: Potentially define get_tethers? + + /// An anchor is a system resource without any properties or functions + /// besides the basic properties of resources. + /// + /// Anchors can be used as groups for tether objects or as tokens passed + /// between processes. + resource Anchor { } + + /// Creates a new `Anchor` resource. + syscall create_anchor { + /// The newly created anchor. + out anchor: Anchor; + + /// The system ran out of resources when handling the request. + error SystemResources; + } +} + +/// This namespace is related to asynchronous running (sys)calls, which +/// is the heart of the operating system I/O. +/// +/// All system calls in Ashet OS are non-blocking except `process.thread.yield`, +/// `overlapped.await_any` and `overlapped.await_any_of`. +/// This means that all regular system calls will return as soon as possible +/// without ever waiting on external events or other operations. +/// +/// But as each operating system requires slow/blocking operations, Ashet OS +/// provides a single way of handling long-running operations: +/// +/// The Asynchronously Running system Call (ARC). +/// +/// Each ARC represents an operation that might not complete immediately, and must +/// be scheduled to the kernel. +/// Later on, the ARC is returned from the kernel in an await operation or cancelled. +/// +/// NOTE: Overlapped operations that are cancelled before completion return `error.Cancelled`. +/// +/// NOTE: Completion may also be observed via `cancel` returning `error.Completed`. +/// +/// NOTE: This concept is typically called *completion queue*. +/// +/// NOTE: The `ARC` structure must always be embedded in the associated structure type +/// for `ARC.type`, as the kernel will cast the pointer to the `ARC` structure +/// to the associated structure type. +/// +/// NOTE: Between `schedule` and the await or cancel of the ARC, the userland +/// must keep the scheduled `ARC` object and the struct containing valid and unchanged. +/// +/// The kernel will modify the `output` and `error` fields of the ARC enclosing structure. +/// +/// NOTE: If the owning process is terminated, all scheduled ARCs of that process are implicitly +/// cancelled (best-effort) and removed from the kernel; they will not be returned to userland. +/// +/// NOTE: Completion delivery is exactly-once. +/// If `cancel` returns `Completed`, the operation will not be returned by `await_any` / `await_any_of`. +/// If an operation was returned by an await syscall, later `cancel` will return `Unscheduled`. +/// +/// NOTE: If an overlapped operation has a system resource as an input, and the system resource is destroyed +/// during the operation, the operation is cancelled. +namespace overlapped { + /// Handle to an asynchronously running (system) call. + /// + /// NOTE: This struct must always be embedded in the associated + /// structure for `ARC.type` at the offset 0. + /// + /// The kernel will derive the actual structure type from `ARC.type` + /// and will cast a pointer to an `ARC` into the actual call structure type. + struct ARC + { + /// The type of operation that is performed. + /// + /// NOTE: This field is never changed by the kernel. + field type: Type; + + /// A user-specified array of pointer-sized fields which may + /// contain userland information associated with the ARC. + /// + /// NOTE: This is primarily meant for event loop systems so + /// they can associate their own data structures with + /// ARCs returned from `await_any`. + /// + /// NOTE: This field is never changed by the kernel. + field tag: [3]usize; + + typedef Type = <>; + } + + /// Starts a new asynchronous operation. + /// + /// NOTE: Until the operation has successfully completed or was + /// cancelled, the ARC structure must be considered owned + /// by the kernel and must not be changed from userspace. + /// + /// The kernel may modify the enclosing structure's `output` and `error` fields + /// while scheduled. The kernel never modifies `ARC.type` and `ARC.tag`. + /// + /// NOTE: When scheduling an ARC, the kernel associates the calling + /// thread with the operation, so the awaiter can choose whether + /// to await only its own thread's ARCs or not. + syscall schedule { + /// The call that should be scheduled. + in @"arc": *ARC; + + /// Returned when the kernel already has an active async operation + /// with the same address. + error AlreadyScheduled; + + //? TODO: Consider "InProgress" or "Conflict" as a new error which + //? would be returned when an ARC cannot be scheduled due to conflicts + //? instead of going through the whole overlapped dance just to return + //? error.InProgress. + + error SystemResources; + } + + /// Cancels an asynchronous call. + /// + /// NOTE: Cancellation leaves the operation in an undefined state. This means + /// that cancellation isn't necessarily atomic and writes may have been + /// performed halfway, nearly completely or not at all. + /// + /// Example: A tiled renderer may have completed all draw commands for + /// half of the picture tiles when being cancelled. + /// In comparison, a linear renderer would have performed + /// half of the commands on the whole picture. + /// + /// NOTE: If the operation has already completed, an error will be returned. + /// + /// NOTE: A cancelled operation cannot be awaited anymore. + /// + /// NOTE: On success or the `Completed` error, the operation will not + /// be owned by the kernel anymore and cannot be awaited anymore by + /// `await_any` or `await_any_of`. + syscall cancel { + /// The operation to cancel. + in arc: *ARC; + + /// Returned when the ARC has already run to completion. + /// + /// NOTE: This means the operation was not cancelled because it already completed + /// with or without an error. Userland should handle this case gracefully. + /// + /// NOTE: If this error is returned, the operation will not be owned by the kernel anymore + /// and shall be treated as if it was returned from `await_any` or `await_any_of` as + /// completed. + error Completed; + + /// The kernel does not know the `arc` operation. + error Unscheduled; + } + + enum Thread_Affinity : u8 { + /// Waits for ARCs scheduled from *any* thread in the current process. + item all_threads = 0; + + /// Waits for ARCs scheduled from *this* thread. + item this_thread = 1; + } + + enum BlockMode : u8 { + /// Don't wait for any additional calls to complete, just return + /// whatever was completed in the meantime. + /// + /// NOTE: This mode does NOT suspend or yield the current thread + /// and keeps the active time slice. So spinning with this mode + /// without other yield points will lock up the system! + item dont_block = 0; + + /// Wait for at least a single call to complete operation. + item wait_one = 1; + + /// Wait until all scheduled operations have completed. + /// + /// This will only wait so long until either + /// a) all scheduled ops are stored into the result array + /// or + /// b) the result array is full + /// + /// NOTE: If `thread_affinity` is `.all_threads`, other threads can still + /// schedule more operations and make this function block longer. + item wait_all = 2; + } + + /// Awaits some scheduled asynchronous operations and returns the + /// number of `completed` elements. + /// + /// The kernel will fill `completed` up to the returned number of elements. + /// All other values are left untouched. + /// + /// NOTE: For blocking operations, this function will suspend the current + /// thread until the request has been completed. This means that other + /// threads can continue their work. + /// + /// NOTE: If `completed.len` is zero, the operation will never suspend and + /// immediately return zero for `completed_count`. + /// + /// NOTE: The completed ARCs are not owned by the kernel anymore and may be scheduled again. + syscall await_any { + /// A caller-provided array which will be filled with pointers to + /// the completed ARCs. + /// NOTE: Kernel will only touch the first `completed_count` elements and + /// keeps the rest unchanged. + in completed: []*ARC; + + /// Defines how the operation will suspend. + in block_mode: BlockMode; + + /// Defines the thread affinity for the await option. + /// This allows awaiting ARCs that were scheduled by the calling thread + /// and ignoring all others. + in thread_affinity: Thread_Affinity; + + /// The number of elements filled into `completed` by the kernel. + out completed_count: usize; + } + + /// Awaits one or more ARCs from a set and returns the number of completed operations. + /// + /// The kernel will only await elements provided in `events` and all of those events must + /// not be awaited by another `await_any_of`. + /// + /// When the call returns, the kernel will have partitioned events into two parts: + /// - The head (`events[0..completed_count]`) elements will be completed. + /// - The tail (`events[completed_count..]`) elements are still in progress. + /// + /// This allows the caller to invoke the syscall again later with the tail part of the + /// array in order to await the rest. + /// + /// NOTE: This syscall will always return as soon as a single event has finished. + /// + /// NOTE: It is invalid to await the same operation with two concurrent calls to `await_any_of`. + /// + /// NOTE: Elements awaited with this function will be guaranteed to not be returned by + /// a concurrent call to `await_any`. + /// + /// NOTE: This function will suspend the current thread and yield to other threads + /// if, and only if none of the events are completed. + /// + /// NOTE: The order in which the elements in `events` will be partitioned by the kernel + /// is implementation-defined and must not be assumed to have any meaningful order. + /// + /// NOTE: The completed ARCs are not owned by the kernel anymore and may be scheduled again. + syscall await_any_of { + /// A pre-filled list of events that shall be awaited. + /// On completion of the call, this list will be reordered by the kernel into + /// two halves, the first `completed_count` containing the completed events. + in events: []*ARC; + + /// The number of ARCs completed inside `events`. + out completed_count: usize; + + /// Another `await_any_of` already awaits an event from `events`. + error InvalidOperation; + + /// The kernel does not know an operation inside `events`. + /// When this error is returned, no ARCs will be removed from the completion queue + /// and `events` is left unmodified. + error Unscheduled; + } +} + +/// Syscalls related to processes, threads and execution control. +namespace process { + /// A process is first and foremost a context for resource resolution. + /// + /// In addition to that, each process has an additional initial thread + /// that is created and launched with `Spawn`. + /// + /// Resources in threads are always resolved in regard to their owning + /// process. + /// + /// A process has two states: + /// - Active: The process is loaded and not terminated yet. + /// - Zombie: The process resource still exists, but the process itself is already terminated. + /// + /// When a process is terminated or killed, all associated threads are exited, and all bound resources + /// are unbound. This may trigger the destruction of these resources. + /// + /// In addition to that, a termination reason is stored informing observers of the process + /// how the process was terminated. + /// + /// If a process terminates itself, it can provide a hint if it was successfully terminated ("clean exit") + /// or if it failed ("error during execution"). + /// + /// A process becomes Zombie on termination and remains until its `Process` resource is destroyed. + /// Destroying the Process resource reaps the zombie and frees remaining bookkeeping. + /// + /// NOTE: Destroying an active process resource will terminate the process and immediately reap the + /// zombie. + /// + /// NOTE: A process may be of two kinds: + /// - regular + /// - daemon + /// A daemon process may have zero associated foreground threads, but still be active. If it has + /// zero threads total, it can keep resources alive but cannot execute code until a new thread is + /// spawned into it. + /// + /// NOTE: A regular process is terminated when its last foreground thread is exited. + /// If all foreground threads have exited without calling `terminate`, the process + /// is terminated with `TerminationReason.regular_exit` and `success = true`. + /// + /// LORE: Ashet OS only has a single boolean for communicating success or failure to the outside. + /// This was chosen as most applications in the wild either use `EXIT_SUCCESS (0)` or `EXIT_FAILURE (1)` + /// in C libraries anyways and don't use the exit code to communicate meaning. + /// If a more complex communication to the outside is required, there are better options like pipes or shared memory + /// passed into the process. + resource Process { } + + /// A thread is the base unit of execution. + /// + /// Each thread operates concurrently to the other threads + /// in a cooperative manner: + /// + /// This means that threads voluntarily yield execution to the + /// OS scheduler in order to let other threads do their work. + /// + /// NOTE: A thread is always associated with a process and syscalls + /// invoked by a thread resolve resources always in regard to + /// that process. + /// + /// LORE: In contrast to other operating systems, threads do not expose + /// a success/failure state. + /// This was chosen as applications can use internal signalling + /// to better communicate failure/success than having a single integer + /// for that. + resource Thread { } + + /// Terminates the current process and marks it as "controlled exit". + syscall terminate { + /// Provides the information if the process terminated successfully or not. + in success: bool; + noreturn; + } + + /// Terminates a given process and marks it as "killed". + /// + /// NOTE: A killed process is never considered successful as it was terminated + /// from the outside. + /// + /// NOTE: If the current process is passed, this function will not return. + /// + /// NOTE: If the `target` process is already terminated, this operation is idempotent. + syscall kill { + /// The process that should be killed. + in target: Process; + + /// `target` is not a valid process resource. + error InvalidHandle; + } + + + /// An argument passed to a process. + struct SpawnArg { + /// The associated name of the argument. + field name: str; + + /// The type of the argument. + field type: Type; + + /// The associated value for `type`. + field value: Value; + + enum Type : u8 { + /// The argument is a mere flag and carries no value. + /// Its existence itself already carries semantics. + item flag = 0; + + /// The argument has a string value associated. + /// NOTE: `Value.text` is active. + item string = 1; + + /// The argument has a resource value associated. + /// NOTE: The spawned process receives a strong binding for the passed resource. + /// NOTE: `Value.resource` is active. + item @"resource" = 2; + } + + union Value { + field text: String; + field @"resource": SystemResource; + } + + struct String { + field text: str; + } + } + + /// Spawns a new regular process. + /// + /// NOTE: The kernel will perform a copy of all strings inside `overlapped.schedule`, so it is safe to + /// reuse the string memory after the operation is successfully scheduled. + /// + /// This prevents unwanted use-after-free by the kernel. + /// + /// NOTE: `Spawn` will create a single initial thread, the main thread. This thread is a + /// foreground thread which will use the executables entry point as its thread function. + async_call Spawn { + /// Relative base directory for `path`. + in dir: fs.Directory; + + /// File name of the executable relative to `dir`. + in path: str; + + /// The arguments passed to the process. + /// It is safe to release the resource binding to the current process as soon as this operation returns. + in arguments: []const SpawnArg; + + /// Handle to the spawned process. + out process: Process; + + error BadExecutable; + error IoError; + error FileNotFound; + error InvalidHandle; + error InvalidPath; + error SystemResources; + } + + /// Creates a new, empty daemon process. + /// + /// NOTE: The created process will be of `ProcessKind.daemon`. + /// + /// NOTE: The kernel will not create a main thread for the process. + syscall create_empty_process { + /// The arguments passed to the process. + /// It is safe to release the resource binding to the current process as soon as this syscall returns. + /// + /// NOTE: The kernel copies all argument strings and stores them in the process object before returning, + /// so the caller may reuse/free argument memory immediately after the syscall returns. + in arguments: []const SpawnArg; + + /// Size of the memory allocated for the process. + /// + /// NOTE: The memory allocated by the kernel will not have well-defined contents. + /// The kernel may zero the memory, or just assign it without change. + /// Userland must not assume contents of the memory without previously writing it. + /// + /// NOTE: This address for this memory will be returned by `get_base_address`. + /// + /// NOTE: The kernel may allocate more than the requested memory. Userland must assume + /// exactly `image_size` are valid and may not write beyond `image_size` + /// bytes after the address returned by `get_base_address`. + /// + /// NOTE: If `0` is passed, no memory will be allocated and `get_base_address` will return an error. + in image_size: usize; + + /// Handle to the created process. + out process: Process; + + /// A handle in `arguments` is not a valid resource handle. + error InvalidHandle; + + error SystemResources; + } + + enum TerminationReason : u8 { + /// The process terminated properly through a call to `terminate`. + item regular_exit = 0; + + /// The process was killed with `kill`. + item killed = 1; + + /// The process was shut down by the kernel in order to protect the system + /// from a crash. + /// This may include execution of invalid instructions, division by zero or + /// other platform illegal behaviour. + item faulted = 2; + } + + /// Completes when the given process terminates. + /// + /// NOTE: The call will immediately complete if `target` is already terminated. + /// + /// NOTE: Awaiting termination of the same process multiple times is idempotent. + /// + /// NOTE: Multiple `WaitForTermination` operations can be scheduled at once and + /// will all complete when the process terminates. + async_call WaitForTermination { + in target: Process; + + /// The reason why the process was terminated. + out reason: TerminationReason; + + /// Contains the success of the process termination. + /// This value is: + /// - The value passed to `terminate.success`. + /// - True if terminated by all foreground threads exiting. + /// - `false` otherwise. + out successful: bool; + + /// `target` is not a valid process resource. + error InvalidHandle; + } + + /// Returns the arguments that were passed to this process in `Spawn`. + syscall get_arguments { + /// The process for which the arguments shall be returned. + /// If `null` is passed, the current process will be used. + in target: ?Process; + + /// A constant slice of the process' arguments. + /// + /// NOTE: The returned memory and all interior pointers are valid as long + /// as the `target` process resource is not destroyed. + /// + /// NOTE: If an argument refers to a `SystemResource`, the resource will be bound + /// to the calling process with an `at_least_weak` bind operation to ensure + /// resource access. + out argv: []const SpawnArg; + + /// `target` is not a valid process resource. + error InvalidHandle; + + error SystemResources; + } + + /// Returns a pointer to the file name of the process. + syscall get_file_name { + /// The process for which the file name shall be returned. + /// If `null` is passed, the current process will be used. + in target: ?Process; + + /// The file name of the process passed to `Spawn` or the empty string if no + /// file name exists. + /// + /// NOTE: This is only the basename of the file and not the full path as + /// the information about the path is not helpful without the associated + /// directory handle. + /// + /// NOTE: The returned memory and all interior pointers are valid as long + /// as the `target` process resource is not destroyed. + out file_name: str; + + /// `target` is not a valid process resource. + error InvalidHandle; + } + + /// Returns the base address of the process. + /// + /// This is the address at which the executable image is loaded and relocated to. + /// + /// NOTE: The memory is valid until the process is terminated. + syscall get_base_address { + /// The process for which the base address shall be returned. + /// If `null` is passed, the current process will be used. + in target: ?Process; + + /// The base address of the process. + out base_address: usize; + + /// `target` is not a valid process resource. + error InvalidHandle; + + /// `target` process has no assigned memory region. + error NoMemory; + } + + /// Enumeration of the different process kinds that exist. + enum ProcessKind : u8 { + /// A regular process is automatically terminated when all foreground threads have exited. + item regular = 0; + + /// A daemon process does not automatically exit when all foreground threads have exited. + /// It stays alive until it is explicitly terminated. + item daemon = 1; + } + + /// Changes the kind of a process. + /// + /// NOTE: If a process is changed to `ProcessKind.regular` and has no active foreground + /// threads, the process is automatically terminated and this function may not return. + syscall set_kind { + /// The process for which the kind shall be updated. + /// If `null` is passed, the current process will be used. + in target: ?Process; + + /// The new kind of the `target` process. + in new_kind: ProcessKind; + + /// `target` is not a valid process resource. + error InvalidHandle; + + /// The `target` process is dead, but still has an alive handle. + error ZombieProcess; + } + + /// Queries the kind of a process. + syscall get_kind { + /// The process for which the kind shall be returned. + /// If `null` is passed, the current process will be used. + in target: ?Process; + + /// The kind of the `target` process. + out kind: ProcessKind; + + /// `target` is not a valid process resource. + error InvalidHandle; + } + + namespace thread { + /// Returns control to the scheduler. Returns when the scheduler + /// schedules the process again. + syscall yield { + } + + /// Gets the resource handle for the currently executing thread. + syscall get_current { + /// The handle to the current thread. + /// NOTE: This handle is ensured to be at least weakly bound to the current process. + out handle: Thread; + } + + /// Gets the process for a given thread. + syscall get_process { + /// The handle for which the process shall be queried. + /// If `null`, will yield the process for the current thread. + in handle: ?Thread; + + /// The handle to the process owning `handle`. + /// NOTE: This handle is ensured to be at least weakly bound to the current process. + out proc: Process; + + /// `handle` is not a valid thread resource. + error InvalidHandle; + + /// The system ran out of resources when handling the request. + /// + /// NOTE: This error can only when `handle` is not `null`. + error SystemResources; + } + + /// Terminates the current thread without returning from the thread function. + /// + /// NOTE: This does not perform any stack unwinding and no code will be executed + /// after a call to this function. + /// + /// NOTE: Exiting a thread stops the execution, but it does not destroy or release the + /// thread resource. + syscall exit { + noreturn; + } + + /// Defines the signature of a thread entry point. + /// The parameter is the `arg` value passed to `spawn`. + typedef ThreadFunction = fnptr (?anyptr) void; + + /// Enumeration of the available thread kinds. + enum ThreadKind : u8 { + /// A foreground thread keeps a regular process alive. + /// + /// As long as a single foreground thread exists, a process is not + /// automatically terminated. + item foreground = 0; + + /// A background thread does not keep a regular process alive. + /// + /// This means that all background threads are automatically exited + /// when the owning process is terminated. + item background = 1; + } + + /// Spawns a new thread with `function` passing `arg` to it. + /// + /// NOTE: A spawned thread will always be associated with the current + /// process. + syscall spawn { + /// The target process for which the thread shall be spawned. + /// If `null`, will use the current process. + /// + /// NOTE: Spawning a thread in a foreign process is a valid strategy for daemons + /// and IPC services, but the implementor has to keep in mind that + /// the memory for `function` and all of the code it invokes has to outlive + /// the threads lifetime. + /// This can be ensured by tethering the `target` thread to the owner of the + /// memory so it is automatically killed if the memory is returned to the OS. + in target: ?Process; + + /// The function that the thread will execute. + in function: ThreadFunction; + + /// The argument passed to `function`. + in arg: ?anyptr; + + /// The kernel will allocate at least this amount of bytes for the threads stack. + /// If zero is passed, the kernel will chose an implementation-defined amount of + /// stack for the thread. + /// + /// NOTE: There is no guarantee that the stack won't be larger than `stack_size` + /// bytes. + in stack_size: usize; + + /// The kind of thread that is created. + in kind: ThreadKind; + + /// The thread that was created. + /// NOTE: This thread handle will be bound to the calling process with the `at_least_weak` bind operation + /// to ensure access. + /// NOTE: The created thread will live logically inside the `target` process. + out thread: Thread; + + /// `target` is not a valid process resource. + error InvalidHandle; + + /// The `target` process is dead, but still has an alive handle. + error ZombieProcess; + + error SystemResources; + } + + /// Kills the given thread. + /// + /// This is equivalent to the `target` thread executing `exit`, but triggered + /// from the outside. + /// + /// NOTE: This does not perform any stack unwinding and no code will be executed + /// in the `target` thread after a call to this function. + /// + /// NOTE: Passing in the current thread as `target` will make this function behave + /// like `exit` and it won't return. + /// + /// NOTE: Killing a thread stops the execution, but it does not destroy or release the + /// thread resource. + /// + /// NOTE: Killing an already exited thread is idempotent. + syscall kill { + in target: Thread; + error InvalidHandle; + } + + /// Waits for the thread to exit. + /// + /// NOTE: The operation will complete immediately if `target` is already exited. + /// + /// NOTE: Awaiting the exit of the same thread multiple times is idempotent. + /// + /// NOTE: Multiple `WaitForExit` operations can be scheduled at once and + /// will all complete when the thread exits. + async_call WaitForExit { + in target: Thread; + + /// Informs how the thread exited: + /// - `true`: The thread exited by its thread function returning. + /// - `false`: The thread exited by invoking `exit` or `kill`. + out regular_exit: bool; + + error InvalidHandle; + } + + /// Suspends the execution of a thread. + /// + /// This means that a thread won't be scheduled for execution + /// until it is resumed. + /// + /// NOTE: If `target` is the current thread, this syscall + /// also yields implicitly and the syscall will + /// return when the thread is resumed. + syscall suspend { + /// The thread that shall be suspended. + /// If this value is `null`, the current thread will be suspended. + in target: ?Thread; + + /// Returned when `target` is not a valid thread resource. + error InvalidHandle; + + /// `target` is a thread that already exited. + error ThreadStopped; + } + + /// Resumes the execution of a thread. + /// + /// This means that the thread will be scheduled by the + /// operating system and continues execution. + /// + /// NOTE: Resuming an already active thread is idempotent and does nothing. + syscall resume { + /// The thread that should be resumed. + in target: Thread; + + /// Returned when `target` is not a valid thread resource. + error InvalidHandle; + + /// `target` is a thread that already exited. + error ThreadStopped; + } + + /// Changes the kind of a thread after creation. + /// + /// NOTE: If the last foreground thread of a regular process is changed + /// to `ThreadKind.background`, the process will be terminated and + /// this syscall may not return. + syscall set_kind { + /// The thread that shall be updated. + /// If this value is `null`, the current thread will be updated. + in target: ?Thread; + + /// The new kind this thread is. + in kind: ThreadKind; + + /// Returned when `target` is not a valid thread resource. + error InvalidHandle; + + /// `target` is a thread that already exited. + error ThreadStopped; + } + + /// Queries the thread kind. + syscall get_kind { + /// The thread that shall be queried. + /// If this value is `null`, the current thread will be queried. + in target: ?Thread; + + /// The kind of thread `target` is. + out kind: ThreadKind; + + /// Returned when `target` is not a valid thread resource. + error InvalidHandle; + + /// `target` is a thread that already exited. + error ThreadStopped; + } + } + + namespace debug { + enum LogLevel : u8 { + /// The log message is about a critical, terminating error. The process or thread usually + /// cannot continue after such a log message. + item critical = 0; + + /// The log message is about a non-critical error. This means the process is not terminating + /// due to the error, but it might still be relevant to the user. + item err = 1; + + /// The log message is not an error, but informs about things that might still be relevant + /// to understand higher level failures like exceeded retries or failed connections. + item warn = 2; + + /// The log message informs the user about regular operations. + /// Nothing critical shall be logged with this level. + item notice = 3; + + /// The log message is only useful for debugging the process. + item debug = 4; + } + + /// Writes to the system debug log. + syscall write_log { + /// The level of severity this log message has. + in log_level: LogLevel; + + /// The message that shall be printed. + /// NOTE: `message` must be terminated with a `LF` to append a new line. + /// Log messages will be concatenated without a joining symbol, + /// so without a `LF` character, all log messages would appear on the same line. + in message: str; + } + + /// Stops the process and allows debugging. + /// + /// When this syscall returns, the process will continue execution normally. + /// + /// NOTE: If the kernel has debugging disabled, this operation may be a no-op. + /// + /// LORE: This syscall is explicitly left under-defined as the debugging style + /// may change over time. At the time of writing (2026-02-06), this basically + /// just triggers a hardware breakpoint which will crash the kernel if no + /// hardware debugger is attached. + /// As this is designed as a low-level debug facility, it is fine for development + /// and semantics can later be improved. + /// The important part is that the syscall takes no arguments and returns neither + /// a value nor an error. + syscall breakpoint { + } + } + + /// + /// Each process has it's own memory heap managed by the kernel. This removes the + /// requirement that each process has to ship their own memory allocator and the kernel + /// may choose a system-optimal allocation strategy. + /// + /// LORE: Ashet OS is tailored for systems with tiny memory footprints. + /// The typical way of memory management in operating systems is that each process + /// has their own virtual memory space and the os can do "append only"-style memory + /// management. + /// + /// The problem with this design is that it requires a memory manangement unit that + /// supports virtual memory, and usually works with page granularity. This means + /// the smallest chunk of memory a process can allocate is a single page, usually 4096 bytes. + /// + /// For the systems we target, this is 0.5‰ of the total system memory assuming 8 MiB of RAM. + /// Thus, Ashet OS provides a finer grained kernel memory allocator which internally allows sharing + /// allocations in a much finer process memory assignments than chunks of 4096 bytes. + /// + namespace memory { + /// Allocates a chunk of memory from the process heap. + syscall allocate { + /// The size of the allocated memory block in bytes. + /// + /// NOTE: Passing 0 will never succeed. + /// + /// NOTE: The kernel ensures at least `size` bytes will be + /// usable in the returned `memory`. + in size: usize; + + /// The alignment of the pointer encoded as the number of + /// left-shifts on a one. + /// + /// This gives us a safer encoding as we only accept powers of two + /// anyways. + /// + /// RANGE: 0 .. 12 + in alignment_shift: u8; + + /// A non-`null` pointer that points to exactly @ref size bytes. + /// + /// NOTE: In practise, this might point to more than @ref size bytes, + /// but the code must not assume *any* excess bytes may exist. + out pointer: [*]u8; + + /// Is returned when the system is out of memory. + error SystemResources; + } + + /// Returns previously allocated memory back to the process heap. + /// + /// NOTE: If `pointer` is not exactly a pointer previously returned by `allocate.pointer`, + /// the behaviour is undefined and could corrupt the system. + /// + /// NOTE: If `pointer` is released multiple times, the behaviour is undefined + /// and could corrupt the system. + syscall release { + /// The exact pointer previously returned in `allocate.pointer`. + in pointer: [*]u8; + } + } + + namespace monitor { + /// Queries all existing process resources. + /// + /// NOTE: The order of processes is not necessarily stable between calls. + syscall enumerate_processes { + /// An array that, if not `null`, will receive the list of processes available. + /// + /// NOTE: The process handles will be bound to the calling process with the `at_least_weak` bind operation + /// to ensure access. + in processes: ?[]Process; + + /// The number of elements written inside `processes` or the total number of processes if `processes` is `null`. + /// + /// NOTE: If `processes` is not null, not more than `processes.len` is returned. + out count: usize; + + error SystemResources; + } + + /// Queries all bound resources by a process. + /// + /// NOTE: The order of resources is not necessarily stable between calls. + syscall query_bound_resources { + /// The process for which the resources should be queried. + in proc: Process; + + /// An array that, if not `null`, will receive the list of resources available. + /// + /// NOTE: The return resources will be bound to the calling process with the `at_least_weak` bind operation + /// to ensure access. + in reslist: ?[]SystemResource; + + /// The number of elements written to `reslist` or the total number of resources if `reslist` is `null`. + /// + /// NOTE: If `reslist` is not null, not more than `reslist.len` is returned. + out count: usize; + + /// `proc` is not a valid process resource. + error InvalidHandle; + + error SystemResources; + } + + /// Returns the total number of bytes the process takes up in RAM. + syscall query_total_memory_usage { + in proc: Process; + + /// The total number of bytes the process currently allocates in RAM. + out size: usize; + + /// `proc` is not a valid process resource. + error InvalidHandle; + } + + /// Returns the number of dynamically allocated bytes for this process. + syscall query_dynamic_memory_usage { + in proc: Process; + + /// The total number of heap bytes the process currently allocates in RAM. + out size: usize; + + /// `proc` is not a valid process resource. + error InvalidHandle; + } + + /// Returns the number of total memory objects this process has right now. + syscall query_active_allocation_count { + in proc: Process; + + /// The total number of heap allocations the process currently has active. + out count: usize; + + /// `proc` is not a valid process resource. + error InvalidHandle; + } + } +} + +/// This namespace contains functions and types related to a monotonic time base. +/// +/// NOTE: Functions inside this namespace are useful for measuring time or awaiting +/// timeouts. +namespace clock { + //? TODO: Consider making `Absolute` based on a 960 kHz timer instead of a + //? 1 MHz/GHz timer precision. This would allow having a global unique + //? "audio clock compatible" time base in the system, allowing perfect + //? syntonization and correlation of different OS events. + + /// Time in nanoseconds since system startup. + enum Absolute : u64 { + /// The very first time point measurable. Nothing can happen before this point. + item system_start = 0; + + /// The very last point of system runtime that can be expressed in Ashet OS. + /// + /// LORE: This is a bit over 580 years of system runtime, which + /// is sufficient to fully cover the lifetime of the electronics, + /// users and probably even countries and societies. + /// Not quite infinity, but close enough for the computer it's running on. + item infinity = 0xFFFF_FFFF_FFFF_FFFF; + + ... + } + + /// A duration in nanoseconds. + enum Duration : u64 { + ... + } + + /// Returns the time in nanoseconds since system startup. + /// + /// NOTE: This clock never goes backwards (non-decreasing). + /// + /// NOTE: The returned value is expressed in nanoseconds, but the underlying hardware + /// may have a coarser resolution. In that case the value advances in steps. + syscall monotonic { + out time: Absolute; + } + + /// Completes when `clock.monotonic() >= deadline`. + /// + /// NOTE: The timer completes immediately if `deadline` is already reached. + async_call Timer { + /// Monotonic timestamp in nanoseconds at which the operation completes. + in deadline: Absolute; + } +} + +/// This namespace contains functions and types related to wall clock and calendar operations. +/// +/// NOTE: Leap seconds are implemented by stretching the last millisecond of a day +/// by an additional second, so the millisecond 23:59:59.999 is 1001 ms long. +/// The encoded `DateTime` value does not gain an extra representable millisecond; +/// the duration of the final millisecond is extended by the kernel when applicable. +/// +/// NOTE: Ashet OS uses [Coordinated Universal Time (UTC)](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), not [International Atomic Time (TAI)](https://en.wikipedia.org/wiki/International_Atomic_Time). +/// +/// NOTE: The local time offset is always in minutes relative to the UTC time. +/// This means that it is computed by `local = utc + offset`. +/// As an example, consider `Europe/Berlin` (Winter), which is UTC+01:00: +/// - `offset = 60` +/// - `local = utc + 60` +/// - `utc = local - 60` +/// +/// NOTE: By default, the kernel uses UTC. Use `set_timezone_offset` or `load_timezone_data` to change that. +/// +/// +/// NOTE: The kernel maintains the local time zone in exactly one of two mutually exclusive modes: +/// - Manual fixed offset (configured via `set_timezone_offset`) +/// - Rule-based offset from tzdata (configured via `load_timezone_data`) +/// The most recently invoked of these two syscalls selects the active mode. +/// +/// LORE: The decision to use minute offsets instead of seconds is that +/// there is no real reason to support this level of precision, +/// as all current real time zones are only having quarter-hour +/// steps. Minutes allow a higher precision than that, but are not +/// unnecessarily high. +/// +/// LORE: Leap seconds are explicitly not part of the kernel API to keep the API +/// simple and predictable. +/// Having to consider leap seconds in each and every API will break more programs +/// than the OS pretending they don't exist on the API boundary. +/// There are two typical implementations for this behaviour: +/// - Leap smearing: The seconds of a day with a leap second are ever so slightly slower/faster, +/// so at the end of the day, the switch from 23:59:59 to 00:00:00 will be "steadily" (with +/// just a tiny fraction of pace difference). +/// - Duplicate second: The time between the wall clock displaying 23:59:59 and 00:00:00 is +/// two seconds instead of one. This means that the last second is taking twice as long as +/// a standard second. +/// For Ashet OS, which uses millisecond precision, the decision is to just make the last +/// millisecond of the day (23:59:59.999) be 1001 ms long. +namespace datetime { + //? + //? Basic date/time management and query + //? + + /// Encodes a packed structure that encodes a calendar date + wall clock time + /// into a single integer. + /// + /// NOTE: A DateTime value is always a UTC value. + /// + /// NOTE: The DateTime value is only valid when the `milliseconds_of_day` field + /// is in the defined range between 0 and 86,399,999 (both inclusive). + /// + /// For any value outside that range the DateTime value is considered *invalid*. + /// + /// NOTE: DateTime values form a *discrete linear order*; the encoding is an *injective*, + /// *strictly monotone order-embedding* into `i64`, whose image is a *gapped (non-contiguous) subset*; + /// decoding is a *partial function* on `i64`. + /// + /// This means not all `i64` values are valid DateTime values, + /// but we can trivially compare them as `i64` as the values + /// compare naturally (earlier points in time are smaller ints). + bitstruct DateTime : i64 { + /// The number of milliseconds inside the encoded day. + /// + /// RANGE: 0 to 86,399,999 + /// + /// LORE: Millisecond precision was chosen as it's the smallest + /// discrete time step in SI prefix units that fits into a u32 + /// value. + field milliseconds_of_day: u32; + + /// The number of days since the epoch, which is the `2000-01-01`. + /// + /// RANGE: 2000-01-01 = 0 + /// + /// LORE: The epoch was chosen to be the first January of 2000 as for + /// a non-UNIX timestamp system it doesn't necessarily make sense + /// to use the same epoch. + /// Using 2000 as the base year is kinda fun though, as it's a leap + /// year. + field days_since_epoch: i32; + } + + /// Returns the current date/time value. + /// + /// NOTE: The value returned by `now` may not be steady nor continuous. + /// As the wall clock can be adjusted by `set`, the value returned + /// by `now` can change abruptly, both in negative and positive + /// direction. + /// + /// NOTE: Precision of timing depends on the current hardware, + /// but should always be at least in seconds precision. + /// + /// NOTE: During a leap second adjustment, the final millisecond of a day + /// (23:59:59.999) may be extended, so repeated calls to `now` may + /// return the same `DateTime` value for longer than 1 ms. + syscall now { + /// The current date and time of the system. + out dt: DateTime; + } + + /// Updates the system's current date/time value. + /// + /// NOTE: Invoking `set` will make the value returned by `now` immediately jump + /// to the newly set value. + syscall set { + /// The new date and time of the system. + in dt: DateTime; + + /// Returned when `dt` does not encode a valid DateTime. + error InvalidValue; + } + + /// Completes when `datetime.now() >= when`. + /// + /// NOTE: A call to `set` may trigger all active alarm calls that + /// are now satisfied. + async_call Alarm { + /// Earliest possible date time of when the alarm triggers. + in when: DateTime; + + /// Returned when `when` does not encode a valid DateTime. + error InvalidValue; + } + + //? + //? Timezone Management + //? + + /// Gets the current offset between local time and UTC. + syscall get_timezone_offset + { + /// The offset of the local time to UTC in minutes. + /// + /// RANGE: -1440 - 1440 + /// + /// NOTE: In tzdata mode, this returns the offset that applies at `datetime.now()` + /// and may change over time as time zone rules change. + out minutes: i16; + } + + /// Sets the current offset between local time and UTC. + syscall set_timezone_offset + { + /// The offset of the local time to UTC in minutes. + /// + /// RANGE: -1440 - 1440 + in minutes: i16; + + /// The provided 'minutes' offset was not in the legal range. + error InvalidZoneOffset; + } + + /// Loads a "timezone data" file according to the [tz database](https://en.wikipedia.org/wiki/Tz_database). + /// + /// This allows setting the automatic update of the current local time zone offset according to + /// the rules encoded in the timezone data. + /// + /// LORE: The decision to use tzdata was pretty simple: It's a standardized format + /// that has proven over time and solves pretty much all of our issues already + /// in a good way. + syscall load_timezone_data + { + /// The binary TZif blob of the timezone data file. + /// + /// NOTE: [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536) specifies the + /// format accepted by this syscall. + /// + /// NOTE: TODO: Specify exact supported version and features of TZif. + in data: bytestr; + + /// The system is out of resources and cannot load the timezone data. + error SystemResources; + + /// The data is not a valid timezone data file. + error InvalidData; + } + + /// Queries the timezone offset for a given point in time. + /// + /// NOTE: This function returns either: + /// - the fixed manual offset set via `set_timezone_offset` (Manual mode), or + /// - the `dt`-dependent offset determined from tzdata (Tzdata mode). + syscall get_timezone_offset_at + { + /// The point in time to query the zone offset for. + in dt: DateTime; + + /// The local time offset in minutes. + out minutes: i16; + + /// Returned when `dt` does not encode a valid DateTime. + error InvalidValue; + } + + //? + //? Gregorian Calendar APIs + //? + + /// A structure encoding a date in the [Proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar), + /// which means it can encode dates prior to 1582. + /// + /// LORE: This structure isn't a necessity for the kernel API, but is introduced + /// as a way of saving code size and memory, as most western applications + /// will use the Gregorian calendar, so it makes sense to share the implementation + /// for a conversion from/to `DateTime` in the kernel. + struct GregorianDate { + /// The astronomical year of the date. + /// NOTE: This means that `year = 0` means 1 BCE. + /// RANGE: -32768 - 32767 + field year: i16; + + /// RANGE: 1-12 + field month: u8; + + /// RANGE: 1-31 + field day: u8; + + /// RANGE: 0-23 + field hour: u8; + + /// RANGE: 0-59 + field minute: u8; + + /// RANGE: 0-59 + /// NOTE: 60 is *not allowed*. See the note on `datetime` for more information. + field second: u8; + + /// RANGE: 0-999 + field millis: u16; + } + + /// Converts a gregorian date into a DateTime. + syscall from_gregorian { + /// The gregorian date that shall be converted into a date time. + in gregorian: GregorianDate; + + /// The offset to UTC in minutes for the date. + /// NOTE: Use 0 for UTC. + /// RANGE: -1440 - 1440 + in local_offset: i16; + + /// The resulting datetime value for `gregorian`. + out dt: DateTime; + + /// The gregorian date contains an invalidly specified date. + error InvalidValue; + + /// The local time zone offset is not in the legal range. + error InvalidZoneOffset; + } + + /// Converts a DateTime value into a gregorian date. + syscall to_gregorian { + /// The DateTime value that shall be converted into a gregorian date. + in dt: DateTime; + + /// The offset to UTC in minutes for the date. + /// NOTE: Use 0 for UTC. + /// RANGE: -1440 - 1440 + in local_offset: i16; + + /// The resulting gregorian date. + out gregorian: GregorianDate; + + /// The date/time value contains an invalid value. + error InvalidValue; + + /// The local time zone offset is not in the legal range. + error InvalidZoneOffset; + + /// `dt` points to a year not representable by `GregorianDate`. + error OutOfRange; + } + + /// Converts a date/time value into the current local gregorian date. + /// + /// NOTE: This function utilizes the current time zone information and + /// may have a dynamic offset to UTC. + syscall to_gregorian_local { + /// The DateTime value that shall be converted into a gregorian date. + in dt: DateTime; + + /// The local date. + out gregorian: GregorianDate; + + /// The date/time value contains an invalid value. + error InvalidValue; + + /// `dt` points to a year not representable by `GregorianDate`. + error OutOfRange; + } + + /// Converts a local gregorian date/time into a generic date/time value. + /// + /// NOTE: This function utilizes the current time zone information and + /// may have a dynamic offset to UTC. + /// + /// NOTE: If the local time is non-existent, `occurrence` is ignored. + /// If the local time is ambiguous, `adjustment` is ignored. + syscall from_gregorian_local { + /// The gregorian date in the local time zone. + in gregorian: GregorianDate; + + /// How to handle a well-formed local wall clock time that cannot be mapped + /// to a UTC timestamp because it does not exist in the current time zone rules. + /// + /// NOTE: This may happen due to daylight saving time or similar rules. + in adjustment: MissingTimeAdjustment; + + /// How to resolve ambiguities when a wall clock time appears multiple times. + /// + /// NOTE: This may happen due to daylight saving time or similar rules. + in occurrence: DuplicateTimeOccurrence; + + /// The date/time value representing the given date. + out dt: DateTime; + + /// The gregorian date contains an invalidly specified date. + error InvalidValue; + + /// The gregorian time maps a non-existing wall clock time and + /// `adjustment` was `reject`. + error NonexistentLocalTime; + + /// The gregorian time maps an ambiguous wall clock time and + /// `occurrence` was `reject`. + error AmbiguousLocalTime; + } + + /// Enumeration of the variants how missing wall clock times will be resolved. + enum MissingTimeAdjustment : u8 { + /// The time is not adjusted, but rejected and yields an error. + item reject = 0; + + /// The time is adjusted to the first possible past point in time. + /// EXAMPLE: `02:30:00.000` is mapped to `01:59:59.999`. + item past = 1; + + /// The time is adjusted to the first possible future point in time. + /// EXAMPLE: `02:30:00.000` is mapped to `03:00:00.000`. + item future = 2; + + /// The time is adjusted to the closest possible point in time. + /// EXAMPLE: `02:29:00.000` is mapped to `01:59:59.999`. + /// EXAMPLE: `02:30:00.000` is mapped to `03:00:00.000`. + /// EXAMPLE: `02:31:00.000` is mapped to `03:00:00.000`. + item closer = 3; + } + + /// Enumeration of the variants how a wall clock time that can occur multiple times + /// is handled. + enum DuplicateTimeOccurrence : u8 { + /// The time is not adjusted, but rejected and yields an error. + item reject = 0; + + /// If the time is ambiguous, assume the earlier variant. + /// EXAMPLE: `02:30` is 2.5 hours past midnight. + item earlier = 1; + + /// If the time is ambiguous, assume the later variant. + /// EXAMPLE: `02:30` is 3.5 hours past midnight. + item later = 2; + } +} + +/// This namespace contains items related to presenting video data. +namespace video { + /// Index of the systems video outputs. + enum VideoOutputID : u8 { + /// The primary video output + item primary = 0; + ... + } + + /// Returns a list of all video outputs. + /// + /// If `ids` is `null`, the total number of available outputs is returned; + /// otherwise, up to `ids.len` elements are written into the provided array + /// and the number of written elements is returned. + syscall enumerate { + in ids: ?[]VideoOutputID; + out count: usize; + } + + /// The video output resource is an exclusive access token to a + /// video output. + /// + /// It allows updating the displayed pixel data and waiting for the + /// vertical blanking interval of the display data. + resource VideoOutput { } + + /// Acquire exclusive access to a video output. + syscall acquire { + in output_id: VideoOutputID; + + /// The resource created from `output_id`. + out output: VideoOutput; + + /// Exclusive access is already held for the video output identified by `output_id`. + error AlreadyExists; + + /// `output_id` is not a valid video output id. + error InvalidId; + + error SystemResources; + } + + /// Returns the resolution of `output` in pixels. + syscall get_resolution { + in output: VideoOutput; + + out resolution: Size; + + /// `output` is not a valid video output resource. + error InvalidHandle; + } + + /// Completes when the video output has fully scanned out an image and is now performing the v-blanking. + /// + /// This allows frame-synchronized presentation of video data. + /// + /// NOTE: All scheduled `WaitForVBlank` operations complete at the start of the next vertical blanking period. + /// + /// This means that a schedule during the current vertical blanking period does not immediately complete + /// the operation, but delays by nearly a full frame. + async_call WaitForVBlank { + in output: VideoOutput; + + /// `output` is not a valid video output resource. + error InvalidHandle; + } + + /// Specifies how `WritePixels` will upload the pixels. + enum PresentMode : u8 { + /// The pixel data is written immediately. + /// + /// NOTE: This mode will immediately upload the pixel data and + /// will not await a vertical blanking period. This means the + /// upload is likely to create visual glitches or tearing. + item immediate = 0; + + /// The kernel attempts a tearing free upload of the pixel data. + /// + /// This means the kernel attempts to align the upload with the + /// vertical blanking period. + /// + /// NOTE: This mode is best-effort, and does not guarantee the + /// video data is uploaded tearing-free. + item vblank = 1; + } + + /// Uploads pixels to a video output. + /// + /// NOTE: If `destination` would update a zero-sized area (`width` or `height` is zero), + /// the operation is a no-op and completes immediately. + /// + /// LORE: Originally, we had the ability to directly get a pointer + /// to the video outputs buffer. + /// As convenient as it is, it implicitly imposed the requirement + /// for the kernel to potentially allocate a pixel buffer if the + /// video output cannot actually provide the video memory inside + /// the systems main memory. + /// + /// This forced the kernel to periodically upload an allocated buffer + /// to external video devices, which is both inefficient and error prone. + /// + /// This syscall + `Buffer` sidestep this problem by making the access of a + /// memory-mapped video memory fallible without removing the ability for a generic + /// upload procedure. + async_call WritePixels { + /// The output which should receive the pixel data. + in output: VideoOutput; + + /// The portion of the video buffer that should be updated. + in destination: Rectangle; + + /// Pointer to the top-left pixel of `destination`. + /// + /// NOTE: The order inside this array is row-major. + /// This means that `pixels[1]` is the pixel at `(destination.x + 1, destination.y)` + /// and `pixels[stride]` is the pixel at `(destination.x, destination.y + 1)`. + /// + /// NOTE: Each scanline starts at `y * stride` elements apart and the buffer must contain + /// at least `destination.height` scanlines. + in pixels: []const Color; + + /// The length of a scanline in `pixels` in elements. + in stride: usize; + + /// Determines when to perform the pixel data write. + in mode: PresentMode; + + /// `output` is not a valid video output resource. + error InvalidHandle; + + /// Returned when `pixels` does not hold enough pixels to update `destination`. + /// + /// This means that `pixels.len` is less than `stride * max(0, destination.height - 1) + destination.width`. + /// + /// NOTE: This error is only returned if `destination.height > 0`. + error BufferSize; + + /// `stride` is less than `destination.width`. + error InvalidStride; + + /// `destination` is outside the actual video buffer resolution. + error InvalidRegion; + } + + /// A buffer mapping provides a memory-mapped view into a + /// front- or backbuffer of a video output. + /// + /// This allows uploading pixel data without the need for a `WritePixels` operation. + /// + /// NOTE: Not every `VideoOutput` supports a buffer mapping. + resource BufferMapping { } + + enum BufferKind : u8 { + /// A front buffer uses the same data as the scanout mechanism. + /// This means that any write to this buffer is *directly* visible + /// as soon as the video output scans out the written pixel locations. + /// + /// NOTE: This means that writes may produce tearing or other visual + /// glitches. + /// + /// NOTE: `Present` is not required to make the changes visible. + item front_buffer = 0; + + /// A back buffer is a second buffer that is not used for scanning out + /// pixel data. + /// + /// This means that writes to a back buffer will never appear on the + /// video output unless the buffer is swapped/copied to the front buffer. + /// + /// To perform this copy/swap, the `Present` operation shall be used. + /// + /// NOTE: It is possible, but not recommended to perform a manual copy + /// from a back buffer mapping to a front buffer mapping. + item back_buffer = 1; + } + + /// Creates a memory mapping for the front or the back buffer of a video output. + /// + /// NOTE: Not every video output supports memory mappings at all. Some video outputs + /// only support a single mode of memory mapping. + /// + /// The supported combinations are: + /// - No mapping support. + /// - Only front buffer. + /// - Only back buffer. + /// - Both front and back buffer. + /// + /// When a buffer type is not supported, `Unsupported` is returned. + /// + /// NOTE: There can be only a single mapping for the front and the back buffer. + /// This means for each video output, a maximum of two `BufferMapping` resources + /// can exist. + /// + /// NOTE: A buffer mapping is implicitly destroyed when its associated video output is + /// destroyed. This is necessary as the destruction of the video output resource + /// revokes access to the video device, and thus also revokes access through memory + /// mappings. + syscall create_buffer_mapping { + in output: VideoOutput; + + /// Which buffer should be mapped. + in requested_kind: BufferKind; + + out buffer: BufferMapping; + + /// `output` is not a valid video output resource. + error InvalidHandle; + + /// The requested buffer type is not supported by the `output` device. + error Unsupported; + + /// A buffer mapping for the `requested_kind` of the video output + /// already exists. + error AlreadyExists; + + error SystemResources; + } + + /// Applies the changes inside `buffer` and guarantees they + /// are visible afterwards. + /// + /// NOTE: For a front buffer, no data movement will happen, but + /// `mode` may still make `Present` await the next vertical blanking + /// period. + /// + /// NOTE: It is not specified if a `Present` for a back buffer is performing a + /// buffer swap operation or a buffer copy operation. + /// + /// NOTE: If `mode == PresentMode.immediate` and `buffer` is a front buffer, the + /// operation completes immediately. + async_call Present + { + /// The buffer mapping that shall be presented. + in buffer: BufferMapping; + + /// Determines when to perform the pixel data update. + in mode: PresentMode; + + /// `buffer` is not a valid buffer mapping resource. + error InvalidHandle; + } + + /// A descriptor of memory-accessible pixel buffer. + /// + /// It is laid out row-major and `base[0]` is the top-left pixel + /// of the mapped image. + struct VideoMemory { + /// Pointer to the first pixel of the first scanline. + /// + /// Each scanline is `.stride` elements separated from + /// each other and contains `width` valid elements. + /// + /// There are `height` total scanlines available. + field base: [*]align(4) Color; + + /// Length of a scanline in elements. + field stride: usize; + + /// Number of valid elements in a scanline + field width: u16; + + /// Number of valid scanlines. + field height: u16; + } + + /// Returns a pointer to linear video memory, row-major. + /// + /// NOTE: The pointer inside `memory` is only valid until the next `Present` operation + /// for any front or back buffer mapping for the associated video output or until + /// the buffer mapping is destroyed. + /// + /// This requires careful management and it is not recommended to share different + /// `BufferMapping` resources with other actors. + syscall get_video_memory { + in buffer: BufferMapping; + + /// The descriptor of the memory mapped video buffer. + out memory: VideoMemory; + + /// `buffer` is not a valid buffer mapping resource. + error InvalidHandle; + } +} + +//? TODO: Review this namespace. +namespace audio { +//? TODO: Write this namespace. +//? +//? Your primary interface for audio streams is the ability to enqueue PCM/MIDI/ChipWrites at certain sample positions relative to your stream start. +//? +//? Later PCM schedules stop previous PCMs at exactly that sample, so only a single PCM data stream is active per logical audio stream. +//? MIDI should be obvious. +//? ChipWrites implements native support for audio chips like a MOS 6581 or AY-3-8910 where you can sample-precisely schedule register +//? writes to your audio chips to create multi-channel-multi-tier audio creations. +} + +/// This namespace contains items related to entropy management. +namespace random { + /// Fills `data` with random bytes. + /// + /// This call never waits. Bytes are generated from the kernel DRBG. + /// If the DRBG cannot be (re)seeded due to insufficient newly collected entropy, + /// output is still produced (possibly without reseeding). + /// + /// NOTE: Do not use this for key generation unless the system guarantees + /// the DRBG has been seeded at least once (see `GetStrictRandom`). + syscall get_soft_random { + /// The buffer that should be filled with random bytes. + in data: bytebuf; + } + + /// Fills `data` with random bytes, but only after the kernel DRBG is seeded. + /// + /// This async call completes once the entropy pool reached the minimum seeding + /// threshold, then generates bytes from the kernel DRBG. + /// + /// NOTE: May take a substantial amount of time on systems with weak entropy sources. + async_call GetStrictRandom { + /// The buffer that should be filled with random bytes. + in data: bytebuf; + } + + //? TODO: add "add entropy" syscall +} + +/// +/// Input devices, input groups, and input event delivery. +/// +/// The kernel exposes input in two ways: +/// - **Input groups**: loss-tolerant, bounded, strictly ordered queues. +/// - **Device waits**: edge-triggered fanout waits that do not buffer and may be lossy. +/// +/// LORE: Input groups exist so userland can define *which* devices it wants to consume, +/// while still retaining a strong ordering across multiple devices inside that group. +/// The previous "global merged queue" model made it hard to correctly split input +/// between unrelated consumers. +/// +/// +/// Event fusing rules: +/// +/// LORE: To reduce event pressure and avoid jitter, the kernel may fuse continuous events inside +/// group queues without changing the final reconstructed state. +/// +/// Rules: +/// - Only continuous events may be fused (e.g. relative motion, absolute motion, wheel, analog axis). +/// - Discrete events are never fused (e.g. key press/release, button press/release, text input). +/// - Fusing never combines different devices and never combines different event types. +/// - Fusing occurs only inside group queues and does not add a fixed input latency. +/// - A fused event uses the timestamp of the last fused constituent. +/// +//? TODO: Expose/standardize the exact fusing window (e.g. ~40ms) if userland ever needs it. +namespace input { + //? TODO: Add system call to upload a new potential keyboard layout. + + /// + /// Opaque identifier for an input device. + /// + /// Device ids are allocated by the kernel when devices are added (system start or hotplug) + /// and are dropped when the device is removed/unplugged. + /// + /// The ids are not assigned in a stable manner, this means the same device will receive a + /// different device id if removed and readded later. Also kernel enumeration at system + /// start has no specified order and devices will not have a stable id. + /// + /// NOTE: A `DeviceId` obtained from `enumerate_devices` is guaranteed to be valid only + /// until the calling thread yields to the scheduler. + /// After a yield, any syscall using that `DeviceId` may fail with `InvalidDevice` when + /// the device was removed. + /// + /// NOTE: Devices are not system resources. They do not have ownership semantics. + /// + /// NOTE: Device ids are allocated by the kernel in a monotonic manner, so it takes around + /// 4 billion plug/unplug operations until a device id is reused again. + /// This will take a while. + /// + /// LORE: It doesn't make sense to handle devices as system resources as they can be + /// potentially removed at runtime by external means and holding such a resource + /// afterwards would make the resource invalid anyways. Destroying a device resource + /// would also have no semantic meaning, as the physical device would still be plugged + /// into the system. + /// + enum DeviceId : u32 { + /// Special value used when an event has no originating device. + /// + /// NOTE: This value is only used as `InputEvent.device` for group-injected events. + /// It is not a valid target for `GetDeviceEvent` or `emit_device_event`. + item synthetic = 0; + + ... + } + + /// Enumerates all currently available input devices. + /// + /// If `ids` is `null`, the total number of available devices is returned; + /// otherwise, up to `ids.len` elements are written into the provided array + /// and the number of written elements is returned. + /// + /// NOTE: Returned ids are only guaranteed to be valid until the calling thread yields. + syscall enumerate_devices { + in ids: ?[]DeviceId; + out count: usize; + } + + + /// Describes the broad class of an input device. + enum DeviceClass : u8 { + item unknown = 0; + item keyboard = 1; + item mouse = 2; + item gamepad = 3; + item joystick = 4; + item @"3d_mouse" = 5; + ... + } + + /// Describes the transport/protocol of an input device. + enum DeviceProtocol : u8 { + item unknown = 0; + item usb = 1; + item bluetooth = 2; + item serial = 3; + item bitbang = 4; + item network = 5; + ... + } + + /// Capability flags of an input device. + bitstruct DeviceCapabilities : u16 { + /// Device can emit HID-style key usage codes. + field keys: bool; + + /// Device provides relative pointer motion events. + field rel_pointer: bool; + + /// Device provides absolute pointer position events. + field abs_pointer: bool; + + reserve u13 = 0; + } + + /// A structure describing an input device. + struct DeviceDescriptor { + field class: DeviceClass; + field protocol: DeviceProtocol; + field capabilities: DeviceCapabilities; + + /// Number of relative analog axes. + /// + /// NOTE: This includes axes like accelerometer axes, which have zero + /// output at rest, and only emit data when changed. + /// Same rate of change = Same value. + field rel_axes_cnt: u16; + + /// Number of absolute analog axes. + /// + /// NOTE: The value for these axes will be normalized by the kernel. + /// + /// NOTE: This includes axes like joysticks which have a zero position, + /// only change their reported value in a reproducible manner. + /// Same position = Same value. + field abs_axes_cnt: u16; + + /// Number of non-keyboard digital buttons the device provides. + /// + /// NOTE: This includes buttons like A/B/X/Y or Start/Select. + field digital_button_count: u16; + + /// The vendor id of the device. + /// NOTE: This value shall be interpreted depending on `protocol`. + /// NOTE: `vendor_id` may be set to `0xFFFF` if not applicable. + field vendor_id: u16; + + /// The product id of the device. + /// NOTE: This value shall be interpreted depending on `protocol`. + /// NOTE: `product_id` may be set to `0xFFFF` if not applicable. + field product_id: u16; + } + + /// Queries metadata about an input device. + /// + /// If `name_buf` is `null`, no name is written but `name_len` is still returned. + /// If `unique_id_buf` is `null`, no unique id is written but `unique_id_len` is still returned. + /// + /// NOTE: `unique_id` is an optional, implementation-defined identifier that can be used by + /// applications to recognize devices again across hotplug. + /// + /// It may be empty if the kernel cannot provide one. + /// + syscall query_device_metadata { + in id: DeviceId; + in name_buf: ?[]u8; + in unique_id_buf: ?[]u8; + + /// If not `null`, the kernel will fill this structure with metadata for the device. + in descriptor: ?*DeviceDescriptor; + + out name_len: usize; + out unique_id_len: usize; + + /// `id` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; + } + + + /// Waits for the next event emitted by a specific device. + /// + /// This operation is **edge-triggered**: + /// - it does not buffer events, + /// - it may be lossy under high pressure, + /// - if multiple events occur between yields, intermediate events may be missed. + /// + /// NOTE: Any number of concurrent `GetDeviceEvent` operations may be scheduled for the + /// same device; they will all complete with the same next event. + /// A subsequent device event requires re-scheduling a new `GetDeviceEvent`. + /// + /// NOTE: If the device is removed for a pending `GetDeviceEvent` operation, it is + /// aborted with `error.Cancelled`. + async_call GetDeviceEvent { + in device: DeviceId; + out event: InputEvent; + + /// `device` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; + + /// `device` is `DeviceId.synthetic`. + error BadDevice; + } + + /// Emits a synthetic event *as if a real device had emitted it*. + /// + /// This updates the internal device state immediately and completes pending `GetDeviceEvent` + /// operations for that device. + /// + /// The kernel sets: + /// - `InputEvent.device = device` + /// - `InputEvent.flags.synthetic = true` + /// - `InputEvent.timestamp = clock.now()` (implementation-defined moment during the syscall) + /// + /// NOTE: This operation does not depend on any input groups existing. + /// If the device is present in groups, the event is enqueued into those group queues. + syscall emit_device_event { + in device: DeviceId; + in payload: InputEventPayload; + + /// `device` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; + + /// `device` is `DeviceId.synthetic`. + error BadDevice; + } + + /// Queries the current state of a device for a batch of `queries`. + /// + /// The kernel fills `queries[i].value` for each entry. + /// + /// NOTE: Unsupported `which` values produce a sane default (0 / centered). + syscall query_device_state { + in device: DeviceId; + in queries: []StateQuery; + + /// `device` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; + + /// `queries[i].what` contains an unknown value. + error InvalidValue; + } + + /// A userland-owned input event queue which merges events from 0..n devices. + /// + /// The queue length is fixed at creation time. + /// + /// NOTE: An input group with zero devices may still receive events through + /// synthetic event injection. + /// + /// NOTE: If a device is removed, it is implicitly removed from all groups. + resource InputGroup { } + + /// Creates a new input group with a fixed event queue size. + /// + /// NOTE: The queue is bounded. If it becomes full, the kernel will drop the + /// oldest queued events so the newest events are kept intact. + /// + /// NOTE: Dropped events are reported through `GetEvent.dropped_since_last`. + syscall create_group { + /// Maximum number of events buffered by this group. + /// + /// NOTE: If `queue_size` is zero, an error will be returned. + in queue_size: usize; + + out group: InputGroup; + + /// `queue_size` is zero. + error InvalidValue; + + error SystemResources; + } + + /// Adds a device to an input group. + /// + /// NOTE: A device can participate in 0..n groups at the same time. + /// Each emitted device event is enqueued once into each group that contains the device. + syscall add_device { + in group: InputGroup; + in device: DeviceId; + + /// `group` is not a valid input group resource. + error InvalidHandle; + + /// `device` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; + } + + /// Removes a device from an input group. + /// + /// NOTE: Removing a device does not purge already queued events originating from that device. + /// Those events remain ordered relative to all other queued events. + syscall remove_device { + in group: InputGroup; + in device: DeviceId; + + /// `group` is not a valid input group resource. + error InvalidHandle; + + /// `device` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; + } + + + /// Enumerates the devices that are currently part of an input group. + /// + /// If `devices` is `null`, the total number of devices in the group is returned; + /// otherwise, up to `devices.len` elements are written into the provided array. + syscall enumerate_group_devices { + in group: InputGroup; + in devices: ?[]DeviceId; + out count: usize; + + /// `group` is not a valid input group resource. + error InvalidHandle; + } + + /// Enumerates the input groups that currently contain the given device. + /// + /// If `groups` is `null`, the total number of groups containing the device is returned; + /// otherwise, up to `groups.len` elements are written into the provided array. + /// + /// NOTE: This returns only groups that are visible to the calling process. + /// + /// NOTE: The group resources returned in `groups` will be bound to the calling process with + /// the `at_least_weak` bind operation to ensure access. + syscall enumerate_device_groups { + in device: DeviceId; + in groups: ?[]InputGroup; + out count: usize; + + /// `device` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; + + error SystemResources; + } + + /// Waits for the next queued event from an input group. + /// + /// The operation completes when: + /// - the group queue is non-empty, or + /// - a new event arrives for the group. + /// + /// NOTE: If events are already available, this operation completes immediately. + /// + /// NOTE: Only a single `GetEvent` operation may be scheduled per group at a time. + /// This enforces strict, non-duplicating consumption and preserves ordering. + /// + /// NOTE: The group maintains a drop counter which increments whenever the queue is full + /// and an event must be dropped. + /// Each completion returns the number of dropped events since the last successful + /// dequeue/completion and resets that counter to zero. + /// + /// LORE: The queue is "newest-wins": on overflow, oldest events are discarded so the most + /// recent user input remains available. + /// + /// LORE: The group state is updated only when an event *leaves* the queue: + /// - a regular head-pop (returned by `GetEvent`), or + /// - an overflow head-pop (dropped due to overflow). + /// Events that are merely queued do not affect group state. + /// + async_call GetEvent { + in group: InputGroup; + + out event: InputEvent; + + /// Number of events dropped since the last successful dequeue from this group. + out dropped_since_last: u32; + + /// `group` is not a valid input group handle. + error InvalidHandle; + + /// A `GetEvent` operation for `group` is already scheduled. + error NonExclusiveAccess; + } + + /// Pushes a synthetic event into a group. + /// + /// The injected event is appended to the back of the group queue (same ordering rule as + /// device-emitted events). The kernel sets: + /// - `InputEvent.device = DeviceId.synthetic` + /// - `InputEvent.flags.synthetic = true` + /// - `InputEvent.timestamp = clock.now()` + /// + /// NOTE: This operation is atomic: it either enqueues the event or returns an error. + /// + /// If `force` is `false`, the syscall fails with `Overflow` if enqueueing would drop + /// an event due to a full queue. + /// + /// If `force` is `true`, the syscall behaves like a hardware event with newest-wins overflow, + /// except it is marked synthetic. + syscall queue_event { + in group: InputGroup; + in payload: InputEventPayload; + in force: bool; + + /// `group` is not a valid input group resource. + error InvalidHandle; + + /// Returned when `force == false` and enqueueing would drop an existing queued event. + error Overflow; + } + + /// Queries the current fused/accumulated state of a group for a batch of `queries`. + /// + /// NOTE: The fused state includes only devices that can meaningfully contribute to the queried item. + /// Non-applicable devices are ignored. + /// + /// NOTE: Group state is updated only when events leave the queue (returned or dropped), + /// so userland state reconstruction from the event stream is equivalent unless events are dropped. + syscall query_group_state { + in group: InputGroup; + in queries: []StateQuery; + + /// `group` is not a valid input group resource. + error InvalidHandle; + + /// `queries[i].what` contains an unknown value. + error InvalidValue; + } + + /// A single state query item. + /// + /// The kernel reads `what` and `which`, and writes `value`. + /// + /// NOTE: All values are returned as i16: + /// - Digital inputs: 0 (inactive) or 1 (active) for a device; for a group, the sum of all + /// pressed contributors (so >1 is possible). + /// - Absolute axes: normalized [-32767..32767] for a device; for a group, summed and clamped. + /// + struct StateQuery { + /// Defines what kind of input should be queried. + field what: Item; + + /// Defines which instance of `what` should be queried. + field which: u16; + + /// Output value filled by the kernel. + field value: i16; + + /// Selects which state component is queried. + enum Item : u16 { + /// `which` is a `KeyUsageCode`. + item keyboard_key = 0; + + /// `which` is a `MouseButton` value. + item mouse_button = 1; + + /// `which` is a per-device button index (matches `InputEvent.Button.button`). + item control_button = 2; + + /// `which` is a per-device absolute axis index (matches `InputEvent.AbsAxis.axis`). + item abs_axis = 3; + + /// Absolute pointer X (normalized i16). `which` must be zero. + item pointer_x = 4; + + /// Absolute pointer Y (normalized i16). `which` must be zero. + item pointer_y = 5; + } + } + + /// Flags attached to an input event. + bitstruct EventFlags : u16 { + /// Set for events that did not originate from a device driver. + field synthetic: bool; + + reserve u15 = 0; + } + + /// An input event as delivered to userland. + struct InputEvent { + /// The type of event that was emitted. + field type: Type; + + /// Timestamp from the moment the kernel receives the event in its input subsystem. + /// + /// NOTE: Multiple events may share the same timestamp due to timer resolution and internal handling. + field timestamp: clock.Absolute; + + /// The originating device id or `DeviceId.synthetic` if none. + field device: DeviceId; + + /// Event flags. + field flags: EventFlags; + + /// The event payload. + field payload: Payload; + + enum Type : u16 { + item key_press = 0; + item key_release = 1; + + item mouse_rel_motion = 2; + item mouse_abs_motion = 3; + item mouse_button_press = 4; + item mouse_button_release = 5; + item mouse_wheel = 6; + + item digital_button_press = 7; + item digital_button_release = 8; + + item rel_axis_motion = 9; + item abs_axis_motion = 10; + + //? TODO: touch_down, touch_up, touch_move + + ... + } + + union Payload { + field keyboard: Keyboard; + + field mouse_rel_motion: MouseRelMotion; + field mouse_abs_motion: MouseAbsMotion; + field mouse_button: MouseButton; + field mouse_wheel: MouseWheel; + + field digital_button: Button; + + field rel_axis: RelAxis; + field abs_axis: AbsAxis; + } + + /// Relative motion delta in device units (implementation-defined). + /// + /// NOTE: Consecutive relative motion events may be fused inside group queues. + struct MouseRelMotion { + /// Relative position delta in the horizontal axis. + /// Positive values move to the right. + field dx: i16; + + /// Relative position delta in the vertical axis. + /// Positive values move downwards. + field dy: i16; + } + + /// Absolute pointer position on each axis in normalized i16: + /// - -32767 == -1.0 + /// - 0 == 0.0 + /// - 32767 == +1.0 + /// + /// NOTE: Consecutive absolute motion events may be fused inside group queues. + struct MouseAbsMotion { + /// Absolute position in the horizontal axis. + /// `-32767` is the left edge, `32767` is the right edge. + field x: i16; + + /// Absolute position in the vertical axis. + /// `-32767` is the top edge, `32767` is the bottom edge. + field y: i16; + } + + struct MouseButton { + /// Which mouse button was pressed/released. + field button: input.MouseButton; + } + + /// Wheel delta (implementation-defined units). + /// + /// NOTE: Consecutive wheel events in the same direction may be fused inside group queues. + struct MouseWheel { + field dx: i16; + field dy: i16; + } + + struct Keyboard { + /// The raw usage code for the key. Meaning depends on the layout; + /// kinda represents the physical position on the keyboard. + field usage: KeyUsageCode; + + /// If set, the pressed key combination has a mapping in the current + /// keyboard layout that produces text input. + /// + /// NOTE: This doesn't necessarily contain printable codes, but can also contain + /// combining characters like `U+0301` (Combining Acute Accent). + /// + /// NOTE: This isn't a true *composed* text input and cannot be directly used in a + /// text field or such. This is primarily meant to be passed into an input + /// method editor. + /// + /// NOTE: The lifetime of this pointer can be assumed valid until a keyboard layout + /// change is performed. + /// + /// LORE: This field isn't a perfect solution, but it's good enough for what we're trying to + /// achieve: International text input. + /// The idea of using combining characters for dead keys allows the IME to actually compose + /// a sequence of `U+0301` (Combining Acute Accent), `U+0041` (Latin Capital Letter A) to be composed + /// into `U+00C1` (Latin Capital Letter A With Acute) instead of emitting two codepoints. + /// + /// This method is flexible enough to be future-proof and extensible. + /// + field text: ?str; + + /// The modifier keys currently active + field modifiers: KeyboardModifiers; + } + + /// A non-keyboard digital button event (e.g. gamepad button). + /// + /// NOTE: `button` is an implementation-defined per-device index. + struct Button { + /// Defines which digital button was pressed/released. + field button: u16; + } + + /// An absolute analog axis event (e.g. joystick axis). + /// + /// NOTE: `axis` is an implementation-defined per-device index. + /// NOTE: `value` uses normalized i16: + /// -32767 == -1.0, 0 == 0.0, 32767 == +1.0 + struct AbsAxis { + /// Defines which axis has changed. + field axis: u16; + field value: i16; + } + + /// A relative analog axis event (e.g. accelerometer axis). + /// + /// NOTE: `axis` is an implementation-defined per-device index. + /// + /// NOTE: Consecutive relative axis events in the same direction may be fused inside group queues. + struct RelAxis { + /// Defines which axis has changed. + field axis: u16; + field delta: i16; + } + } + + enum MouseButton : u8 { + item none = 0; + item left = 1; + item right = 2; + item middle = 3; + item nav_previous = 4; + item nav_next = 5; + } + + /// Keyboard modifier state accompanying key events. + bitstruct KeyboardModifiers : u16 { + field shift: bool; + field alt: bool; + field ctrl: bool; + field gui: bool; + field shift_left: bool; + field shift_right: bool; + field ctrl_left: bool; + field ctrl_right: bool; + field alt_graph: bool; + field gui_left: bool; + field gui_right: bool; + reserve u5 = 0; + } + + /// + /// This is an enumeration of all well-known HID Keyboard/Keypad Page (0x07) usage codes for + /// keys. + /// + /// NOTE: These codes do not necessarily correlate with what's printed on the key, but what's + /// printed on the same location of a typical US layout keyboard. + /// Use key usage codes for when you're interested in the *location* of a key, not the + /// its semantic meaning. + /// For example, the typical `WASD` input scheme would be `ZQSD` on an AZERTY keyboard, but + /// the locations would be the same. + /// + /// LORE: This mapping was chosen as it's the most widespread standard key list. These codes + /// are directly produced by both USB and Bluetooth keyboards and don't require any translation + /// in these cases. Also HID is a widespread standard. + /// + /// NOTE: The notes in this enumeration are taken verbatim from + /// [HID Usage Tables, Version 1.6, Keyboard/Keypad Page (0x07)](https://usb.org/sites/default/files/hut1_6.pdf). + /// + enum KeyUsageCode : u16 { + //? 01 Keyboard ErrorRollOver + //? 02 Keyboard POSTFail + //? 03 Keyboard ErrorUndefined + + /// Keyboard `a` and `A` + /// NOTE: Typically remapped for other languages in the host system. + item a = 0x04; + + /// Keyboard `b` and `B` + item b = 0x05; + + /// Keyboard `c` and `C` + /// NOTE: Typically remapped for other languages in the host system. + item c = 0x06; + + /// Keyboard `d` and `D` + item d = 0x07; + + /// Keyboard `e` and `E` + item e = 0x08; + + /// Keyboard `f` and `F` + item f = 0x09; + + /// Keyboard `g` and `G` + item g = 0x0A; + + /// Keyboard `h` and `H` + item h = 0x0B; + + /// Keyboard `i` and `I` + item i = 0x0C; + + /// Keyboard `j` and `J` + item j = 0x0D; + + /// Keyboard `k` and `K` + item k = 0x0E; + + /// Keyboard `l` and `L` + item l = 0x0F; + + /// Keyboard `m` and `M` + /// NOTE: Typically remapped for other languages in the host system. + item m = 0x10; + + /// Keyboard `n` and `N` + item n = 0x11; + + /// Keyboard `o` and `O` + /// NOTE: Typically remapped for other languages in the host system. + item o = 0x12; + + /// Keyboard `p` and `P` + /// NOTE: Typically remapped for other languages in the host system. + item p = 0x13; + + /// Keyboard `q` and `Q` + /// NOTE: Typically remapped for other languages in the host system. + item q = 0x14; + + /// Keyboard `r` and `R` + item r = 0x15; + + /// Keyboard `s` and `S` + item s = 0x16; + + /// Keyboard `t` and `T` + item t = 0x17; + + /// Keyboard `u` and `U` + item u = 0x18; + + /// Keyboard `v` and `V` + item v = 0x19; + + /// Keyboard `w` and `W` + /// NOTE: Typically remapped for other languages in the host system. + item w = 0x1A; + + /// Keyboard `x` and `X` + /// NOTE: Typically remapped for other languages in the host system. + item x = 0x1B; + + /// Keyboard `y` and `Y` + /// NOTE: Typically remapped for other languages in the host system. + item y = 0x1C; + + /// Keyboard `z` and `Z` + /// NOTE: Typically remapped for other languages in the host system. + item z = 0x1D; + + + /// Keyboard `1` and `!` + /// NOTE: Typically remapped for other languages in the host system. + item @"1" = 0x1E; + + /// Keyboard `2` and `@` + /// NOTE: Typically remapped for other languages in the host system. + item @"2" = 0x1F; + + /// Keyboard `3` and `#` + /// NOTE: Typically remapped for other languages in the host system. + item @"3" = 0x20; + + /// Keyboard `4` and `$` + /// NOTE: Typically remapped for other languages in the host system. + item @"4" = 0x21; + + /// Keyboard `5` and `%` + /// NOTE: Typically remapped for other languages in the host system. + item @"5" = 0x22; + + /// Keyboard `6` and `^` + /// NOTE: Typically remapped for other languages in the host system. + item @"6" = 0x23; + + /// Keyboard `7` and `&` + /// NOTE: Typically remapped for other languages in the host system. + item @"7" = 0x24; + + /// Keyboard `8` and `*` + /// NOTE: Typically remapped for other languages in the host system. + item @"8" = 0x25; + + /// Keyboard `9` and `(` + /// NOTE: Typically remapped for other languages in the host system. + item @"9" = 0x26; + + /// Keyboard `0` and `)` + /// NOTE: Typically remapped for other languages in the host system. + item @"0" = 0x27; + + /// Keyboard Return (ENTER) + item enter = 0x28; + + /// Keyboard ESCAPE + item escape = 0x29; + + /// Keyboard DELETE (Backspace) + /// NOTE: Backs up the cursor one position, deleting a character as it goes. + item backspace = 0x2A; + + /// Keyboard Tab + item tab = 0x2B; + + /// Keyboard Spacebar + item space = 0x2C; + + /// Keyboard `-` and `_` + item minus = 0x2D; + + /// Keyboard `=` and `+` + /// NOTE: Typically remapped for other languages in the host system. + item equals = 0x2E; + + /// Keyboard `[` and `{` + /// NOTE: Typically remapped for other languages in the host system. + item square_bracket_open = 0x2F; + + /// Keyboard `]` and `}` + /// NOTE: Typically remapped for other languages in the host system. + item square_bracket_close = 0x30; + + /// Keyboard `\\` and `|` + /// NOTE: Typically remapped for other languages in the host system. + item backslash = 0x31; + + /// Keyboard Non-US `#` and `~` + /// NOTE: Typical language mappings: + /// US: `\` `|` + /// Belg: `µ` `\`` `£` + /// French Canadian: `<` `}` `>` + /// Danish: `'` `*` + /// Dutch: `<` `>` + /// French: `*` `µ` + /// German: `#` `'` + /// Italian: `ù` `§` + /// LatinAmerica: `}` `\`` `]` + /// Norwegian: `,` `*` + /// Spain: `}` `Ç` + /// Swedish: `,` `*` + /// Swiss: `$`, `£` + /// UK: `#` `~` + item non_us_hash = 0x32; + + /// Keyboard `;` and `:` + /// NOTE: Typically remapped for other languages in the host system. + item semicolon = 0x33; + + /// Keyboard `'` and `"` + /// NOTE: Typically remapped for other languages in the host system. + item apostrophe = 0x34; + + /// Keyboard Grave Accent (`\``) and Tilde (`~`) + /// NOTE: Typically remapped for other languages in the host system. + item grave_accent = 0x35; + + /// Keyboard `,` and `<` + /// NOTE: Typically remapped for other languages in the host system. + item comma = 0x36; + + /// Keyboard `.` and `>` + /// NOTE: Typically remapped for other languages in the host system. + item period = 0x37; + + /// Keyboard `/` and `?` + /// NOTE: Typically remapped for other languages in the host system. + item slash = 0x38; + + /// Keyboard Caps Lock + /// NOTE: Implemented as a non-locking key; sent as member of an array. + item caps_lock = 0x39; + + /// Keyboard F1 + item f1 = 0x3A; + + /// Keyboard F2 + item f2 = 0x3B; + + /// Keyboard F3 + item f3 = 0x3C; + + /// Keyboard F4 + item f4 = 0x3D; + + /// Keyboard F5 + item f5 = 0x3E; + + /// Keyboard F6 + item f6 = 0x3F; + + /// Keyboard F7 + item f7 = 0x40; + + /// Keyboard F8 + item f8 = 0x41; + + /// Keyboard F9 + item f9 = 0x42; + + /// Keyboard F10 + item f10 = 0x43; + + /// Keyboard F11 + item f11 = 0x44; + + /// Keyboard F12 + item f12 = 0x45; + + /// Keyboard PrintScreen + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item print_screen = 0x46; + + /// Keyboard Scroll Lock + /// NOTE: Implemented as a non-locking key; sent as member of an array. + item scroll_lock = 0x47; + + /// Keyboard Pause + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item pause = 0x48; + + /// Keyboard Insert + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item insert = 0x49; + + /// Keyboard Home + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item home = 0x4A; + + /// Keyboard PageUp + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item page_up = 0x4B; + + /// Keyboard Delete Forward + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + /// NOTE: Deletes one character without changing position. + item delete = 0x4C; + + /// Keyboard End + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item end = 0x4D; + + /// Keyboard PageDown + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item page_down = 0x4E; + + /// Keyboard RightArrow + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item right_arrow = 0x4F; + + /// Keyboard LeftArrow + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item left_arrow = 0x50; + + /// Keyboard DownArrow + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item down_arrow = 0x51; + + /// Keyboard UpArrow + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item up_arrow = 0x52; + + /// Keypad Num Lock and Clear + /// NOTE: Implemented as a non-locking key; sent as member of an array. + item num_lock = 0x53; + + /// Keypad `/` + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item kp_divide = 0x54; + + /// Keypad `*` + item kp_multiply = 0x55; + + /// Keypad `-` + item kp_subtract = 0x56; + + /// Keypad `+` + item kp_add = 0x57; + + /// Keypad ENTER + item kp_enter = 0x58; + + /// Keypad `1` and End + item kp_1 = 0x59; + + /// Keypad `2` and Down Arrow + item kp_2 = 0x5A; + + /// Keypad `3` and PageDn + item kp_3 = 0x5B; + + /// Keypad `4` and Left Arrow + item kp_4 = 0x5C; + + /// Keypad `5` + item kp_5 = 0x5D; + + /// Keypad `6` and Right Arrow + item kp_6 = 0x5E; + + /// Keypad `7` and Home + item kp_7 = 0x5F; + + /// Keypad `8` and Up Arrow + item kp_8 = 0x60; + + /// Keypad `9` and PageUp + item kp_9 = 0x61; + + /// Keypad `0` and Insert + item kp_0 = 0x62; + + /// Keypad `.` and Delete + item kp_period = 0x63; + + /// Keyboard Non-US `\\` and `|` + /// NOTE: Typical language mappings: + /// Belg: `<` `\` `>` + /// French Canadian: `<` `°` `>` + /// Danish: `<` `\` `>` + /// Dutch: `]` `|` `[` + /// French: `<` `>` + /// German: `<` `|` `>` + /// Italian: `<` `>` + /// Latin America: `<` `>` + /// Norwegian: `<` `>` + /// Spain: `<` `>` + /// Swedish: `<` `|` `>` + /// Swiss: `<` `>` + /// UK: `\` `|` + /// Brazil: `\` `|` + /// NOTE: Typically near the Left-Shift key in AT-102 implementations. + item non_us_backslash = 0x64; + + /// Keyboard Application + /// NOTE: Windows key for Windows 95, and Compose. + item application = 0x65; + + /// Keyboard Power + item power = 0x66; + + /// Keypad `=` + item kp_equals = 0x67; + + /// Keyboard F13 + item f13 = 0x68; + + /// Keyboard F14 + item f14 = 0x69; + + /// Keyboard F15 + item f15 = 0x6A; + + /// Keyboard F16 + item f16 = 0x6B; + + /// Keyboard F17 + item f17 = 0x6C; + + /// Keyboard F18 + item f18 = 0x6D; + + /// Keyboard F19 + item f19 = 0x6E; + + /// Keyboard F20 + item f20 = 0x6F; + + /// Keyboard F21 + item f21 = 0x70; + + /// Keyboard F22 + item f22 = 0x71; + + /// Keyboard F23 + item f23 = 0x72; + + /// Keyboard F24 + item f24 = 0x73; + + /// Keyboard Execute + item execute = 0x74; + + /// Keyboard Help + item help = 0x75; + + /// Keyboard Menu + item menu = 0x76; + + /// Keyboard Select + item select = 0x77; + + /// Keyboard Stop + item stop = 0x78; + + /// Keyboard Again + item again = 0x79; + + /// Keyboard Undo + item undo = 0x7A; + + /// Keyboard Cut + item cut = 0x7B; + + /// Keyboard Copy + item copy = 0x7C; + + /// Keyboard Paste + item paste = 0x7D; + + /// Keyboard Find + item find = 0x7E; + + /// Keyboard Mute + item mute = 0x7F; + + /// Keyboard Volume Up + item volume_up = 0x80; + + /// Keyboard Volume Down + item volume_down = 0x81; + + /// Keyboard Locking Caps Lock + /// NOTE: Implemented as a locking key; sent as a toggle button. + /// Available for legacy support; however, most systems should use the non-locking version of this key + item locking_caps_lock = 0x82; + + /// Keyboard Locking Num Lock + /// NOTE: Implemented as a locking key; sent as a toggle button. + /// Available for legacy support; however, most systems should use the non-locking version of this key + item locking_num_lock = 0x83; + + /// Keyboard Locking Scroll Lock + /// NOTE: Implemented as a locking key; sent as a toggle button. + /// Available for legacy support; however, most systems should use the non-locking version of this key + item locking_scroll_lock = 0x84; + + /// Keypad Comma + /// NOTE: Keypad Comma is the appropriate usage for the Brazilian keypad period (`.`) key. + /// This represents the closest possible match, and system software should do the correct + /// mapping based on the current locale setting. + item kp_comma = 0x85; + + /// Keypad Equal Sign + /// NOTE: Used on AS/400 keyboards. + item kp_equals_as400 = 0x86; + + + /// Keyboard International1 + /// NOTE: Keyboard International1 should be identified via footnote as the appropriate usage for the Brazilian + /// forward-slash (`/`) and question-mark (`?`) key. + /// This usage should also be renamed to either "Keyboard Non-US `/` and `?`" or to "Keyboard International1" + /// now that it's become clear that it does not only apply to Kanji keyboards anymore. + item international1 = 0x87; + + /// Keyboard International2 + item international2 = 0x88; + + /// Keyboard International3 + item international3 = 0x89; + + /// Keyboard International4 + item international4 = 0x8A; + + /// Keyboard International5 + item international5 = 0x8B; + + /// Keyboard International6 + item international6 = 0x8C; + + /// Keyboard International7 + /// NOTE: Toggle Double-Byte/Single-Byte mode + item international7 = 0x8D; + + /// Keyboard International8 + /// NOTE: Undefined, available for other Front End Language Processors. + item international8 = 0x8E; + + /// Keyboard International9 + /// NOTE: Undefined, available for other Front End Language Processors. + item international9 = 0x8F; + + /// Keyboard LANG1 + /// NOTE: Hangul/English toggle key. This usage is used as an input method editor control key on a Korean language keyboard. + item lang1 = 0x90; + + /// Keyboard LANG2 + /// NOTE: Hanja conversion key. This usage is used as an input method editor control key on a Korean language keyboard. + item lang2 = 0x91; + + /// Keyboard LANG3 + /// NOTE: Defines the Katakana key for Japanese USB word-processing keyboards. + item lang3 = 0x92; + + /// Keyboard LANG4 + /// NOTE: Defines the Hiragana key for Japanese USB word-processing keyboards. + item lang4 = 0x93; + + /// Keyboard LANG5 + /// NOTE: Defines the Zenkaku/Hankaku key for Japanese USB word-processing keyboards. + item lang5 = 0x94; + + /// Keyboard LANG6 + /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. + item lang6 = 0x95; + + /// Keyboard LANG7 + /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. + item lang7 = 0x96; + + /// Keyboard LANG8 + /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. + item lang8 = 0x97; + + /// Keyboard LANG9 + /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. + item lang9 = 0x98; + + + + /// Keyboard Alternate Erase + /// NOTE: Example, Erase-Eaze™ key. + item alternate_erase = 0x99; + + /// Keyboard SysReq/Attention + /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. + /// That is, a key does not send extra codes to compensate for the state of any Control, + /// Alt, Shift or Num Lock keys. + item term_sysreq_attention = 0x9A; + + /// Keyboard Cancel + item term_cancel = 0x9B; + + /// Keyboard Clear + item term_clear = 0x9C; + + /// Keyboard Prior + item term_prior = 0x9D; + + /// Keyboard Return + item term_return = 0x9E; + + /// Keyboard Separator + item term_separator = 0x9F; + + /// Keyboard Out + item term_out = 0xA0; + + /// Keyboard Oper + item term_oper = 0xA1; + + /// Keyboard Clear/Again + item term_clear_again = 0xA2; + + /// Keyboard CrSel/Props + item term_crsel_props = 0xA3; + + /// Keyboard ExSel + item term_exsel = 0xA4; + + /// Keypad `00` + item kp_double_0 = 0xB0; + + /// Keypad `000` + item kp_triple_0 = 0xB1; + + /// Thousands Separator + /// NOTE: The symbol displayed will depend on the current locale settings + /// of the operating system. For example, the US thousands separator would + /// be a comma, and the decimal separator would be a period. + item kp_thousands_sep = 0xB2; + + /// Decimal Separator + /// NOTE: The symbol displayed will depend on the current locale settings + /// of the operating system. For example, the US thousands separator would + /// be a comma, and the decimal separator would be a period. + item kp_decimal_sep = 0xB3; + + /// Currency Unit + /// NOTE: The symbol displayed will depend on the current locale settings of the operating system. + /// For example the US currency unit would be $ and the sub-unit would be ¢. + item kp_currency_unit = 0xB4; + + /// Currency Sub-unit + /// NOTE: The symbol displayed will depend on the current locale settings of the operating system. + /// For example the US currency unit would be $ and the sub-unit would be ¢. + item kp_currency_subunit = 0xB5; + + /// Keypad `(` + item kp_round_bracket_open = 0xB6; + + /// Keypad `)` + item kp_round_bracket_close = 0xB7; + + /// Keypad `{` + item kp_curly_bracket_open = 0xB8; + + /// Keypad `}` + item kp_curly_bracket_close = 0xB9; + + /// Keypad Tab + item kp_tab = 0xBA; + + /// Keypad Backspace + item kp_backspace = 0xBB; + + /// Keypad `A` + item kp_a = 0xBC; + + /// Keypad `B` + item kp_b = 0xBD; + + /// Keypad `C` + item kp_c = 0xBE; + + /// Keypad `D` + item kp_d = 0xBF; + + /// Keypad `E` + item kp_e = 0xC0; + + /// Keypad `F` + item kp_f = 0xC1; + + /// Keypad XOR + item kp_logic_xor = 0xC2; + + /// Keypad `∧` + item kp_logic_and = 0xC3; + + /// Keypad % + item kp_percent = 0xC4; + + /// Keypad `<` + item kp_less_than = 0xC5; + + /// Keypad `>` + item kp_greater_than = 0xC6; + + /// Keypad `&` + item kp_ampersand = 0xC7; + + /// Keypad `&&` + item kp_double_ampersand = 0xC8; + + /// Keypad `|` + item kp_pipe = 0xC9; + + /// Keypad `||` + item kp_double_pipe = 0xCA; + + /// Keypad `:` + item kp_colon = 0xCB; + + /// Keypad `#` + item kp_hash = 0xCC; + + /// Keypad Space + item kp_space = 0xCD; + + /// Keypad `@` + item kp_at = 0xCE; + + /// Keypad `!` + item kp_exclamation = 0xCF; + + /// Keypad Memory Store + item kp_memory_store = 0xD0; + + /// Keypad Memory Recall + item kp_memory_recall = 0xD1; + + /// Keypad Memory Clear + item kp_memory_clear = 0xD2; + + /// Keypad Memory Add + item kp_memory_add = 0xD3; + + /// Keypad Memory Subtract + item kp_memory_subtract = 0xD4; + + /// Keypad Memory Multiply + item kp_memory_multiply = 0xD5; + + /// Keypad Memory Divide + item kp_memory_divide = 0xD6; + + /// Keypad `+/-` + item kp_plus_minus = 0xD7; + + /// Keypad Clear + item kp_clear = 0xD8; + + /// Keypad Clear Entry + item kp_clear_entry = 0xD9; + + /// Keypad Binary + item kp_binary = 0xDA; + + /// Keypad Octal + item kp_octal = 0xDB; + + /// Keypad Decimal + item kp_decimal = 0xDC; + + /// Keypad Hexadecimal + item kp_hexadecimal = 0xDD; + + /// Keyboard Left Control + item left_control = 0xE0; + + /// Keyboard Left Shift + item left_shift = 0xE1; + + /// Keyboard Left Alt + item left_alt = 0xE2; + + /// Keyboard Left GUI + /// NOTE: Windows key for Windows 95, and Compose. + /// NOTE: Windowing environment key, examples are Microsoft® LEFT WIN key, Macintosh® LEFT APPLE key, Sun® LEFT META key. + item left_gui = 0xE3; + + /// Keyboard Right Control + item right_control = 0xE4; + + /// Keyboard Right Shift + item right_shift = 0xE5; + + /// Keyboard Right Alt + item right_alt = 0xE6; + + /// Keyboard Right GUI + /// NOTE: Windows key for Windows 95, and Compose. + /// NOTE: Windowing environment key, examples are Microsoft® RIGHT WIN key, Macintosh® RIGHT APPLE key, Sun® RIGHT META key. + item right_gui = 0xE7; + + ... + } +} + +namespace network { + /// An address of the IPv4 internet protocol. + struct IPv4 { + /// The four bytes of the IP address. + /// + /// NOTE: This is a u32 that is always encoded in network byte order. + field addr: [4]u8 ; //? TODO: align(4) + } + + /// An address of the IPv6 internet protocol. + struct IPv6 { + /// The 16 bytes of the IP address. + /// + /// NOTE: This is always encoded in network byte order. + field addr: [16]u8; //? TODO: align(4) + + /// The interface for which this IP address is valid. + /// + /// NOTE: For non-link-local addresses, scope must be `link.InterfaceId.any`. + field scope: link.InterfaceId; + } + + /// A polymorphic IP address that can be both IPv4 or IPv6. + struct IP { + /// Defines which field of `addr` is active. + /// + /// NOTE: Must be `Type.ipv4` or `Type.ipv6`. + field type: Type; + + /// Union of the possible address types. + field addr: AnyAddr; + + enum Type : u8 { + item ipv4 = 0; + item ipv6 = 1; + + /// Not a concrete type of IP type, but a sentinel different + /// kernel APIs use for defining that they don't scope a syscall or + /// a operation to a specific IP type. + item any = 255; + } + + union AnyAddr { + /// Active when `IP.type == Type.ipv4`. + field v4: IPv4; + + /// Active when `IP.type == Type.ipv6`. + field v6: IPv6; + } + } + + /// An endpoint defines a connection target for TCP and UDP connections. + /// It is a tuple formed of an IP address and a port. + struct EndPoint { + /// IP address of the connection endpoint. + field ip: IP; + + /// The port number of the connection endpoint. + /// + /// NOTE: Uses host byte order. + field port: u16; + } + + /// + /// TODO: Write about the general idea of network interfaces. + /// + /// TODO: Talk about kernel subsystems being enabled/disabled and what each subsystem + /// is supposed to to. + /// + /// Kernel Subsystems + /// ================= + /// + /// IPv4: The kernel's built-in IPv4 stack. + /// This subsystem implements a regular IPv4 stack. If disabled, this interface + /// won't allow IPv4 operation through kernel interfaces anymore. + /// + /// NOTE: Disabling the IPv4 stack for an interface will remove all IPv4 addresses + /// and routes for this interface. + /// IPv6: + /// This subsystem implements a regular IPv6 stack. If disabled, this interface + /// won't allow IPv6 operation through kernel interfaces anymore. + /// + /// NOTE: Disabling the IPv6 stack for an interface will remove all IPv6 addresses + /// and routes for this interface. + /// + /// DHCPv4: + /// TODO: Write subsystem docs + /// This subsystem automatically performs DHCP management for the interface. + /// If enabled, the kernel automatically requests and refreshes DHCP leases, + /// and manages the routes. + /// + /// NOTE: This subsystem is disabled by default. + /// + /// DHCPv6: + /// TODO: Write subsystem docs + /// + /// NOTE: This subsystem is disabled by default. + /// + /// SLAAC: + /// TODO: Write subsystem docs + /// + /// NOTE: This subsystem is enabled by default. + /// + /// + /// TODO: Write how routing works in Ashet OS. + /// + namespace link { + /// Unique identifier of a network interface. + /// + /// NOTE: Interface ids are allocated in a monotonically increasing + /// way, and will be stable until a network interface is removed + /// from the kernel. + /// + /// NOTE: Id allocation will never allocate any of the named values inside this + /// enumeration. + /// + /// NOTE: Interface ids received from any kernel syscall or overlapped operation + /// are ephemeral and are only guaranteed to be valid until the next yielding + /// syscall. + /// + /// NOTE: Except `loopback`, the enumeration order of interfaces is unspecified + /// and the ids cannot be assumed stable between reboots. + enum InterfaceId : u32 { + /// The loopback interface is a virtual interface + /// that makes all sent packets be received by the same + /// interface again. + /// + /// This way, sockets can be bound to a local interface + /// and communicate without affecting any external systems. + /// + /// NOTE: The loopback interface has the IP addresses `127.0.0.1/8` + /// and `::1/128` by default. + /// + /// The kernel also adds a default connected route for these + /// two addresses. + item loopback = 0; + + /// A sentinel value that can be used to annotate the absence of a + /// specific interface. + /// + /// NOTE: This is not a true interface that can be used to query + /// information, but is a required "workaround" for IPv6 + /// scopes to be able to encode that an IP address isn't + /// scoped to a specific interface. + /// + /// NOTE: Using this on all APIs that don't explicitly mention it yields + /// the error `InvalidInterface`. + item any = 0xFFFFFFFF; + + ... + } + + /// Enumeration of all supported network interface types. + enum InterfaceType : u8 { + /// The interface is supported by the kernel, but the type + /// of network interface does not fit any of the other categories. + item unknown = 0; + + /// The interface is a loopback interface. + item loopback = 1; + + /// The interface is an Ethernet (IEEE 802.3) interface. + item ethernet = 2; + + /// The interface is virtual and is controlled by software. + item virtual = 3; + + /// The interface is a WLAN (IEEE 802.11) interface. + item wifi = 4; + + /// The interface is based on IEEE 802.15.4 (e.g. 6LoWPAN/Thread/Zigbee-style links). + item ieee_802_15_4 = 5; + + /// The interface is Bluetooth-based (e.g. PAN/BNEP or IPv6-over-BLE/IPSP). + item bluetooth = 6; + + /// The interface is point-to-point (e.g. PPP/SLIP). Usually has no meaningful link-layer address. + item point_to_point = 7; + + /// The interface is InfiniBand-based (IPoIB). + item infiniband = 8; + + /// The interface is a Wireless Body Area Network (WBAN), typically IEEE 802.15.6. + /// + /// NOTE: The underlying PHY may be narrowband, UWB, or body-coupled (HBC), + /// depending on the device. + item wban = 9; + } + + /// The physical address of a network interface. + /// + /// NOTE: This isn't just a MAC address, but it can hold several + /// different types of address. + struct PhysicalAddress { + /// Length of the physical address in bytes. + field len: u8; + + /// The type of the physical address. Specifies how `bytes` are interpreted. + field type: Type; + + /// Describes how the address value was assigned/generated (if known). + /// + /// NOTE: This is intentionally generic. For example, Bluetooth LE privacy + /// addresses (static/resolvable/non-resolvable) can be mapped onto + /// the `random_*` variants here. + field assignment: Assignment; + + /// Reserved. Must be zero. + field _reserved0: u8 = 0; + + /// Contains the bytes of the physical address. + /// + /// NOTE: The first `len` bytes are valid. All bytes beyond the + /// first `len` bytes must be zero. + /// + /// NOTE: These bytes must be interpreted according to `type`. + field bytes: [20]u8; + + /// Describes how a physical address was assigned/generated. + /// + /// NOTE: This is determined by the kernel in a best-effort fashion, and + /// may be `unknown` even if the address format is known. + enum Assignment: u8 { + /// The kernel does not know how this address was assigned. + item unknown = 0; + + /// Universally administered / externally assigned identifier. + /// + /// NOTE: For EUI-48/EUI-64 this typically means IEEE-assigned (U/L bit = 0). + item universal = 1; + + /// Locally administered identifier (not globally assigned). + /// + /// NOTE: For EUI-48/EUI-64 this typically means U/L bit = 1. + item local = 2; + + /// Randomly generated but expected to remain stable for long periods + /// (until reconfigured/reset). + /// + /// NOTE: Bluetooth LE "Static Random Address" maps here. + item random_stable = 3; + + /// Randomly generated and expected to rotate over time for privacy. + /// + /// NOTE: Wi-Fi MAC randomization and Bluetooth LE "Non-Resolvable Private Address" map here. + item random_rotating = 4; + + /// Randomly generated and expected to rotate, but can be mapped back to a stable + /// identity by peers that possess a shared secret / resolver. + /// + /// NOTE: Bluetooth LE "Resolvable Private Address" maps here. + item random_rotating_resolvable = 5; + } + + /// Enumeration of possible types for physical link addresses. + enum Type: u8 { + /// Special marker that encodes `?PhysicalAddress == null`. + /// + /// NOTE: `absent` means the `PhysicalAddress` exists in a logical sense, but is empty. + /// `null` means the `PhysicalAddress` value itself is absent/missing. + /// + /// NOTE: All addresses of this type are empty (zero bytes long). + /// + /// NOTE: A `PhysicalAddress` `null` value must be fully zeroed out: + /// - `len = 0` + /// - `assignment = Assignment.unknown` + /// - `_reserved0 = 0` + /// - `bytes = {0} ** 20`. + item null = 0; + + /// This type marks an absent physical address. + /// + /// This typically means the associated interface does not support physical addresses at all. + /// + /// NOTE: All addresses of this type are empty (zero bytes long). + /// + /// NOTE: This is a special case to handle links that have no address, + /// but without introducing "out of band" communication for absent + /// physical addresses. + /// + /// NOTE: This is distinct from `null` in that a `PhysicalAddress` exists but is empty, + /// while `null` means the `PhysicalAddress` value does not exist. + /// + /// NOTE: For `absent`, the struct must satisfy: + /// - `len = 0` + /// - `assignment = Assignment.unknown` + /// - `_reserved0 = 0` + /// - `bytes = {0} ** 20`. + item absent = 1; + + /// A physical address is available, but the kernel cannot represent the type of address. + /// + /// NOTE: Any `len` may be valid for this kind of physical address. + item unknown = 2; + + /// An address from the EUI-48 namespace. This is typically known as a MAC address. + /// + /// NOTE: These addresses are typically used with Ethernet or WLAN interfaces. + /// + /// NOTE: `PhysicalAddress.len` must be `6`. + /// + /// NOTE: See RFC 9542 for more information on this address type. + item eui_48 = 3; + + /// An address from the EUI-64 namespace. This is typically known as a MAC address. + /// + /// NOTE: These addresses are typically used with 802.15.4 based protocols (e.g. ZigBee). + /// + /// NOTE: `PhysicalAddress.len` must be `8`. + /// + /// NOTE: See RFC 9542 for more information on this address type. + item eui_64 = 4; + + /// An IPoIB (IP over InfiniBand) link-layer address. + /// + /// NOTE: This is the 20-byte "link-layer address" used by IPoIB for IPv4/ARP + /// and for IPv6 Neighbor Discovery source/target link-layer address options. + /// + /// Layout (network byte order): + /// - byte 0: Reserved flags (must be zero on send; ignore on receive) + /// - bytes 1-3: Queue Pair Number (QPN, 24-bit) + /// - bytes 4-19: Port GID (16 bytes) + /// + /// NOTE: This address is not guaranteed to be stable across reboots or even + /// network interface resets because the QPN may change. + /// + /// NOTE: In IPv6 Neighbor Discovery, the on-wire option is padded to 24 bytes + /// total; the extra padding is not part of this 20-byte address. + /// + /// See RFC 4391, Section 9.1.1 and Section 9.3. + /// + /// NOTE: `PhysicalAddress.len` must be `20`. + item infiniband = 5; + + /// A local WBAN link-layer identifier. + /// + /// NOTE: `PhysicalAddress.len` must be `2`. + /// + /// Interpretation: + /// - byte 0: WBAN / BAN identifier (local scope) + /// - byte 1: Node identifier within that WBAN + /// + /// NOTE: This is not a standalone IEEE-defined 16-bit address format. + /// It is a packing of the IEEE 802.15.6 WBAN_ID + Node_ID fields. + /// + /// NOTE: This address is *not* globally unique. It is only meaningful within + /// the given interface's WBAN. + /// + /// NOTE: This is intended to cover IEEE 802.15.6 style WBAN links, including + /// Human Body Communication (HBC) PHY variants. + item wban_local = 6; + + /// IEEE 802.15.4 short address (16-bit). + /// + /// NOTE: `PhysicalAddress.len` must be `2`. + /// + /// NOTE: The value logically encodes a `u16` in big endian format. + item ieee_802_15_4_short = 7; + } + } + + /// A bit set of several subsystems that the kernel + /// can provide per interface. + bitstruct SubsystemSet : u32 { + field ipv4: bool; + field ipv6: bool; + field dhcp4: bool; + field dhcp6: bool; + field slaac: bool; + + reserve u27 = 0; + } + + struct InterfaceDescription { + /// The type of this network interface. + field type: InterfaceType; + + /// The physical address this interface has. + field address: PhysicalAddress; + + /// The display name of the network interface. + /// + /// NOTE: The lifetime of this string is bound to the lifetime + /// of the network interface. Assume it is only valid between + /// obtaining the interface description from the kernel and the + /// next thread yield. + field name: str; + + /// Name of the NIC vendor or an empty string if unknown. + /// + /// NOTE: The lifetime of this string is bound to the lifetime + /// of the network interface. Assume it is only valid between + /// obtaining the interface description from the kernel and the + /// next thread yield. + field vendor: str; + + /// The maximum bandwidth this interface can theoretically achieve + /// in bits per second. + /// + /// NOTE: For the loopback interface, this value is always zero. + /// + /// NOTE: For virtual interfaces, the driver shall set this value + /// to either a real value or zero, if no upper bound is known. + field max_bandwidth: u64; + + /// The current bandwidth this interface has negotiated with the + /// connected network in bits per second. + /// + /// NOTE: If zero, the value cannot be determined for one of several reasons: + /// - The link is down. + /// - The driver has no way to query the current bandwidth. + /// - There is no physically realistic value (e.g. for virtual or loopback interfaces). + field current_bandwidth: u64; + + /// The set of currently enabled kernel subsystems. + field enabled_subsystems: SubsystemSet; + } + + /// Enumerates the currently available network interfaces. + syscall enumerate_interfaces { + /// Buffer that shall receive the list of interfaces or `null` if the + /// total amount of interfaces should be queried. + in list: ?[]InterfaceId; + + /// If `list` is not `null`, returns the number of elements written to `list`, + /// otherwise it returns the total number of currently available interfaces. + out count: usize; + } + + /// Queries the description of an interface. + syscall get_description { + in interface: InterfaceId; + + out description: InterfaceDescription; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + /// Queries the physical address for the interface. + syscall get_physical_address { + in interface: InterfaceId; + + out address: PhysicalAddress; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + /// Attempts to change the physical address for an interface. + async_call SetPhysicalAddress { + in interface: InterfaceId; + + in address: PhysicalAddress; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// The interface does not allow changing its physical address. + error Unsupported; + + error SystemResources; + } + + /// Enables or disables kernel subsystems for an interface. + /// + /// NOTE: The two sets `enable` and `disable` must be disjoint and + /// must not contain overlapping subsystems. + syscall control_subsystems { + /// The interface for which kernel subsystems should be enabled + /// or disabled. + in interface: InterfaceId; + + /// Every subsystem in this set will be started. + in enable: SubsystemSet; + + /// Every subsystem in this set will be stopped. + in disable: SubsystemSet; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// `enable` and `disable` are overlapping sets and conflict + /// in their semantics. + error InvalidValue; + } + + /// Queries the currently enabled subsystems for the given + /// interface. + syscall get_subsystems { + in interface: InterfaceId; + + /// The set of all enabled network subsystems for `interface`. + out enabled: SubsystemSet; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + //? IP Addressing + + /// Enumeration of potential sources for IP addresses. + enum AddressOrigin : u8 { + /// The IP address was manually added with `AddAddress`. + item manual = 0; + + /// The IP address was assigned by the DHCPv4 network subsystem. + item dhcp4 = 1; + + /// The IP address was assigned by the DHCPv6 network subsystem. + item dhcp6 = 2; + + /// The IP address was assigned by the SLAAC network subsystem. + item slaac = 3; + + /// The IP address was assigned automatically by the kernel through + /// configuration files. + item autoconfig = 4; + } + + /// A binding of an IP address to a network interface. + struct AddressBinding { + /// The IP address that is bound. + field address: IP; + + /// The prefix of the address. Defines which prefix is + /// reachable through the interface directly without routing. + /// + /// NOTE: Must be ≤ 32 for IPv4. + /// + /// NOTE: must be ≤ 128 for IPv6. + field prefix_len: u8; + + /// The origin of the IP address. + /// + /// NOTE: `AddAddress` will ignore this field and always + /// use `AddressOrigin.manual`. + field origin: AddressOrigin; + + /// The lifetime of the IP address. + /// + /// After this timestamp is reached by `clock.monotonic`, the IP address + /// will be automatically removed by the kernel. + /// + /// NOTE: The kernel will also automatically remove the associated connected route. + /// + /// NOTE: If an IP address should not expire, pass `clock.Absolute.infinity`. + field valid_until: clock.Absolute; + } + + /// Enumerates the currently available IP addresses for an interface. + syscall enumerate_addresses { + /// The interface to query. + in interface: InterfaceId; + + /// Buffer that shall receive the list of address bindings or `null` if the + /// total amount of bindings should be queried. + in bindings: ?[]AddressBinding; + + /// If `bindings` is not `null`, returns the number of elements written to `bindings`, + /// otherwise it returns the total number of currently available bindings. + out count: usize; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + /// Adds a new IP address to an interface. + /// + /// NOTE: This operation will implicitly add a new route to the routing + /// table based off (`interface`, `binding.address`, `binding.prefix_len`). + /// + /// The route will use the prefix derived from `binding.address` and will + /// set the `IPv6.scope` for the `Route.network` to `InterfaceId.any`. + /// + /// This route will have `RouteOrigin.connected`. + /// + /// NOTE: This operation is upserting on `binding.address` and will + /// replace the properties of the current address with those from + /// `binding`. + /// This will also update the connected route in the routing table. + /// + /// NOTE: This is an asynchronous call as adding IP addresses may require + /// communication with the NIC to set up filters. This could take + /// some time and thus, the operation was made overlapped. + + async_call AddAddress { + //? TODO: Add a "force: bool" parameter and a new "error NetworkConflict" (ARP/DAD conflict) + //? Detail: IPv6 requires DAD (Neighbor Solicitation for the derived IP) before the address + //? is fully usable. If DAD fails (someone else has the IP), the address must be marked + //? as duplicated and not used. + + + /// The interface which should receive a new IP binding. + in interface: InterfaceId; + + /// The binding specification for the IP address. + in binding: AddressBinding; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// Binding is not a valid value. + /// + /// This could be due to the following reasons: + /// - `binding.address` is not valid. + /// - `binding.prefix_len` cannot be applied to `binding.address`. + /// - `binding.valid_until` is in the past. + /// - `binding.address.addr.ipv6.scope` is not `interface` for link-local IPv6 addresses. + /// - `binding.address.addr.ipv6.scope` is not `InterfaceId.any` for non-link-local IPv6 addresses. + error InvalidValue; + + /// There was an i/o error that lead to the failure of this operation. + error IoError; + + /// The IPv4 or IPv6 network subsystem is disabled and the address type can't be used on this interface. + error SubsystemDisabled; + + error SystemResources; + } + + /// Removes an address from the interface. + /// + /// NOTE: This operation will implicitly remove the connected route from the routing + /// table with the key (`interface`, prefix address derived from `address`, `RouteOrigin.connected`). + /// + /// NOTE: This is an asynchronous call as adding IP addresses may require + /// communication with the NIC to set up filters. This could take + /// some time and thus, the operation was made overlapped. + /// + /// NOTE: If `address` does not exist on `interface`, the operation does nothing. + /// + /// LORE: This operation has no subsystem failure possible as removing an address + /// is idempotent for non-existing addresses, thus there's no reason to + /// care for this error. The final outcome is the same: The address doesn't + /// exist on the interface. + async_call RemoveAddress { + /// The interface which should have an address removed. + in interface: InterfaceId; + + /// The address that shall be removed. + /// + /// NOTE: As for each address, only a single binding can exist, we just need + /// the address to remove the IP binding. + /// + /// NOTE: If this is an IPv6 address, `scope` must match the rules for valid IPv6 + /// addressed and `scope` must be either `interface` or `InterfaceId.any`. + in address: IP; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// `address` is not valid. + error InvalidValue; + + /// There was an i/o error that lead to the failure of this operation. + error IoError; + + error SystemResources; + } + + //? Routing + + /// Enumeration of potential origins for routes. + enum RouteOrigin : u8 { + /// The route was manually created with `add_route`. + item manual = 0; + + /// The route was created by the DHCPv4 network subsystem. + item dhcp4 = 1; + + /// The route was created by the DHCPv6 network subsystem. + item dhcp6 = 2; + + /// The route was created by the SLAAC network subsystem. + item slaac = 3; + + /// The route was created by the kernel through configuration files. + item autoconfig = 4; + + /// The route was created by the kernel when adding an IP address + /// to a network interface. + item connected = 5; + } + + /// A route describes rules on where to send IP packets. + /// + /// A route is valid if: + /// - `network.type` is `gateway.type`. + /// - `prefix_len` is valid for `network.type`. + /// - `valid_until` is in the future. + /// - If the route is an IPv6 route: + /// - `network.scope` is `InterfaceId.any`. + /// - `gateway.scope` is `interface` for all link-local addresses. + /// - `gateway.scope` is `InterfaceId.any` for all other addresses. + struct Route { + /// Defines the target network for this route. + /// + /// NOTE: This value is only semantically valuable together with `prefix_len`. + /// + /// NOTE: For IPv6, `scope` must be set to `InterfaceId.any` to prevent conflicting + /// definitions with `interface`. + field network: IP; + + /// Defines how many bits of the IP in `network` identify the target network. + /// + /// NOTE: Must be ≤ 32 for IPv4. + /// + /// NOTE: must be ≤ 128 for IPv6. + field prefix_len: u8; + + /// Defines where the next-hop for the target network is and + /// sends the packets this way. + /// + /// NOTE: If set to the unspecified IP (`0.0.0.0` or `::`), the + /// route defines an on-link route and target addresses should + /// be discovered via neighbor discovery (ARP for IPv4, NDP for IPv6). + /// + /// NOTE: If not set to an unspecified IP, the address must be a valid + /// unicast address. + /// + /// NOTE: If gateway is a link-local IPv6 address, the `scope` must be the + /// `interface` of the route. + /// + /// NOTE: `gateway.type` must match `network.type`. + field gateway: IP; + + /// The interface that will be used for sending the packets to the target network. + field interface: InterfaceId; + + /// The priority is a tie-breaker for when multiple rules would match + /// with the same prefix, but different interfaces. + /// + /// Higher priorities win. + /// + /// NOTE: When two routes with the same prefix and priority match, + /// the first inserted route is taken. + field priority: u16; + + /// The source system that added this route to the routing table. + /// + /// NOTE: `add_route` will ignore the value and will always add a route + /// with `RouteOrigin.manual`. + field origin: RouteOrigin; + + /// The lifetime of the route. + /// + /// After this timestamp is reached by `clock.monotonic`, the route + /// will be automatically removed by the kernel. + /// + /// NOTE: If a route should not expire, pass `clock.Absolute.infinity`. + field valid_until: clock.Absolute; + } + + /// Enumerates the routing table. + /// + /// NOTE: The table is returned ordered longest to shortest prefix, + /// highest-to-lowest priority, then retains insertion order. + /// + /// LORE: Enumerating routes ordered is cheap for the kernel as it + /// has to keep the table ordered in-memory anyways for efficient + /// evaluation, and thus we can expose this property into userland. + syscall enumerate_routes { + /// Buffer that shall receive the list of routes or `null` if the + /// total amount of routes should be queried. + in routes: ?[]Route; + + /// If `routes` is not `null`, returns the number of elements written to `routes`, + /// otherwise it returns the total number of routes. + out count: usize; + } + + /// Adds a new route to the routing table. + syscall add_route { + /// The route to be added. + in route: Route; + + /// The route is not valid. + /// + /// See `Route` documentation for validation rules. + error InvalidValue; + + /// Another route for the same target network exists on the same interface. + /// + /// This means that another route with (`route.interface`, `route.network`, `route.prefix_len`, `route.gateway`) exists. + error Conflict; + + /// `route.interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The IPv4 or IPv6 network subsystem is disabled and the `route.network` type can't be used on `route.interface`. + error SubsystemDisabled; + + error SystemResources; + } + + /// Removes an existing route from the routing table. + /// + /// NOTE: Removing a non-existent route is idempotent and does nothing. + /// + /// LORE: This syscall has no subsystem failure possible as removing a route + /// is idempotent for non-existing routes, thus there's no reason to + /// care for this error. The final outcome is the same: The route doesn't + /// exist on the interface anymore. + syscall remove_route { + /// The route to delete. + /// + /// NOTE: When selecting which rules should be deleted, `route.priority`, + /// `route.origin`, `route.valid_until` are ignored. + in route: Route; + + /// The route is not valid. + /// + /// See `Route` documentation for validation rules. + error InvalidValue; + + /// `route.interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + //? Link state + + enum LinkState : u8 { + /// The network interface has not recognized any connection. + item down = 0; + + /// The network interface is connected to the network. + item up = 1; + } + + /// Queries the current link state of a network interface. + syscall get_link_state { + in interface: InterfaceId; + + /// Current state of the link. + out state: LinkState; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + /// Completes when a link changes its state. + async_call WaitForLinkState { + /// The interface for which a link state shall be awaited. + in interface: InterfaceId; + + /// If not `null`, the operation completes when the link becomes `desired`. + /// Otherwise, the operation completes on the next interface state change. + in desired: ?LinkState; + + /// The new link state. + out current: LinkState; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + } + + /// Sends an ICMP or ICMPv6 echo request. + /// + /// NOTE: This can be used to test if a host is reachable. + async_call Ping { + /// The interface that shall send the ping message. + /// + /// NOTE: May be `InterfaceId.any` to use the routing table + /// to determine which interface and gateway shall be used. + in interface: InterfaceId; + + /// The IP that should be tested. + /// + /// NOTE: `target.type` decides which underlying protocol shall be used. + in target: IP; + + /// Maximum number of hops before the operation times out. + in ttl: u8; + + /// The deadline for the operation. + /// + /// The operation returns the `Timeout` error when `timeout` is smaller than `clock.monotonic()`. + /// + /// LORE: In contrast to many other overlapped operations, a `Ping` would potentially + /// never complete and it's an expected outcome that no response is received. + /// Thus, the general rule of "no timeouts in overlapped operations" is broken + /// here on purpose. + in timeout: clock.Absolute; + + /// The payload of the ICMP/ICMPv6 echo request that is sent with the message. + /// + /// NOTE: This buffer must stay valid until the end of the operation. + in payload_request: bytestr; + + /// The payload of the ICMP/ICMPv6 echo response that may be received. + /// + /// NOTE: This buffer must stay valid until the end of the operation. + /// + /// NOTE: `payload_response.len` must not be smaller than `payload_request.len`. + /// + /// NOTE: This buffer will only include the echoed payload. + in payload_response: bytebuf; + + /// The IP address which finally answered our echo request. + /// + /// NOTE: For responses from link-local IPv6 addresses, `IPv6.scope` is set + /// to the interface that received the echo response. + out responder: IP; + + /// The timestamp when the kernel received the echo reply. + out received_at: clock.Absolute; + + /// Actual number of bytes sent from `payload_request`. + /// + /// NOTE: This may be smaller than `payload_request.len` when the message is truncated. + out request_len: usize; + + /// Number of bytes received from `responder`. + out response_len: usize; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// No response was received until `timeout`. + error Timeout; + + /// The ping operation yielded an ICMP error instead of an ICMP echo. + error IcmpError; + + /// The kernel does not know a route to `target`. + error MissingRoute; + + /// A parameter was badly specified. + /// + /// This may be due to: + /// - `payload_request.len` > `payload_response.len`. + /// - `target` is an link-local IPv6 address with a `scope` that is not `interface`. + error InvalidValue; + + /// The IPv4 or IPv6 network subsystem is disabled and the `target` type can't be used on the effective interface. + error SubsystemDisabled; + + /// `interface` is down and cannot send any data. + error LinkDown; + + /// There was an i/o error that lead to the failure of this operation. + error IoError; + + error SystemResources; + } + } + + /// Neighbor discovery / neighbor cache (ARP for IPv4, NDP for IPv6). + namespace neighborhood { + /// Enumerates potential origins for a network neighbor. + enum Origin : u8 { + /// The neighbor was manually created with `add_neighbor`. + item manual = 0; + + /// The neighbor was discovered by the kernel through an ARP/NDP request. + item learned = 1; + + /// The IP address was created automatically by the kernel through + /// configuration files. + item autoconfig = 2; + } + + /// A neighbor is the mapping of an IP address to a physical address. + /// + /// The following invariants apply to the timestamp values: + /// - `last_updated` >= `known_since` + /// - `expires_after` >= `known_since` + /// - `fresh_until` >= `known_since` + /// - `fresh_until` <= `expires_after` + /// - `fresh_until` >= `last_updated` if `state == State.reachable` + struct Neighbor { + /// The interface on which the neighbor was discovered. + field interface: link.InterfaceId; + + /// The IP address that identifies the neighbor. + /// + /// NOTE: Depending on `ip.type`, the following protocols were used for discovery: + /// - IPv4: ARP + /// - IPv6: NDP + /// + /// NOTE: If this IP is a link-local IPv6 ip, its scope must be equal to `interface`. + field ip: IP; + + /// The physical address that was discovered. + /// + /// NOTE: This value is an optional `link.PhysicalAddress` with the following rules: + /// - If `state` is `State.reachable`, `physical.type` is never `link.PhysicalAddress.Type.null`. + /// - Else, the `physical.type` always is `link.PhysicalAddress.Type.null`. + field physical: link.PhysicalAddress; + + /// The state defines if a neighbor is usable or not. + /// + /// NOTE: Can never have a higher numeric value than `enumerate_neighbors.max_state` + /// when received through enumeration. + field state: State; + + /// The source which discovered the neighbor. + /// + /// NOTE: When the neighbor is added with `add_neighbor`, this value is ignored + /// and `Origin.manual` is used. + field origin: Origin; + + /// Flags storing additional information about this neighbor. + field flags: Flags; + + /// The timestamp when this neighbor entry was added to the list. + /// + /// NOTE: This field allows computing the age of the neighbor. + /// + /// NOTE: The other timestamps will always be at least `known_since`. + field known_since: clock.Absolute; + + /// The timestamp when this neighbor entry was refreshed last. + /// + /// NOTE: This allows deriving a liveness for the neighbor. + field last_updated: clock.Absolute; + + /// The timestamp until which this neighbor is assumed to be valid. + /// + /// NOTE: For IPv4/ARP, the lifetime is computed from a kernel configuration. + /// + /// NOTE: For IPv6/NDP, this is derived from the ReachableTime. + /// + /// NOTE: As long as `state == State.reachable`, this value is never less + /// than `last_updated`. + /// + /// NOTE: When `clock.monotonic` returns a value bigger than this, the neighbor has + /// become stale. + /// This means the neighbor isn't necessarily valid anymore, but the kernel assumes + /// it's still usable. + /// The effect of this is that the kernel will still attempt to directly communicate + /// with the neighbor without awaiting a neighbor discovery, but it will trigger + /// an asynchronous re-discovery to ensure the neighbor still exists. + field fresh_until: clock.Absolute; + + /// The drop-dead time after which this neighbor is killed. + /// + /// NOTE: The kernel will automatically remove the entry as soon + /// as `clock.monotonic` reaches this value. + field expires_after: clock.Absolute; + + /// Flags that further specify the neighbors state. + bitstruct Flags : u8 { + /// This neighbor is believed to be an IPv6 capable router on the link. + /// + /// NOTE: This value may change over time based on observed communications on the network. + /// + /// NOTE: For IPv4 neighbors, this value is always `false`. + field is_router: bool; + + /// This neighbor is proxied through network segments. + /// + /// Set only when the kernel knows ND proxying is in effect for this mapping (e.g., learned through ND-proxy mechanisms / configuration). Otherwise the value is false. + /// + /// NOTE: For IPv4 neighbors, this value is always `false`. + field is_proxy: bool; + + reserve u6 = 0; + } + + /// Enumeration of potential states a neighbor has. + /// + /// NOTE: The numeric value of a state is a "usefulness" order and + /// is used by `enumerate_neighbors` to filter states. + enum State : u8 { + /// The neighbor is actively reachable and valid. + /// + /// This means the kernel will immediately use the physical address without performing + /// a request first. + /// + /// NOTE: When `clock.monotonic` returns a value bigger than `fresh_until`, the kernel + /// will perform an automatic background discovery of the neighbor to check if + /// neighbor is still valid. + /// + /// NOTE: When this state is active, `Neighbor.physical.type` is never `link.PhysicalAddress.Type.null`. + item reachable = 0; + + /// A neighbor discovery was executed and the neighbor could not be found. + /// + /// NOTE: This means the kernel will return an error for operations using this + /// neighbor address until `clock.monotonic` reaches `expires_after`. + /// + /// The next request after the `expires_after` is reached will trigger a new discovery process. + /// + /// NOTE: When this state is active, `Neighbor.physical.type` is `link.PhysicalAddress.Type.null`. + item failed = 1; + + /// The neighbor was requested, but isn't `reachable` nor `failed` yet. + /// + /// NOTE: This state is only set until the kernel has performed the first discovery. + /// It does not represent the background discovery. + /// + /// NOTE: When this state is active, `Neighbor.physical.type` is `link.PhysicalAddress.Type.null`. + item resolving = 2; + } + } + + /// Enumerates the currently known neighborhood for an interface. + syscall enumerate_neighbors { + /// The interface for which we want to receive the neighbors + /// or `link.InterfaceId.any` to enumerate the neighbors of all interfaces. + in interface: link.InterfaceId; + + /// Defines for which IP protocols the neighbors should be returned. + /// + /// NOTE: If `IP.Type.any` is passed, both IPv4 and IPv6 neighbors are returned, + /// assuming the corresponding subsystem is enabled. + in protocol: IP.Type; + + /// A buffer that receives the neighbors of `interface` or `null` to query + /// the total amount of neighbors. + in neighbors: ?[]Neighbor; + + /// Defines the maximum integer state value this enumeration returns. + /// This effectively allows filtering the returned list for: + /// - `Neighbor.State.reachable`: Only reachable entries are returned. This is the "true" neighborhood as currently known. + /// - `Neighbor.State.failed`: Only entries with a well-defined state are returned. This also yields knowledge about who is currently not our neighbor. + /// - `Neighbor.State.resolving`: All entries are returned. This allows querying if we're currently searching for a specific neighbor. + in max_state: Neighbor.State; + + /// The number of items written to `neighbors` if not `null`, otherwise + /// the total number of neighbors returned by the query. + out count: usize; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// Returned when `interface` is not `link.InterfaceId.any` and: + /// - `protocol` is `IP.Type.ipv4` and the ipv4 subsystem is disabled for `interface`. + /// - `protocol` is `IP.Type.ipv6` and the ipv6 subsystem is disabled for `interface`. + /// - `protocol` is `IP.Type.any` and both the ipv4 and ipv6 subsystem are disabled for `interface`. + error SubsystemDisabled; + } + + /// Queries a single neighbor. + /// + /// NOTE: `neighbor.state` must be queried to check reachability for a given + /// IP, as the neighbor might have any possible state. + syscall get_neighbor { + /// The interface for which the neighbor should be queried, or + /// or `link.InterfaceId.any` to query the neighbor on all interfaces. + in interface: link.InterfaceId; + + /// The IP address to query. + /// + /// NOTE: If `interface` is not `link.InterfaceId.any` and this IP is a link-local IPv6 ip, + /// its scope must be equal to `interface`. + /// + /// NOTE: If `interface` is `link.InterfaceId.any` and this IP is a link-local IPv6 ip, + /// its scope is used for `interface`. + in ip: IP; + + /// The neighbor entry for `ip`. + out neighbor: Neighbor; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// `ip` is an IPv6 address, but the scope is not suitable for `interface`. + error InvalidValue; + + /// The requested neighbor is not available in the neighborhood. + error NotAvailable; + + /// The `ip` is available on more than one `interface`. + /// + /// NOTE: This error can only happen when `interface` is `link.InterfaceId.any`. + error Conflict; + + /// Returned when the associated subsystem of `ip.type` is disabled for `interface`. + /// NOTE: Can never be returned if `interface == link.InterfaceId.any`. If no interface + /// with an enabled subsystem for `ip.type` has `ip`, `NotAvailable` is returned.” + error SubsystemDisabled; + } + + /// Adds a new static neighbor to the neighborhood. + /// + /// NOTE: The kernel will do the following transformations on `neighbor`: + /// - `neighbor.state = Neighbor.State.reachable` if `neighbor.physical.type != link.PhysicalAddress.Type.null` + /// - `neighbor.state = Neighbor.State.failed` if `neighbor.physical.type == link.PhysicalAddress.Type.null` + /// - `neighbor.origin = Origin.manual` + /// - `neighbor.known_since = clock.monotonic()` + /// - `neighbor.last_updated = clock.monotonic()` + /// - `neighbor.fresh_until = neighbor.expires_after` + /// + /// NOTE: `neighbor` will be validated *after* the transformations are applied. + syscall add_neighbor { + /// The neighbor to add. + + in neighbor: Neighbor; + + /// Defines that if a neighbor with the same IP already exists, + /// it is implicitly replaced with `neighbor`. + in upsert: bool; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The neighbor does not encode a correct value. + /// + /// In addition to the general validity rules of `Neighbor`, the following rules apply: + /// - `neighbor.interface` must not be `link.InterfaceId.any`. + /// - `neighbor.ip` must be a valid IP address. + /// - `neighbor.ip.scope` must be `neighbor.interface` if `neighbor.ip` is a link-local IPv6 address. + /// - `neighbor.expires_after` must not be in the past. + error InvalidValue; + + /// `upsert` is `false` and a neighbor with `neighbor.ip` already exists. + error Conflict; + + /// Returned when the associated subsystem of `neighbor.ip.type` is disabled for `neighbor.interface`. + error SubsystemDisabled; + + error SystemResources; + } + + /// Removes a single neighbor from the neighborhood. + /// + /// NOTE: If the neighbor with `ip` was created through + /// neighbor discovery, `include_learned` must be + /// set, otherwise `Forbidden` is returned as an error. + /// + /// NOTE: If no neighbor with `ip` exists, this syscall is idempotent. + syscall remove_neighbor { + /// The interface for which the neighbor should be removed. + in interface: link.InterfaceId; + + /// The neighbor to be removed. + /// + /// NOTE: If this IP is a link-local IPv6 ip, its scope must be equal to `interface`. + in ip: IP; + + /// Guardrail to prevent accidental removal of neighbors created + /// through neighbor discovery. + in include_learned: bool; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// `ip` is an IPv6 address, but the scope is not suitable for `interface`. + error InvalidValue; + + /// If `include_learned` is `false` and the neighbor to remove has + /// `Neighbor.origin == Origin.learned`. + /// + /// NOTE: Will never happen if no neighbor with `ip` exists. + error Forbidden; + + /// Returned when: + /// - `ip.type` is `IP.Type.ipv4` and the ipv4 subsystem is disabled for `interface`. + /// - `ip.type` is `IP.Type.ipv6` and the ipv6 subsystem is disabled for `interface`. + error SubsystemDisabled; + } + + /// Invalidates the neighbor table for `interface` and removes all items for `protocol`. + syscall flush_neighbors { + /// The interface for which the neighborhood should be flushed. + /// If `link.InterfaceId.any` is passed, all neighborhoods for all interfaces are flushed. + in interface: link.InterfaceId; + + /// Defines for which IP protocols the neighbors should be flushed. + /// + /// NOTE: If `IP.Type.any` is passed, both IPv4 and IPv6 neighbors are flushed, + /// assuming the corresponding subsystem is enabled. + in protocol: IP.Type; + + /// If `true`, will only remove the neighbors with `Neighbor.origin == Origin.learned`. + in keep_manual: bool; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// Returned when the associated subsystem of `protocol` is disabled for `interface` + /// and `interface` is not `link.InterfaceId.any`. + error SubsystemDisabled; + } + + /// Resolves an IP address the the associated physical address. + /// + /// NOTE: The kernel will potentially delay sending ARP/NDP requests + /// until an internal timeout has elapsed. + /// + /// This is to prevent flooding the network with requests. + /// + /// `flood` overwrites this behavior. + /// + /// NOTE: Not every schedule of a resolve triggers a new discovery. + /// The kernel is free to fuse several scheduled `Resolve` + /// operations for the same (`interface`, `target`) groups. + /// + /// `deadline` is still respected for each individual operation. + /// + /// This does not apply to operations that have `flood` set. + async_call Resolve { + /// The interface which should query its network for the physical address. + in interface: link.InterfaceId; + + /// The IP for which the physical address should be resolved. + /// + /// NOTE: The following protocols will be used depending on `target.type`: + /// - IPv4: ARP + /// - IPv6: NDP + /// + /// NOTE: If this IP is a link-local IPv6 ip, its scope must be equal to `interface`. + in target: IP; + + /// The deadline for the operation. + /// + /// The operation returns the `Timeout` error when `deadline` is smaller than `clock.monotonic()`. + /// + /// LORE: In contrast to many other overlapped operations, a `Resolve` would potentially + /// never complete and it's an expected outcome that no response is received. + /// Thus, the general rule of "no timeouts in overlapped operations" is broken + /// here on purpose. + in deadline: clock.Absolute; + + /// Overwrites the kernels internal flood protection and immediately starts + /// sending a request. + /// + /// NOTE: Setting `flood` disables the internal fusing and the kernel will trigger + /// many ARP/NDP requests. + in flood: bool; + + /// The information received by the resolver. + out neighbor: Neighbor; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// No response was received until `deadline`. + error Timeout; + + /// `target` was an invalid IP or + /// `target` is a link-local IPv6 address with `target.scope != interface`. + error InvalidValue; + + /// The associated subsystem for `target.type` is disabled on `interface`. + error SubsystemDisabled; + + /// `interface` is down and cannot send any data. + error LinkDown; + + /// There was an i/o error that lead to the failure of this operation. + error IoError; + + error SystemResources; + } + + //? TODO: Add async_call WaitForUpdate when a proper model for the wait operation was found. + } + + /// IPv6 Stateless Address Autoconfiguration (SLAAC). + namespace slaac { + + //? TODO: Add support for RFC 4191 ( Default Router Preferences and More-Specific Routes) + + /// Represents a single Prefix Information Option (PIO) learned from an IPv6 Router Advertisement. + /// + /// NOTE: The following invariants apply to the timestamp types: + /// - `preferred_until` >= `received_at` + /// - `updated_at` >= `received_at` + /// - `valid_until` >= `received_at` + /// - `valid_until` >= `preferred_until` + /// - `valid_until` >= `updated_at` + struct Prefix { + /// The router that announced the prefix. + /// + /// NOTE: This address is typically a link-local address with the associated interface for `IPv6.scope`. + /// + /// NOTE: If router is link-local, `router.scope` must be the associated interface. + field router: IPv6; + + /// The prefix announced by the router. + /// + /// NOTE: `prefix.scope` must be `link.InterfaceId.any`. + /// + /// NOTE: All bits beyond `prefix_len` are zeroed out to enable determinism. + field prefix: IPv6; + + /// The number of bits inside `prefix` that are part of the prefix. + /// NOTE: Can be between 0 and 128 inclusive. + field prefix_len: u8; + + /// Defines additional information for this prefix. + field flags: Flags; + + /// The timestamp when the kernel initially received this prefix. + field received_at: clock.Absolute; + + /// The timestamp when the kernel last received this prefix. + field updated_at: clock.Absolute; + + /// Timestamp until which the kernel will prefer this prefix and derived IP addresses. + /// + /// NOTE: The prefix is still valid until `valid_until`, but the network stack should choose + /// another prefix if possible. + field preferred_until: clock.Absolute; + + /// Timestamp at which the kernel will drop the prefix. + field valid_until: clock.Absolute; + + bitstruct Flags : u8 { + /// The prefix is reachable directly through this interface without + /// the need of a gateway. + /// + /// NOTE: If the slaac subsystem is enabled on the associated interface, the kernel will + /// automatically add (or upsert) a on-link prefix route for this prefix with: + /// - `link.Route.network` set to `Prefix.prefix`. + /// - `link.Route.prefix_len` set to `Prefix.prefix_len`. + /// - `link.Route.gateway` set to the unspecified address (`::`). + /// - `link.Route.interface` set to the associated interface of the `Prefix`. + /// - `link.Route.priority` set to `0`. + /// - `link.Route.origin` set to `link.RouteOrigin.slaac`. + /// - `link.Route.valid_until` set to a value depending on the context. + /// + /// If multiple `Prefix` entries exist for the same + /// (`interface`, `prefix`, `prefix_len`) (i.e. advertised by different routers), + /// the derived on-link prefix route remains present as long as at least one matching + /// `Prefix` entry with `on_link = true` is still valid. + /// + /// The kernel sets `link.Route.valid_until` to the maximum `valid_until` + /// across all currently valid matching `Prefix` entries with `on_link = true`. + /// + /// If the set of matching prefixes changes (update/expiry/flush), + /// the kernel recomputes `link.Route.valid_until` accordingly and removes the + /// derived route once no matching prefixes remain. + /// + /// NOTE: The kernel will only create/update routes with `link.Route.origin = link.RouteOrigin.slaac` + /// and will not modify routes of other origins. + field on_link: bool; + + /// The interface shall automatically derive IP addresses from the prefix. + /// + /// NOTE: See `Config` on how this bit will be used. + /// + /// NOTE: The kernel will automatically derive addresses from the prefix when the slaac subsystem + /// is enabled and `prefix_len` is 64. + /// + /// The derived addresses will be added with `link.AddressBinding.origin` + /// set to `link.AddressOrigin.slaac`. + /// + /// If multiple matching `Prefix` entries exist for the same + /// (`interface`, `prefix`, `prefix_len`) with `autonomous = true`, + /// the kernel sets `link.AddressBinding.valid_until` to the maximum `valid_until` + /// across all currently valid matching entries, and recomputes it on + /// update/expiry/flush. The address is removed once no matching prefixes remain. + /// + /// NOTE: The kernel will only create/update address bindings with `link.AddressBinding.origin = link.AddressOrigin.slaac` + /// and will not modify address bindings of other origins. + field autonomous: bool; + + reserve u6 = 0; + } + } + + /// Represents the router identity from a single Router Advertisement. + /// Also contains neighborhood discovery parameters sent with the Router Advertisement. + /// + /// NOTE: The following invariants apply to the timestamp types: + /// - `valid_until` >= `received_at` + /// - `valid_until` >= `updated_at` + /// - `updated_at` >= `received_at` + struct Router { + /// The address of the discovered router. + /// + /// NOTE: If the slaac subsystem is enabled for the interface, a default route + /// will be upserted that uses `address` as the next-hop: + /// + /// - `link.Route.network` set to the unspecified address (`::`). + /// - `link.Route.prefix_len` set to `0`. + /// - `link.Route.gateway` set to `Router.address`. + /// - `link.Route.interface` set to the associated interface for this router. + /// - `link.Route.priority` set to `0`. + /// - `link.Route.valid_until` set to `Router.valid_until`. + /// - `link.Route.origin` set to `link.RouteOrigin.slaac`. + /// + /// NOTE: If this IP is a link-local IPv6 IP, its scope must be equal to associated interface. + /// + /// NOTE: The kernel will only create/update routes with `link.Route.origin = link.RouteOrigin.slaac` + /// and will not modify routes of other origins. + field address: IPv6; + + /// The max. number of hops for outbound packets via this router. + /// + /// NOTE: 0 means the router did not advertise a hop limit and the + /// actual value is unknown. + field hop_limit: u8; + + /// The MTU for the link. + /// + /// NOTE: 0 means the router did not advertise an MTU and the + /// actual value is unknown. + field mtu: u32; + + /// The time in milliseconds a neighbor should be considered reachable. + /// + /// NOTE: If zero, a default value shall be used (typically 30 seconds). + /// + /// NOTE: This value affects how `neighborhood.Neighbor.fresh_until` is derived. + field reachable_time_ms: u32; + + /// Time between retransmitted Neighbor Solicitation messages in milliseconds. + /// + /// This affects neighbor discovery retries (including DAD). + /// + /// NOTE: If zero, the router did not specify a value and the kernel uses its default. + field retrans_time_ms: u32; + + /// Defines additional information for this router. + field flags: Flags; + + /// The timestamp at which the kernel received this Router Advertisement. + field received_at: clock.Absolute; + + /// The timestamp when the kernel last received this Router Advertisement. + field updated_at: clock.Absolute; + + /// The timestamp at which the kernel will automatically remove the router. + field valid_until: clock.Absolute; + + bitstruct Flags : u8 { + /// If set, defines that DHCPv6 shall be used to obtain addresses. + /// + /// NOTE: If set, and the dhcp6 subsystem is enabled for the interface, the kernel + /// will automatically perform the DHCPv6 requests when the Router Advertisement + /// is received. + field managed: bool; + + /// If set, defines that additional configuration like DNS servers shall be obtained through + /// DHCPv6. + /// + /// NOTE: If set, and the dhcp6 subsystem is enabled for the interface, the kernel + /// will automatically perform the DHCPv6 requests when the Router Advertisement + /// is received. + field other_config: bool; + + reserve u6 = 0; + } + } + + //? Configuration: + + /// Enumeration of potential methods for stable address generation. + /// + /// NOTE: Addresses generated with these methods are reboot-safe + /// and will be stable for the system. + enum StableAddressGeneration : u8 { + /// Don't generate an address at all. + item none = 0; + + /// Derives stable addresses from the physical link address. + /// + /// NOTE: This method is best-effort and may use other sources to + /// generate reboot-stable IDs for the interface. + item eui64 = 1; + + /// Derives stable addresses in a privacy-preserving manner using a + /// stable secret on the system. + item rfc7217 = 2; + } + + /// Enumeration of potential methods for temporary address generation. + /// + /// NOTE: Addresses generated with these methods are ephemeral and may + /// be rotated after a certain time. + enum TemporaryAddressGeneration : u8{ + /// Don't generate an address at all. + item none = 0; + + /// Temporary addresses are generated in a (pseudo) random pattern. + item rfc8981 = 1; + } + + /// SLAAC configuration for an interface. + struct Config { + /// The currently used stable address generation method to automatically derive + /// addresses when requested. + /// + /// NOTE: The default values is `StableAddressGeneration.eui64` unless + /// changed by a kernel configuration. + field stable_method: StableAddressGeneration; + + /// The currently used temporary address generation method to automatically derive + /// addresses when requested. + /// + /// NOTE: The default values is `TemporaryAddressGeneration.none` unless + /// changed by a kernel configuration. + field temp_method: TemporaryAddressGeneration; + } + + /// Queries the currently enabled configuration for the given interface. + /// + /// NOTE: This syscall will also work when the slaac subsystem is disabled, + /// to enable setting a policy before activating the subsystem. + syscall get_config { + /// The interface for which the configuration shall be returned. + in interface: link.InterfaceId; + + /// The currently active configuration. + out config: Config; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + /// Changes the currently enabled configuration for the given interface. + /// + /// NOTE: This will not re-generate addresses derived with the previous configuration. + /// + /// NOTE: This syscall will also work when the slaac subsystem is disabled, + /// to enable setting a policy before activating the subsystem. + syscall set_config { + /// The interface for which the SLAAC configuration shall be updated. + in interface: link.InterfaceId; + + /// The new configuration, replacing the previous one. + in config: Config; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + } + + //? Status Query: + + /// A summary of the SLAAC status for an interface. + struct Status { + /// The timestamp when the kernel last received a Router Advertisement + /// or a `Router`/`Prefix` expired. + field last_update: clock.Absolute; + + /// Additional boolean properties + field flags: Flags; + + bitstruct Flags : u16 { + /// `true` if at least a single `Prefix.flags.on_link` is set. + field on_link: bool; + + /// `true` if at least a single `Prefix.flags.autonomous` is set. + field autonomous: bool; + + /// `true` if at least a single `Router.flags.managed` is set. + field managed: bool; + + /// `true` if at least a single `Router.flags.other_config` is set. + field other_config: bool; + + reserve u12 = 0; + } + } + + /// A quick status query to get the current SLAAC status. + /// + /// LORE: This syscall primarily exists to prevent userland + /// to enumerate all properties just to learn some trivial + /// information. + syscall get_status { + /// The interface for which the status shall be queried. + in interface: link.InterfaceId; + + /// The current status of `interface`. + out status: Status; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// Returned when the SLAAC subsystem of `interface` is disabled. + error SubsystemDisabled; + } + + //? Queries: + + /// Enumerates the routers learned through SLAAC for `interface`. + syscall enumerate_routers { + /// The interface for which the routers shall be queried. + in interface: link.InterfaceId; + + /// A buffer that will receive the enumerated routers. + /// + /// NOTE: If `null`, no routers will be enumerated, but only the total + /// count is returned. + in routers: ?[]Router; + + /// Total number of available routers. + /// + /// NOTE: If smaller than `routers.len`, the elements for `routers[count..]` will + /// be left unchanged. + out count: usize; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// Returned when the SLAAC subsystem of `interface` is disabled. + error SubsystemDisabled; + } + + /// Enumerates the prefixes learned through SLAAC for `interface`. + syscall enumerate_prefixes { + /// The interface for which the prefixes shall be queried. + in interface: link.InterfaceId; + + /// A buffer that will receive the enumerated prefixes. + /// + /// NOTE: If `null`, no prefixes will be enumerated, but only the total + /// count is returned. + in prefixes: ?[]Prefix; + + /// Total number of available prefixes. + /// + /// NOTE: If smaller than `prefixes.len`, the elements for `prefixes[count..]` will + /// be left unchanged. + out count: usize; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// Returned when the SLAAC subsystem of `interface` is disabled. + error SubsystemDisabled; + } + + /// Defines what parts to flush. + bitstruct FlushMode : u8 { + /// If `true`, removes all routers from the selected interface. + /// + /// NOTE: This implies that all "default" routes added for this + /// interface through the SLAAC subsystem will also be removed. + field routers: bool; + + /// If `true`, removes all prefixes from the selected interface. + /// + /// NOTE: This implies that all on-link prefix routes added for this + /// interface through the SLAAC subsystem will also be removed. + field prefixes: bool; + + reserve u6 = 0; + } + + /// Flushes the SLAAC state for a given interface. + /// + /// NOTE: A flush counts as an "update event" and will change the + /// `Status.last_update` field to `clock.monotonic()`. + syscall flush { + /// The interface for which the SLAAC state shall be flushed. + in interface: link.InterfaceId; + + /// Defines how to flush the state. + /// + /// NOTE: If all bits inside mode are zero, `InvalidValue` is returned. + in mode: FlushMode; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// Returned when the SLAAC subsystem of `interface` is disabled. + error SubsystemDisabled; + + /// All bits inside `mode` were zero. + error InvalidValue; + } + + /// Forces the refresh of the router advertisements by sending a + /// Router Solicitation. + /// + /// NOTE: The operation will complete when at least a single Router Advertisement + /// was received. + /// + /// NOTE: Not every schedule of a refresh triggers a new Router Solicitation. + /// The kernel is free to fuse several scheduled `Refresh` + /// operations for the same `interface`. + /// + /// `deadline` is still respected for each individual operation. + /// + /// NOTE: If `interface` has no suitable link-local address available, the kernel + /// will send the Router Solicitation from the unspecified address (`::`). + /// If multiple suitable addresses are available, the kernel will choose + /// one in a stable manner: The source address will be the same until the + /// address bindings of `interface` change. + async_call Refresh { + /// The interface for which the SLAAC state shall be refreshed. + in interface: link.InterfaceId; + + /// The deadline for the operation. + /// + /// The operation returns the `Timeout` error when `deadline` is smaller than `clock.monotonic()`. + /// + /// LORE: In contrast to many other overlapped operations, a `Refresh` would potentially + /// never complete and it's an expected outcome that no response is received. + /// Thus, the general rule of "no timeouts in overlapped operations" is broken + /// here on purpose. + in deadline: clock.Absolute; + + /// The new status of `interface`. + out status: Status; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// No response was received until `deadline`. + error Timeout; + + /// `deadline` is not in the future. + error InvalidValue; + + /// Returned when the SLAAC subsystem of `interface` is disabled. + error SubsystemDisabled; + + /// `interface` is down and cannot send any data. + error LinkDown; + + /// There was an i/o error that lead to the failure of this operation. + error IoError; + + error SystemResources; + } + + /// This operation completes when `Status.last_update` changes. + async_call WaitForUpdate { + /// The interface for which an update shall be awaited. + in interface: link.InterfaceId; + + /// The new SLAAC status of `interface`. + out new_status: Status; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// Returned when the SLAAC subsystem of `interface` is disabled. + error SubsystemDisabled; + + error SystemResources; + } + } + + /// Everything related to DHCP management for IPv4. + namespace dhcp4 { + + /// NOTE: These options are defined in RFC 2132. + enum OptionCode : u8 { + //? TODO: Add documentation for all options including their contents and size constraints based on the information from RFC 2132. + + item pad = 0; + item subnet_mask = 1; + item time_offset = 2; + item router = 3; + + item domain_name_server = 6; + + item ip_address_lease_time = 51; + + item server_identifier = 54; + + item renewal_time_value = 58; + + item rebinding_time_value = 59; + + item end = 255; + //? TODO: fill out the rest of the options and document their meaning and use + ... + } + + /// A DHCP option. + struct Option { + /// The type of option + field code: OptionCode; + + /// The value of the option. + /// + /// NOTE: Maximum length of `value` is 255. + /// + /// NOTE: The length of the value must match the legal values for `code`. + /// + /// NOTE: The lifetime of this value for fields in a lease is documented on `Lease` itself. + /// + /// NOTE: Options passed to `RequestLease` will be copied by the kernel in the schedule operation. + field value: []const u8; + } + + /// A DHCP lease. + /// + /// NOTE: Any pointer to an array inside the lease is valid until one of the invalidation events occur: + /// - The interface for the lease is removed from the kernel. + /// - The lease is automatically refreshed by the kernel (may only happen if the dhcp4 subsystem is enabled). + /// - A `ReleaseLease` is scheduled for the owning interface. + /// - A `RequestLease` completes for the owning interface. + /// + /// These events can only happen during a scheduler yield, so the pointers may be dead + /// when the thread yields. + /// + /// In general, an application should not hold onto `Lease` information for longer + /// than technically necessary. + struct Lease { + /// The server that issued the DHCP lease. + /// + /// NOTE: This is either the value sent with `OptionCode.server_identifier` + /// or if the option does not exist, the IP from which the DHCPACK was sent. + field server: IPv4; + + /// The address issued by the DHCP server. + /// + /// NOTE: If the lease is acquired with DHCPINFORM this value will be the + /// unspecified address (`0.0.0.0`). + field address: IPv4; + + /// The prefix length of the network for `address`. + /// + /// NOTE: This value is derived from the value sent with `OptionCode.subnet_mask` + /// or 32 if the option is not present. + /// + /// NOTE: If the lease is acquired with DHCPINFORM this value will be zero. + field prefix_len: u8; + + /// The list of announced routers by the DHCP server. + /// + /// NOTE: This list will be derived from the value sent with `OptionCode.router`. + /// + /// NOTE: The lifetime of this array is documented on `Lease` itself. + field routers: []const IPv4; + + /// The list of DNS servers provided by the DHCP server. + /// + /// NOTE: This list will be derived from the value sent with `OptionCode.domain_name_server`. + /// + /// NOTE: The lifetime of this array is documented on `Lease` itself. + field dns_servers: []const IPv4; + + /// The complete list of options sent by the DHCP server. + /// + /// NOTE: The order inside this array is the same as it was sent by the server. + /// + /// NOTE: The lifetime of this array is documented on `Lease` itself. + /// + /// NOTE: The kernel will remove all options with `OptionCode.pad` and `OptionCode.end`. + field options: []const Option; + + /// The timestamp when the kernel applied the DHCPACK message that issued + /// this lease. + field issued_at: clock.Absolute; + + /// The timestamp when the lease should be renewed (T1). + /// + /// NOTE: If the lease was created by `DHCPINFORM`, the value will be set to + /// `clock.Absolute.infinity`. + /// + /// NOTE: If present, will be set from `OptionCode.renewal_time_value`, otherwise + /// will use the default value of 50% of the total lease time. + /// + /// NOTE: If no lease time can be derived, `clock.Absolute.infinity` is used. + field renew_at: clock.Absolute; + + /// The timestamp when the lease should be rebound (T2). + /// + /// NOTE: If the lease was created by `DHCPINFORM`, the value will be set to + /// `clock.Absolute.infinity`. + /// + /// NOTE: If present, will be set from `OptionCode.rebinding_time_value`, otherwise + /// will use the default value of 87.5% of the total lease time. + /// + /// NOTE: If no lease time can be derived, `clock.Absolute.infinity` is used. + field rebind_at: clock.Absolute; + + /// The timestamp after which the lease is not valid anymore. + /// + /// NOTE: If the lease was created by `DHCPINFORM`, the value will be set to + /// `clock.Absolute.infinity`. + /// + /// NOTE: If present, will be set from `OptionCode.ip_address_lease_time`, otherwise + /// will be set to the requested address lease time if present in `RequestLease.options`, + /// or otherwise will be set to `clock.Absolute.infinity` to mark that no lease time was + /// issued. + field valid_until: clock.Absolute; //? when does the lease expire + } + + /// Enumerations of how the kernel will handle the received lease of `RequestLease`. + enum AutoUpdateMode : u8 { + /// The kernel will not do anything with the received lease. + /// + /// NOTE: This can be used to perform a DHCP request without directly changing network + /// configuration. + item disabled = 0; + + /// The kernel will only use the `Lease.address` and `Lease.prefix_len` to + /// perform an automatic `link.AddAddress` operation on `RequestLease.interface`. + /// + /// This includes: + /// - Automatic adding/updating of the received IP address to `interface`. + /// - Automatic creation/update of the connected route for `interface`. + item address_only = 1; + + /// In addition to the effects of `address_only`, the kernel will also upsert + /// a route to the network `0.0.0.0/0` through `RequestLease.interface` for + /// all received routers. + /// + /// If multiple routers are advertised, the `Route.priority` will be staged + /// such that the last router has priority 1 and each previous router will + /// have a priority 1 higher. + /// + /// This means that the first advertised router has the highest priority (which is + /// set to the number of routers). + item address_and_route = 2; + + /// In addition to the effects of `address_only`, the kernel will also upsert + /// the received DNS servers associated with `RequestLease.interface`. + item full = 3; + } + + /// Requests new DHCP lease. Completes when at least a single server has successfully + /// provided a DHCP lease or the operation timed out. + /// + /// NOTE: Only a single `RequestLease` or `ReleaseLease` operation can be active per `interface` at the + /// same time. + /// + /// NOTE: The kernel will always use the first successful DHCPACK when multiple options are available. + /// + /// NOTE: When multiple DHCPOFFER messages are received, the kernel will perform the DHCPREQUEST process + /// in parallel for each received option unless a DHCPACK was already received. + async_call RequestLease { + /// The network interface that shall perform the DHCP request. + in interface: link.InterfaceId; + + /// The deadline until which the whole operation has to be completed. + /// + /// LORE: In contrast to many other overlapped operations, a `RequestLease` would potentially + /// never complete and it's an expected outcome that no response is received. + /// Thus, the general rule of "no timeouts in overlapped operations" is broken + /// here on purpose. + in deadline: clock.Absolute; + + /// The unicast address of the DHCP server that shall be used to obtain the DHCP lease. + /// + /// NOTE: When the unspecified address (0.0.0.0) is passed, + /// the kernel will perform a broadcast to discover potential DHCP servers. + in server: IPv4; + + /// Additional options to send with the request. + /// + /// NOTE: The kernel omits any of its own options if `options` contains at least + /// one option with the same `Option.code`. + /// + /// This allows userland to overwrite all potentially kernel-provided options. + /// + /// NOTE: The kernel will copy the options array in `overlapped.schedule`. + /// This means the user can use the memory after successful scheduling. + /// + /// NOTE: The options in this array will be sent in exactly this order *after* + /// the kernel-sent options. + /// + /// This allows userland to emit multiple instances of the same option + /// code (some encodings support this); the kernel transmits as-is. + /// + /// NOTE: The kernel will never merge any values of this array. + in options: []const Option; + + /// If `true`, the kernel will perform a DHCPINFORM process instead of a the regular + /// DHCPDISCOVER/DHCPREQUEST process. + /// + /// This allows querying local network configuration even with a non-DHCP configured IP address. + /// + /// NOTE: This requires that we already have a statically configured IP address. + /// + /// NOTE: If `server` is the unspecified address (`0.0.0.0`), the kernel will send a + /// broadcast message for each configured IPv4 addresses for `interface`. + in inform_only: bool; + + /// Defines how the kernel will update `interface` when the request is successful. + /// + /// NOTE: If the dhcp4 subsystem is enabled, this option is ignored and the kernel will + /// handle the results like it would've performed an automatic/timed DHCP request/renew. + /// + /// This means the operation can be used as a force-refresh the DHCP lease. + /// + /// NOTE: If the dhcp4 subsystem is disabled, setting this option does not imply the + /// kernel will perform automatic renewal or rebinding of the DHCP lease. + in update_mode: AutoUpdateMode; + + /// The lease provided by the server. + /// + /// NOTE: If `inform_only` is `true`, `lease.address` is `0.0.0.0` + /// and `lease.prefix_len` is zero. + out lease: Lease; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// No response was received until `deadline`. + error Timeout; + + /// An invalid value was passed as a parameter. + /// + /// This may be due to: + /// - `server` is neither the unspecified address nor a valid unicast address. + /// - `options` contains an invalid DHCP option. + /// - `deadline` is not in the future. + error InvalidValue; + + /// Another `RequestLease` or `ReleaseLease` operation is in progress for `interface`. + /// + /// NOTE: This error may "spuriously" happen when the dhcp4 subsystem is enabled + /// and currently performs an internal DHCP operation. + error InProgress; + + /// The DHCP server offered an address that is not ours, but reachable + /// through `interface`. + /// + /// NOTE: This error implies the kernel has responeded with DHCPDECLINE. + error AddressConflict; + + /// All available DHCP servers have rejected our request with `DHCPNAK`. + error Rejected; + + /// The kernel does not know a route to `server`. + /// + /// NOTE: This error can only happen when `server` is not unspecified. + error MissingRoute; + + /// The kernel cannot send the DHCPINFORM message as `interface` has no + /// assigned IPv4 address. + error MissingSourceAddress; + + /// The IPv4 subsystem is disabled on `interface`. + error SubsystemDisabled; + + /// `interface` is down and cannot send any data. + error LinkDown; + + /// There was an i/o error that lead to the failure of this operation. + error IoError; + + error SystemResources; + } + + /// Releases current lease, idempotent if none exists. + /// + /// NOTE: Only a single `RequestLease` or `ReleaseLease` operation can be active per `interface` at the + /// same time. + /// + /// NOTE: This operation will implicitly disable the dhcp4 subsystem as otherwise + /// the subsystem would immediatly try to request a new DHCP lease and `ReleaseLease` + /// would be just an implicit `RequestLease` operation. + async_call ReleaseLease { + /// The interface for which a lease should be released. + in interface: link.InterfaceId; + + /// If `true` will remove the kernel-managed lease object even in + /// the case of a communication error. + /// + /// NOTE: Each error code that will still remove the release + /// documents this. + /// + /// NOTE: If `force` is set, the scheduling implicitly cancels an + /// active `RequestLease` operation. + in force: bool; + + /// The deadline until which the whole operation has to be completed. + /// + /// LORE: In contrast to many other overlapped operations, a `RequestLease` would potentially + /// never complete and it's an expected outcome that no response is received. + /// Thus, the general rule of "no timeouts in overlapped operations" is broken + /// here on purpose. + in deadline: clock.Absolute; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + /// No response was received until `deadline`. + /// + /// NOTE: The leases was still removed when `force` was set. + error Timeout; + + /// Another `RequestLease` or `ReleaseLease` operation is in progress for `interface`. + /// + /// NOTE: This error cannot happen when `force` is set. + error InProgress; + + /// An invalid value was passed as a parameter. + /// + /// This may be due to: + /// - `deadline` is not in the future. + error InvalidValue; + + /// The IPv4 subsystem is disabled on `interface`. + /// + /// NOTE: This error implies that the interface won't have any lease anyways. + error SubsystemDisabled; + + /// `interface` is down and cannot send any data. + /// + /// NOTE: This error informs the caller that the kernel cannot + /// send the DHCPRELEASE message. + /// + /// NOTE: The leases was still removed when `force` was set. + error LinkDown; + + /// There was an i/o error that lead to the failure of this operation. + /// + /// NOTE: The leases was still removed when `force` was set. + error IoError; + + error SystemResources; + } + + /// Returns current lease or NotAvailable if none exists + syscall get_info { + /// Interface for which the lease shall be queried. + in interface: link.InterfaceId; + + /// The current lease the kernel stores for `interface`. + out info: Lease; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The kernel currently has no lease associated with `interface`. + error NotAvailable; + + /// The IPv4 subsystem is disabled on `interface`. + error SubsystemDisabled; + } + + /// Completes when new lease was set, old lease was released or expired. + /// + /// NOTE: Multiple `WaitForUpdate` can be issued which will all complete at the + /// same time when the status changes. + async_call WaitForUpdate { + /// The interface for which a new DHCP state shall be awaited. + in interface: link.InterfaceId; + + /// The new lease or `null` if the lease was removed. + out info: ?Lease; + + /// `interface` is not a valid interface id (anymore). + error InvalidInterface; + + /// The underlying `interface` was removed during this operation. + error Gone; + + error SystemResources; + } + } + + namespace dhcp6 { + //? TODO: Design the DHCPv6 kernel API + } + + namespace dns { + //? TODO: Design the DNS kernel API + + //? TODO: Keep in mind that DNS servers can be globally set or associated with an + //? interface. Also DNS servers have a lifetime as well. + } +} + +/// A file or directory on Ashet OS can be named with any legal UTF-8 sequence +/// that does not contain `/` and `:`. It is recommended to only create file names +/// that are actually typeable on the operating system though. +/// +/// File names are measured in bytes (not Unicode codepoints). A single path segment +/// must not exceed `fs.max_file_name_len` bytes. +/// +/// There are some reserved file names: +/// +/// - `.` is the "current directory" selector and does not add to the path. +/// - `..` is the "parent directory" selector and navigates up in the directory hierarchy if possible. +/// +/// Paths: +/// +/// The filesystem kernel API only accepts *relative* paths. +/// +/// A kernel path is a UTF-8 string composed of a sequence of names separated by one or more `/`. +/// +/// Syntax rules: +/// - The empty string `""` is invalid. +/// - Consecutive slashes (regex `/+`) will be compacted into a single `/`. +/// - A leading `/` is invalid. +/// - A trailing `/` is invalid. +/// - `.` is allowed and means "this directory". +/// - `..` is allowed and navigates to the parent directory. At filesystem root, `..` saturates. +/// +/// Here are some examples for valid paths: +/// - `example.txt` +/// - `docs/wiki.txt` +/// - `system/fonts/../config.ini` +/// +/// NOTE: Absolute paths and filesystem designators like `SYS:/foo` are userland concepts. +/// +/// The canonical format for absolute paths in userland is a filesystem name, followed by a `:/`, +/// then a regular path relative to the root directory of the filesystem. +/// +/// Examples: +/// - `SYS:/apps/editor/code` +/// - `USB0:/foo/../bar` (which is equivalent to `USB0:/bar`) +/// +/// +/// NOTE: The filesystem that is used to boot the OS from has an alias `SYS:` that +/// is always a legal way to address this file system. +/// +/// NOTE: Reserved names can never exist as actual directory entries. +/// +/// NOTE: There is a limit on how long a file/directory name can be, but there's no limit +/// on how long a path can be. +/// This means there is no implicit directory nesting limit. +/// +/// NOTE: A file system identifier uses the following rules for names: +/// - Allowed characters: `[A-Z0-9\.]` +/// - Must start with a letter. +/// - Must not end with `.`. +/// +/// Examples: +/// - `SYS` +/// - `USB0.2` +/// - `NFS1` +/// +/// NOTE: Overlapped operations scheduled against the same underlying filesystem object +/// are ordered deterministically (FIFO-equivalent semantics), even across multiple +/// system resources. +/// The kernel may perform safe internal optimizations as long as the observable +/// semantics match the scheduling order. +/// +/// NOTE: Filesystem operations are write-through. On successful completion of an +/// operation, the change is committed to the underlying storage. +namespace fs { + /// The maximum number of bytes in a file system identifier name. + /// + /// This is chosen to be a power of two, and long enough to accommodate + /// typical file system names: + /// - `SYS` + /// - `USB0` + /// - `USB10` + /// - `USB10.3` + /// - `PF0` + /// - `CF7` + const max_fs_name_len = 8; + + /// The maximum number of bytes in a file system type name. + /// + /// Chosen to be a power of two, and long enough to accomodate typical names: + /// - `FAT16` + /// - `FAT32` + /// - `exFAT` + /// - `NTFS` + /// - `ReiserFS` + /// - `ISO 9660` + /// - `btrfs` + /// - `AFFS` + const max_fs_type_len = 32; + + /// The maximum number of bytes in a file name. + /// + /// LORE: This was chosen based off a survey of my local file system + /// and checking what kind of files exists. + /// As some programs use sha256 checksums for file names and 64 bytes + /// are enough to store a hex-encoded 256 bit sequence (`114ac2caf8fefad1116dbfb1bd68429f68e9e088b577c9b3f5a3ff0fe77ec886`), + /// the initial choice was 64 byte. + /// + /// With the invention of Ashet FS, a 64 byte string was possible, but would've wasted + /// 56 bytes of padding space due to two 32 bit pointers inside the same file system node, + /// the limit was raised to 120 characters. + /// + /// With 120 characters, we're settled well for basically all realistic file names + /// encountered in the wild. + const max_file_name_len = 120; + + /// Identifies a mounted filesystem instance known to the kernel. + /// + /// NOTE: File system ids are allocated in a monotonically increasing + /// way, and will be stable until a file system is unmounted/removed + /// from the kernel. + /// + /// NOTE: Except `system`, the enumeration order of file systems is unspecified + /// and the ids cannot be assumed stable between reboots. + enum FileSystemId : u32 { + /// The filesystem the OS booted from. + item system = 0; + + /// All other ids are unique file systems. + ... + } + + /// Enumerates all currently available filesystems. + syscall enumerate_filesystems { + /// If not `null`, will receive filesystem ids. + in list: ?[]FileSystemId; + + /// The number of ids written to `list`, or the total count if `list` is `null`. + out count: usize; + } + + /// Finds a filesystem by name. + /// + /// NOTE: Name matching is case-sensitive. + /// + /// NOTE: The passed `name` may include a trailing `:`. + syscall find_filesystem { + in name: str; + + out id: FileSystemId; + + /// No filesystem exists with the given name. + error NotFound; + + /// `name` does not conform to the name rules for filesystems. + error InvalidName; + } + + struct FileSystemInfo { + /// System-unique id of this file system + field id: FileSystemId; + + /// Compressed infos about the file system + field flags: Flags; + + /// User-addressable file system identifier (e.g. `SYS`, `USB0`, ...). + /// + /// Encoding: + /// - UTF-8 + /// - NUL-padded (first NUL determines length, otherwise full array). + field name: [max_fs_name_len]u8; + + /// String identifier of a file system driver (e.g. `FAT32`, `NFS`, ...) + /// + /// Encoding: + /// - UTF-8 + /// - NUL-padded (first NUL determines length, otherwise full array). + field filesystem: [max_fs_type_len]u8; + + bitstruct Flags : u16 { + /// This is the system boot filesystem. + field system: bool; + + /// The file system can be removed by the user. + field removable: bool; + + /// The filesystem is immutable and cannot be modified. + field immutable: bool; + + reserve u13 = 0; + } + } + + /// Queries information about a filesystem. + syscall get_filesystem_info { + in fs_id: FileSystemId; + out info: FileSystemInfo; + + /// The given filesystem id does not exist. + error InvalidFileSystem; + + error SystemResources; + } + + //? TODO: a way to query free space / capacity (if you want userland UIs to show disk usage without filesystem-specific driver calls) + + /// A directory is a group of files and other directories in a file system. + resource Directory { } + + /// Opens a directory relative to the root of a filesystem. + async_call Mount { + in fs_id: FileSystemId; + + /// The directory path relative to the root of the filesystem. + /// + /// NOTE: Passing `"."` yields the root directory handle. + in path: str; + + out dir: Directory; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The requested entry does not exist. + error FileNotFound; + + /// The given filesystem id did not exist when scheduling the operation. + error InvalidFileSystem; + + /// The given `path` is syntactically invalid. + error InvalidPath; + + /// The requested entry exists but is not a directory. + error NotADir; + + /// The underlying filesystem of `fs_id` was removed + /// during the creation of the directory. + error Gone; + + error SystemResources; + } + + /// Opens a directory relative to `start_dir`. + async_call OpenDir { + in start_dir: Directory; + + /// The directory path relative to `start_dir`. + in path: str; + + out dir: Directory; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The requested entry does not exist. + error FileNotFound; + + /// `start_dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `start_dir` was removed. + error Gone; + + /// The given `path` is syntactically invalid. + error InvalidPath; + + /// The requested entry exists but is not a directory. + error NotADir; + + error SystemResources; + } + + /// Writes the name of a directory into `*name_out`. + /// + /// NOTE: For the filesystem root directory, the returned name is `"."`. + /// + /// NOTE: This syscall does not require filesystem I/O. + syscall get_dir_name { + in dir: Directory; + in name_out: *FileName; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + } + + /// Returns the filesystem id a directory resides on. + /// + /// NOTE: This syscall does not require filesystem I/O. + syscall get_dir_filesystem { + in dir: Directory; + out fs_id: FileSystemId; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + } + + /// Returns true if `dir` is the root directory of its filesystem. + /// + /// NOTE: This syscall does not require filesystem I/O. + syscall is_root_dir { + in dir: Directory; + out is_root: bool; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + } + + /// A directory enumerator allows enumerating directory entries. + /// + /// Enumeration properties: + /// - The enumeration order is unspecified. + /// - The enumerator is not rewindable/resettable. + /// - The enumerator is not a snapshot. + /// + /// Determinism rule: + /// - Enumeration is deterministic under scheduling. + /// - The kernel tracks mutations and enumerators to ensure an entry is never returned twice. + /// - An enumerator may over-enumerate (return entries that are deleted later), + /// but must never under-enumerate (skip entries that exist). + resource DirEnumerator { } + + /// Information about a filesystem entry (file or directory). + /// + /// NOTE: This structure intentionally does not include the entry name. + /// APIs that enumerate directory items accept an optional `FileName*` + /// output for the name. + struct FileInfo { + /// The size in bytes. + /// + /// NOTE: For directories, this value is always zero. + field size: u64; + + /// Timestamp of the creation time of the file. + /// + /// NOTE: Only valid when `flags.creation_date_valid` is true. + field creation_date: datetime.DateTime; + + /// Timestamp of the last modification time of the file. + /// + /// NOTE: Only valid when `flags.modified_date_valid` is true. + field modified_date: datetime.DateTime; + + /// Additional packed information. + field flags: Flags; + + enum FileType : u2 { + item file = 0; + item directory = 1; + } + + bitstruct Flags : u16 { + /// Entry type. + field type: FileType; + + /// `creation_date` is valid. + field creation_date_valid: bool; + + /// `modified_date` is valid. + field modified_date_valid: bool; + + reserve u12 = 0; + } + } + + /// A fixed-size file name buffer. + /// + /// The file name bytes are stored in `bytes[0..len]`. + /// Bytes beyond `len` are unspecified. + /// + /// NOTE: `len` is always `<= max_file_name_len`. + struct FileName { + field len: u8; + field bytes: [max_file_name_len]u8; + } + + /// Creates a directory enumerator for `dir`. + syscall create_enumerator { + in dir: Directory; + + out enumerator: DirEnumerator; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + + error SystemResources; + } + + /// Returns information about the next entry in a directory enumeration. + /// + /// If `name_out` is not `null`, the kernel writes the entry name into `*name_out`. + /// The `name_out` pointer must remain valid until the operation completes. + async_call GetNextDirItem { + in enumerator: DirEnumerator; + + /// Optional output buffer for the entry name. + in name_out: ?*FileName; + + /// The information about the directory entry. + out info: FileInfo; + + /// Returned when the enumerator reached the end of the directory. + /// + /// NOTE: The `enumerator` resource should be destroyed after this error is + /// returned as there is no way to unwind an enumerator resource. + error EndOfDirectory; + + /// `enumerator` is not a valid enumerator resource. + error InvalidHandle; + + /// The underlying filesystem of `enumerator` was removed. + error Gone; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + error SystemResources; + } + + /// Deletes a filesystem entry by path. + /// + /// Active handle rule: + /// - If the target (or any descendant when `recurse=true`) has an active handle + /// (`File`, `Directory`, `Location`, or `DirEnumerator`), the operation fails with `ActiveHandle`. + async_call Delete { + in dir: Directory; + + /// The path relative to `dir`, points to the file system entry that shall + /// be deleted. + in path: str; + + /// Defines if the operation should recursively delete a directory if + /// `path` points to a directory resource. + /// + /// - `false`: Deleting a non-empty directory fails with `DirectoryNotEmpty`. + /// - `true`: Directories are deleted recursively. + in recurse: bool; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The requested entry does not exist. + error FileNotFound; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + + /// The given `path` is syntactically invalid. + error InvalidPath; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + /// The target is a non-empty directory and `recurse` is false. + error DirectoryNotEmpty; + + /// The operation conflicts with existing active handles. + error ActiveHandle; + + /// The filesystem is immutable and cannot be modified. + error ImmutableFileSystem; + + error SystemResources; + } + + /// Creates a new directory relative to `dir`. + async_call MkDir { + in dir: Directory; + + /// A path relative to `dir` which points to the directory that shall be created. + /// + /// NOTE: If `path` contains subdirectories, missing intermediate directories are created. + in path: str; + + /// If `true`, the operation will return a directory handle. + in mkopen: bool; + + /// Optional handle to the created directory. + /// + /// If `mkopen` is true, `new_dir` receives an opened handle to the created directory. + /// If `mkopen` is false, `new_dir` is returned as `null`. + out new_dir: ?Directory; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The target path was expected to be non-existent, but an entry exists. + error Exists; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + + /// The given `path` is syntactically invalid. + error InvalidPath; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + /// There is not enough free space on the filesystem. + error NoSpaceLeft; + + /// The filesystem is immutable and cannot be modified. + error ImmutableFileSystem; + + error SystemResources; + } + + /// Queries a filesystem entry by path. + async_call StatEntry { + in dir: Directory; + in path: str; + out info: FileInfo; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The requested entry does not exist. + error FileNotFound; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + + /// The given `path` is syntactically invalid. + error InvalidPath; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + error SystemResources; + } + + /// Renames or moves an entry within the same filesystem. + /// + /// NOTE: This operation is logically atomic under scheduling: + /// operations scheduled after it observe the new name/location; + /// operations scheduled before it observe the old name/location. + /// + /// NOTE: This is a cheap operation and does not require the copying of data. + /// + /// NOTE: This operation does not support replacing an existing destination. + async_call NearMove { + /// The directory defining the base for both the source + /// and destination of the rename operation. + in dir: Directory; + + /// Path relative to `dir` that points to the current file or directory name. + in src_path: str; + + /// Path relative to `dir` that points to the new file or directory name. + in dst_path: str; + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The destination path was expected to be non-existent, but an entry exists. + error Exists; + + /// The source entry does not exist. + error FileNotFound; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + + /// A passed path is syntactically invalid. + error InvalidPath; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + /// There is not enough free space on the filesystem (e.g. directory entry allocation). + error NoSpaceLeft; + + /// The filesystem is immutable and cannot be modified. + error ImmutableFileSystem; + + error SystemResources; + } + + + /// Moves an entry between unrelated directories. + /// + /// Atomicity: + /// - The move is all-or-nothing unless an `IoError` prevents full atomicity. + /// - On failures like `NoSpaceLeft`, the kernel attempts to roll back changes. + /// + /// Active handle rule: + /// - The kernel checks for `ActiveHandle` recursively before starting the move. + /// - While the operation is active, moved entries are treated as not visible at the source. + /// - The destination becomes visible only on successful completion. + /// + /// NOTE: This operation can move between different filesystems and may copy data. + /// + /// NOTE: If `src_dir` and `dst_dir` are in the same file system, the kernel may perform + /// the move operation with the efficiency of `NearMove`, but the `ActiveHandle` checks + /// are still performed. + async_call FarMove { + /// The directory that defines the source file system. + in src_dir: Directory; + + /// Path relative to `src_dir` that defines which entry should be moved. + in src_path: str; + + /// The directory that defines the destination file system. + in dst_dir: Directory; + + /// Path relative to `dst_dir` that defines where the entry should be moved. + in dst_path: str; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The destination path was expected to be non-existent, but an entry exists. + error Exists; + + /// The source entry does not exist. + error FileNotFound; + + /// One of the directory handles is invalid. + error InvalidHandle; + + /// The underlying filesystem of a passed handle was removed. + error Gone; + + /// A passed path is syntactically invalid. + error InvalidPath; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + /// The operation conflicts with existing active handles. + error ActiveHandle; + + /// There is not enough free space on the destination filesystem. + error NoSpaceLeft; + + /// A involved filesystem is immutable and cannot be modified. + error ImmutableFileSystem; + + error SystemResources; + } + + /// Copies an entry between unrelated directories. + /// + /// NOTE: This operation can copy between different filesystems. + /// + /// Atomicity: + /// - The copy is all-or-nothing unless an `IoError` prevents full atomicity. + /// - On failures like `NoSpaceLeft`, the kernel attempts to roll back changes. + async_call Copy { + /// The directory that defines the base of `src_path`. + in src_dir: Directory; + + /// A path relative to `src_dir` that defines the entry which shall be copied. + in src_path: str; + + /// The directory that defines the base of `dst_path`. + in dst_dir: Directory; + + /// A path relative to `dst_dir` that defines the target of the copy operation. + in dst_path: str; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The destination path was expected to be non-existent, but an entry exists. + error Exists; + + /// The source entry does not exist. + error FileNotFound; + + /// One of the directory handles is invalid. + error InvalidHandle; + + /// The underlying filesystem of a passed handle was removed. + error Gone; + + /// A passed path is syntactically invalid. + error InvalidPath; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + /// There is not enough free space on the destination filesystem. + error NoSpaceLeft; + + /// A involved filesystem is immutable and cannot be modified. + error ImmutableFileSystem; + + error SystemResources; + } + + /// A file is a handle to a binary data storage that is stored + /// in a file system. + /// + /// NOTE: Files are byte-addressed and accessed by explicit offsets. + resource File { } + + enum FileAccess : u8 { + item read_only = 0; + item write_only = 1; + item read_write = 2; + } + + enum FileMode : u8 { + /// Opens file when it exists on disk + item open_existing = 0; + + /// Creates file when it does not exist, or opens the file without truncation. + item open_always = 1; + + /// Creates file when there is no file with that name + item create_new = 2; + + /// Creates file when it does not exist, or opens the file and truncates it to zero length + item create_always = 3; + } + + /// Opens a file relative to `dir`. + /// + /// NOTE: Opening a directory path as a file fails with `NotAFile`. + async_call OpenFile { + in dir: Directory; + in path: str; + + /// Defines what access to the file is desired. + in access: FileAccess; + + /// Defines how the open operation should handle non-existing/existing files. + in mode: FileMode; + + out handle: File; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The requested entry does not exist. + error FileNotFound; + + /// The target path was expected to be non-existent, but an entry exists. + error Exists; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + + /// The given `path` is syntactically invalid. + error InvalidPath; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + /// The requested entry exists but is not a file. + error NotAFile; + + /// There is not enough free space on the filesystem. + error NoSpaceLeft; + + /// The filesystem is immutable and cannot be modified. + /// + /// NOTE: This error is only returned when `access` requests write access. + error ImmutableFileSystem; + + error SystemResources; + } + + /// Writes the basename of a file into `*name_out`. + /// + /// NOTE: This syscall does not require filesystem I/O. + syscall get_file_name { + in file: File; + + in name_out: *FileName; + + /// `file` is not a valid file resource. + error InvalidHandle; + + /// The underlying filesystem of `file` was removed. + error Gone; + } + + /// Reads data from a file at `offset` into `buffer`. + /// + /// NOTE: Multiple `Read` and `Write` operations can be scheduled at the same time + /// and may complete concurrently. + async_call Read { + in file: File; + + /// The offset of the read operation in bytes from the beginning of the file. + in offset: u64; + + /// The buffer which shall receive the read data. + /// NOTE: `buffer` must stay valid until the operation completes. + in buffer: bytebuf; + + /// The number of bytes written to `buffer`. + /// + /// NOTE: This is only ever less than `buffer.len` if the + /// read operation would read over the end of the file. + /// + /// If that is the case, `count` is computed as: + /// `count = file_size -| offset`. + out count: usize; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// `file` is not a valid file resource. + error InvalidHandle; + + /// The underlying filesystem of `file` was removed. + error Gone; + + error SystemResources; + } + + /// Writes data to a file at `offset` from `buffer`. + /// + /// NOTE: Writes never extend a file. File growth is only possible via `Resize`. + /// + /// NOTE: Multiple `Read` and `Write` operations can be scheduled at the same time + /// and may complete concurrently. + async_call Write { + in file: File; + + /// The offset of the write operation in bytes from the beginning of the file. + in offset: u64; + + /// The data that shall be written to the file. + in buffer: bytestr; + + /// The number of bytes written to file. + /// + /// NOTE: This is only ever less than `buffer.len` if the + /// write operation would write over the end of the file. + /// + /// If that is the case, `count` is computed as: + /// `count = file_size -| offset`. + out count: usize; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// `file` is not a valid file resource. + error InvalidHandle; + + /// The underlying filesystem of `file` was removed. + error Gone; + + /// The file handle does not permit writing. + error WriteProtected; + + /// The filesystem is immutable and cannot be modified. + error ImmutableFileSystem; + + error SystemResources; + } + + /// Queries information about an opened file. + async_call StatFile { + in file: File; + out info: FileInfo; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// `file` is not a valid file resource. + error InvalidHandle; + + /// The underlying filesystem of `file` was removed. + error Gone; + + error SystemResources; + } + + /// Resizes a file to `length` bytes. + /// + /// Growth properties: + /// - The filesystem must physically allocate storage (no sparse/overcommitted growth). + /// - Newly allocated bytes are zero-filled. + /// + /// Shrink properties: + /// - Shrinking is immediate and permanent. + /// - If the file is grown again later, the new bytes are zero. + /// + /// NOTE: Can be also used to truncate a file to zero length. + async_call Resize { + in file: File; + in length: u64; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// `file` is not a valid file resource. + error InvalidHandle; + + /// The underlying filesystem of `file` was removed. + error Gone; + + /// There is not enough free space on the filesystem. + error NoSpaceLeft; + + /// The file handle does not permit writing. + error WriteProtected; + + /// The filesystem is immutable and cannot be modified. + error ImmutableFileSystem; + + error SystemResources; + } + + /// A filesystem location is a `Directory` plus an associated relative path. + /// + /// This resource is used to transport a filesystem location across process boundaries, + /// including potentially non-existent targets (e.g. the output parameter inside Process arguments). + /// + /// A location can be opened/used similar to how a `(dir, filename)` pair + /// can be used. + /// + /// LORE: This type was introduced as a solution on how to pass file names + /// over a command line interface into an application. + /// As Ashet OS prefers relative paths to known directory handles over + /// absolute paths, a shell still needs the ability to pass non-existing + /// locations for parameters like `--output=…`. + /// Thus, the `Location` type was introduced which fuses a directory together + /// with a relative path. + resource Location { } + + /// Specifies how a `Location` should be interpreted by consumers. + enum LocationIntent : u8 { + /// The location may refer to a file or a directory. + item any = 0; + + /// The location should be treated as a file. + item file = 1; + + /// The location should be treated as a directory. + item directory = 2; + } + + /// Creates a new `Location` from `dir` and `path`. + /// + /// The stored path is normalized and syntactically resolved. + /// + /// NOTE: The path stored in a `Location` is normalized and syntactically resolved: + /// - Repeated separators are collapsed. + /// - `.` components are removed. + /// - `..` cancels a preceding non-`..` component (`x/../y` becomes `y`). + /// - If the resolved path would be empty, it is stored as `"."`. + /// + /// This is purely syntactical processing and does not touch the filesystem. + syscall create_location { + /// The directory that is the base of our location. + in dir: Directory; + + /// The path relative to `dir` which the location describes. + in path: str; + + /// The intent for the file system location. + /// + /// NOTE: This can be queried with `get_location_intent`. + in intent: LocationIntent; + + out loc: Location; + + /// `dir` is not a valid directory resource. + error InvalidHandle; + + /// The underlying filesystem of `dir` was removed. + error Gone; + + /// The given `path` is syntactically invalid. + error InvalidPath; + + error SystemResources; + } + + /// Returns the intent stored inside a `Location`. + syscall get_location_intent { + in loc: Location; + out intent: LocationIntent; + + /// `loc` is not a valid location resource. + error InvalidHandle; + + /// The underlying filesystem of `loc` was removed. + error Gone; + } + + /// Returns a clone of the base directory stored in a `Location`. + syscall get_location_dir { + in loc: Location; + out dir: Directory; + + /// `loc` is not a valid location resource. + error InvalidHandle; + + /// The underlying filesystem of `loc` was removed. + error Gone; + + error SystemResources; + } + + /// Returns the normalized, resolved path stored in a `Location`. + /// + /// NOTE: The returned string remains valid as long as `loc` is not destroyed. + syscall get_location_path { + in loc: Location; + out path: str; + + /// `loc` is not a valid location resource. + error InvalidHandle; + + /// The underlying filesystem of `loc` was removed. + error Gone; + } + + /// Opens the `Location` as a directory. + /// + /// NOTE: This is the `Location` variant of `OpenDir`. + async_call OpenAsDirectory { + in loc: Location; + out dir: Directory; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The requested entry does not exist. + error FileNotFound; + + /// `loc` is not a valid location resource. + error InvalidHandle; + + /// The underlying filesystem of `loc` was removed. + error Gone; + + /// The requested entry exists but is not a directory. + error NotADir; + + error SystemResources; + } + + /// Opens the `Location` as a file. + /// + /// NOTE: This is the `Location` variant of `OpenFile`. + async_call OpenAsFile { + in loc: Location; + in access: FileAccess; + in mode: FileMode; + out file: File; + + /// The underlying storage subsystem had an I/O failure. + error IoError; + + /// The requested entry does not exist. + error FileNotFound; + + /// The target path was expected to be non-existent, but an entry exists. + error Exists; + + /// `loc` is not a valid location resource. + error InvalidHandle; + + /// The underlying filesystem of `loc` was removed. + error Gone; + + /// The requested entry exists but is not a file. + error NotAFile; + + /// A path traversal expected a directory, but found a non-directory entry. + error NotADir; + + /// There is not enough free space on the filesystem. + error NoSpaceLeft; + + /// The filesystem is immutable and cannot be modified. + /// + /// NOTE: This error is only returned when `access` requests write access. + error ImmutableFileSystem; + + error SystemResources; + } +} + +/// This namespace contains items related to shared memory objects. +namespace shm { + /// A shared memory object which, for its livetime, provides + /// a memory region which can be read and modified. + /// + /// NOTE: The memory region is valid until the resource is destroyed. + resource SharedMemory { } + + /// Constructs a new shared memory object with `size` bytes of memory. + /// Shared memory 1can be written without any memory protection. + /// + /// NOTE: The shared memory region will not be initialized by the kernel + /// so the content after creation is unspecified. + /// It should be set to the desired contents by the initial creator. + syscall create { + /// Number of bytes for the shared memory region. + /// The operation will fail when `0` is passed. + in size: usize; + + /// The created shared memory object. + out handle: SharedMemory; + + /// Returned when `size` is 0. + error InvalidSize; + + error SystemResources; + } + + /// Returns the memory region for the shared memory object. + /// + /// NOTE: The memory returned by this function is valid until the `handle` object is destroyed. + syscall get_memory { + in handle: SharedMemory; + + /// The memory region of the shared memory object. + out memory: []align(16) u8; + + /// The `handle` is not a valid shared memory object. + error InvalidHandle; + } +} + +/// This namespace contains items related to data pipes. +namespace pipe { + + /// A pipe is a two-ended, one-directional communication + /// channel which can either transport data streams or packets. + /// + /// Pipes can be synchronous or buffered: + /// - A synchronous pipe can only transfer data if a `Read` and a `Write` are active + /// at the same time. + /// - A buffered pipe has an internal memory which can store some elements + /// and makes `Read` and `Write` independent of each other. + /// + /// NOTE: Pipes will never transfer partial elements. + /// + /// NOTE: If multiple `Read` and `Write` operations are scheduled, the kernel + /// will process them in a FIFO manner. + /// This means that no interleaving between multiple `Write` operations will happen. + /// + /// NOTE: If a pipe is synchronous, a `Read` or `Write` operation can only complete + /// when a concurrent opposite operation is active. + /// The kernel will transfer the data directly from a `Write` operation into the + /// buffer of a `Read` operation without storing elements in kernel memory. + /// + /// NOTE: If a pipe is synchronous, and a `Read` uses `PipeMode.at_least_one`, it will + /// consume the maximum possible amount of elements from a single `Write`, but will + /// not merge data from multiple `Write` operations. + /// + /// NOTE: If a pipe is buffered, and a `Read` operation uses `PipeMode.at_least_one`, the + /// operation will consume a maximum of `fifo_length` elements, even if `Read.buffer` + /// could store more elements. + /// + /// NOTE: The `PipeMode` of a `Read` or `Write` operation only affects the operation itself + /// and will never affect other concurrently scheduled operations. + resource Pipe { } + + /// Creates a new pipe with `fifo_length` elements of `element_size` bytes. + /// If `fifo_length` is 0, the pipe is synchronous and can only send data + /// if a `Read` call is active. Otherwise, up to `fifo_length` elements can be + /// stored in a FIFO. + syscall create { + /// The size of the primitives in bytes the pipe operates on. Each element + /// transferred by the pipe has this size. + /// + /// NOTE: An elements size of 1 is making the pipe byte-oriented. + /// This can be mentally seen as data streaming instead of + /// packet oriented transmission. + /// + /// NOTE: An elements size of 0 is illegal and returns an error. + in element_size: usize; + + /// The number of elements that can be buffered inside the pipe before + /// making `Write` blocking. + /// + /// Passing 0 here makes the pipe a synchronous pipe, + /// any other value makes the pipe buffered. + in fifo_length: usize; + + /// The newly created pipe resource. + out handle: Pipe; + + error SystemResources; + + /// Returned when `element_size == 0`. + error InvalidSize; + } + + /// Returns the length of the pipe-internal FIFO in elements. + syscall get_fifo_length { + /// The pipe which should be queried. + in handle: Pipe; + + /// The length of the FIFO in elements. + out length: usize; + + /// `handle` is not a valid pipe resource. + error InvalidHandle; + } + + /// Returns the size of the elements stored in the pipe. + syscall get_element_size { + /// The pipe which should be queried. + in handle: Pipe; + + /// The size of the elements in bytes. + out size: usize; + + /// `handle` is not a valid pipe resource. + error InvalidHandle; + } + + enum PipeMode : u8 { + /// Completes immediately even if no elements could be processed. + /// NOTE: This means that `Read.count` or `Write.count` can be zero after completion. + item nonblocking = 0; + + /// Returns when at least one element could be processed. + /// NOTE: This means that `Read.count` or `Write.count` is at least one after completion + /// unless `data` or `buffer` do not hold a single element. + item at_least_one = 1; + + /// Returns only when all elements are processed. + /// NOTE: This means that `Read.count` or `Write.count` are at the maximum possible value + /// derived from `stride` and `data.len`/`buffer.len`. + item all = 2; + } + + /// Writes elements from `data` into the given pipe. + /// + /// NOTE: The number of elements inside `data` is computed by `(data.len - element_size + 1) / stride`. + async_call Write { + in handle: Pipe; + + /// Pointer to the first element. Length defines how many elements are to be transferred. + /// + /// NOTE: If `data.len < element_size`, the operation transfers 0 elements and completes + /// immediately. + in data: bytestr; + + /// Distance in bytes between each element in `data`. Can be different from the pipe's element + /// size to allow sparse data to be transferred. + /// + /// NOTE: If `0` is passed, `stride` will be set to the `element_size` property of the pipe. + /// + /// NOTE: It is legal to pass a `stride` smaller than `element_size`. This will copy elements + /// which are overlapping. + in stride: usize; + + /// Defines how the write should operate. + in mode: PipeMode; + + /// Number of elements written into the pipe. + out count: usize; + + /// `handle` is not a valid pipe resource. + error InvalidHandle; + } + + /// Reads elements from a pipe into `buffer`. + /// + /// NOTE: The max. number of elements written to `buffer` is computed by `(buffer.len - element_size + 1) / stride`. + async_call Read { + in handle: Pipe; + + /// Points to the first element to be received. + /// + /// NOTE: The kernel will only write chunks of `element_size` in steps of `stride` bytes. + /// It will not write any other part of the buffer. + /// + /// NOTE: `BufferSize` is returned if `buffer.len < element_size`. + in buffer: bytebuf; + + /// Distance between each element in `buffer`. Can be different from the pipe's element size + /// to allow sparse data to be transferred. + /// + /// NOTE: If `0` is passed, `stride` will be set to the `element_size` property of the pipe. + /// + /// NOTE: It is legal to pass a `stride` smaller than `element_size`. This will write elements + /// which are overlapping inside `buffer`. If this is the case, only the last element + /// written is complete. + in stride: usize; + + /// Defines how the read should operate. + in mode: PipeMode; + + /// Number of elements written to `buffer`. + out count: usize; + + /// `handle` is not a valid pipe resource. + error InvalidHandle; + + /// `buffer.len` is smaller than `element_size`. + error BufferSize; + } +} + +/// This namespace contains items related to synchronization between multiple threads. +namespace sync { + /// A mutex implements an object which can be locked and unlocked. + /// + /// NOTE: As Ashet OS is cooperatively scheduled, it is not necessary to guard an access/operation + /// with a mutex without a scheduler yield. + /// This means using a mutex is only sensible when access to a certain resource should be guarded + /// over scheduler yield points. + /// + /// LORE: In contrast to most other operating systems, a mutex in Ashet OS isn't tied + /// to a thread or a process, but is a regular system resource that can be passed + /// around and can be shared between several processes and threads. + /// + /// This means that the concept of a "recursive mutex" doesn't make sense, as a mutex + /// has no knowledge of the locking thread. + resource Mutex { } + + /// Creates a new mutex. + syscall create_mutex { + out mutex: Mutex; + + error SystemResources; + } + + /// Tries to lock a mutex and returns if it was successful. + syscall try_lock { + /// The mutex that shall be locked. + in mutex: Mutex; + + /// `true` if the lock was successful, `false` otherwise. + out is_locked: bool; + + /// `mutex` is not a valid mutex resource. + error InvalidHandle; + } + + /// Unlocks a mutex. + /// + /// Completes the oldest pending `Lock` operation if one exists. + syscall unlock { + in mutex: Mutex; + + /// `mutex` is not a valid mutex resource. + error InvalidHandle; + + //? TODO: Consider "NotLocked" error to make it possible to detect + //? programming errors + } + + /// Locks a mutex. Will complete once the mutex is locked. + async_call Lock { + /// The mutex that shall be locked. + in mutex: Mutex; + + /// `mutex` is not a valid mutex resource. + error InvalidHandle; + } + + /// A sync-event is an edge-triggered notification mechanism that + /// can synchronize multiple actors. + resource SyncEvent { } + + /// Creates a new `SyncEvent` object that can be used to synchronize + /// different processes. + syscall create_event { + /// The created SyncEvent resource. + out event: SyncEvent; + error SystemResources; + } + + /// Completes the oldest pending `WaitForEvent` operation waiting for the given event. + /// + /// NOTE: If currently no `WaitForEvent` operation is pending on `event`, + /// the notification is lost. + syscall notify_one { + /// The event that shall be notified. + in event: SyncEvent; + + /// `true` if the notification completed a pending `WaitForEvent` operation, otherwise `false`. + out received: bool; + + /// `event` is not a valid sync event. + error InvalidHandle; + } + + /// Completes all `WaitForEvent` operations waiting for the given event. + /// + /// NOTE: If currently no `WaitForEvent` operation is pending on `event`, + /// the notification is lost. + syscall notify_all { + /// The event that shall be notified. + in event: SyncEvent; + + /// The number of completed `WaitForEvent` operations. + /// + /// NOTE: If `received == 0`, the notification was lost. + out received: usize; + + /// `event` is not a valid sync event. + error InvalidHandle; + } + + /// Waits for the given `SyncEvent` to be notified. + async_call WaitForEvent { + /// The event which shall be awaited. + in event: SyncEvent; + + /// `event` is not a valid sync event. + error InvalidHandle; + } +} + +/// This namespace contains items related to graphics rendering. +namespace draw { + /// A font is required to render text and defines how + /// glyphs are drawn. + resource Font { } + + /// A framebuffer is something that can be drawn on. + resource Framebuffer { } + + enum FramebufferType : u8 { + /// A pure in-memory frame buffer used for off-screen rendering. + item memory = 0; + + /// A video device backed frame buffer. Can be used to paint on a screen + /// directly. + item video = 1; + + /// A frame buffer provided by a window. These frame buffers + /// may hold additional semantic information. + item window = 2; + + /// A frame buffer provided by a user interface element. These frame buffers + /// may hold additional semantic information. + item widget = 3; + } + + /// Returns the font for the given font name, if any. + /// + /// NOTE: System fonts are fonts that are either embedded in the kernel or + /// automatically loaded from the `SYS:/system/fonts` folder on + /// boot. + /// + /// NOTE: The returned resource can be unbound, but cannot be destroyed. + /// A `resources.destroy` operation will unbind the font resource from all + /// processes, effectively invalidating this userland handle. + /// + /// The underlying kernel resource won't be destroyed. + syscall get_system_font { + /// The name of the system font. + in font_name: str; + + //? TODO: Add a way to hint font sizes for vector fonts. + + /// The resource handle of the system font. + out handle: Font; + + /// No system font with the given name exists. + error FileNotFound; + + error SystemResources; + } + + /// Creates a new custom font from the given data. + syscall create_font { + /// The encoded font data for a bitmap or vector format. + /// + /// TODO: Specify which font formats are allowed. + in data: bytestr; + + //? TODO: Add a way to hint font sizes for vector fonts. + + /// A font resource that represents the font inside `data`. + out handle: Font; + + /// `data` does not encode a valid font. + error InvalidData; + + error SystemResources; + } + + /// Returns true if the given font is a system-owned font. + syscall is_system_font { + in font: Font; + + /// `true` if `font` is a system font resource, otherwise `false`. + out system_font: bool; + + /// `font` is not a valid font resource. + error InvalidHandle; + } + + /// Measures the size of a text string. + /// + /// NOTE: This function accepts strings using the LF line separator + /// and will return the height of all lines and the width of + /// the longest line. + syscall measure_text_size { + in font: Font; + in text: str; + out size: Size; + + /// `font` is not a valid font resource. + error InvalidHandle; + } + + /// Creates a new in-memory framebuffer that can be used for off-screen painting. + /// + /// NOTE: The contents of the newly created framebuffer are unspecified. + syscall create_memory_framebuffer { + /// The size of the created framebuffer in pixels. + in size: Size; + + out handle: Framebuffer; + + /// Returned when `size.width` or `size.height` are zero. + error InvalidSize; + + error SystemResources; + } + + /// Creates a new framebuffer based off a video output. Can be used to output pixels + /// to the screen. + /// + /// NOTE: The returned `handle` is destroyed automatically when `output` + /// is destroyed. + syscall create_video_framebuffer { + in output: video.VideoOutput; + out handle: Framebuffer; + + /// `output` is not a valid video output resource. + error InvalidHandle; + + error SystemResources; + } + + /// Creates a new framebuffer that allows painting into a GUI window. + /// + /// NOTE: The returned `handle` is destroyed automatically when `window` + /// is destroyed. + syscall create_window_framebuffer { + in window: gui.Window; + out handle: Framebuffer; + + /// `window` is not a valid window resource. + error InvalidHandle; + + error SystemResources; + } + + /// Creates a new framebuffer that allows painting into a widget. + /// + /// NOTE: The returned `handle` is destroyed automatically when `widget` + /// is destroyed. + syscall create_widget_framebuffer { + in widget: gui.Widget; + out handle: Framebuffer; + + /// `widget` is not a valid widget resource. + error InvalidHandle; + + error SystemResources; + } + + /// Returns the type of a framebuffer object. + syscall get_framebuffer_type { + in fb: Framebuffer; + + /// The type of framebuffer `fb` is. + out type: FramebufferType; + + /// `fb` is not a valid framebuffer resource. + error InvalidHandle; + } + + /// Returns the size of a framebuffer object. + syscall get_framebuffer_size { + in fb: Framebuffer; + + /// The size of the framebuffer in pixels. + out size: Size; + + /// `fb` is not a valid framebuffer resource. + error InvalidHandle; + } + + /// Returns the video memory for a memory framebuffer. + /// + /// NOTE: The returned memory is stable and valid until the `fb` is destroyed. + /// + /// NOTE: Any framebuffer except memory framebuffers cannot have + /// memory mappings. + syscall get_framebuffer_memory { + in fb: Framebuffer; + + /// The descriptor of the pixel memory that forms the contents of `fb`. + out memory: video.VideoMemory; + + /// `fb` is not a valid framebuffer resource. + error InvalidHandle; + + /// `fb` is not a framebuffer created with `create_memory_framebuffer`. + error Unsupported; + } + + /// Marks a portion of the framebuffer as changed and forces the OS to + /// perform an update action if necessary. + syscall invalidate_framebuffer { + in fb: Framebuffer; + + /// The area of the framebuffer that has changed. + /// + /// NOTE: `area` is limited to the actual bounds of the framebuffer. + /// + /// NOTE: If `area.width` or `area.height` are zero, nothing will be invalidated. + in area: Rectangle; + + /// `fb` is not a valid framebuffer resource. + error InvalidHandle; + } + + /// Renders the provided Ashet Graphics Protocol `sequence` into `target` framebuffer. + /// + /// The operation will complete when rendering is done. + /// + /// NOTE: On machines without hardware acceleration, this operation might be + /// completed synchronously. + async_call Render { + /// The framebuffer which should be drawn to. + in target: Framebuffer; + + /// The AGP code that defines the drawing. + /// + /// NOTE: The kernel will validate the code inside `overlapped.schedule` and + /// immediately complete the operation with `BadCode` if `sequence` + /// is not a valid AGP command sequence. + /// + /// NOTE: The kernel will create an ephemeral copy of the code inside `overlapped.schedule` + /// if the operation will not be completed immediately. + in sequence: bytestr; + + /// If the target framebuffer is invalidatable, it is automatically invalidated after the completion + /// of the command sequence, ensuring presentation of the contents. + /// + /// This is useful when painting into widgets or windows to ensure the window manager + /// actually sees the changes as soon as they are done, reducing graphics pipeline latency. + in auto_invalidate: bool; + + /// `sequence` is not a valid AGP command sequence. + error BadCode; + + /// `target` is not a valid framebuffer resource. + error InvalidHandle; + } +} + +//? TODO: Review this namespace. +namespace gui { + resource Window { } + + resource Widget { } + + resource Desktop { } + + resource WidgetType { } + + enum NotificationSeverity : u8 { + /// Important information that require immediate action + /// by the user. + /// + /// This should be handled with care and only for reall + /// urgent situations like low battery power or + /// unsufficient disk memory. + item attention = 0; + + /// This is a regular user notification, which should be used + /// sparingly. + /// + /// Typical notifications of this kind are in the category of + /// "download completed", "video fully rendered" or similar. + item information = 128; + + /// Silent notifications that might be informational, but do not + /// require attention by the user at all. + item whisper = 255; + + ... + } + + enum MessageBoxIcon : u8 { + item information = 0; + item question = 1; + item warning = 2; + item @"error" = 3; + } + + bitstruct WindowFlags : u32 { + field popup: bool; + field resizable: bool; + reserve u30 = 0; + } + + bitstruct CreateWindowFlags : u32 { + field popup: bool = false; + reserve u31 = 0; + } + + typedef WidgetEventHandler = fnptr (WidgetType, Widget, *const WidgetEvent) void; + + struct WidgetDescriptor { + field uuid: UUID; + + /// Number of bytes allocated in a Widget for this widget type. + /// See @ref gui.get_widget_data function for further information. + field data_size: usize; + + field flags: Flags; + + //? TODO: Fill this out + + //? Event Handlers: + + field handle_event: WidgetEventHandler; + + bitstruct Flags : u32 { + /// If `true`, the user can focus this widget with the mouse or keyboard. + field focusable: bool; + + /// If `true`, the user is able to open a context menu on this. + field context_menu: bool; + + /// If `true`, this widget is able to receive events with the mouse. + /// If `false`, the widget is ignored in the position-to-widget resolution. + field hit_test_visible: bool; + + /// If `true`, the user is able to potentially drop data via Drag&Drop + /// on this widget. + field allow_drop: bool; + + /// If `true`, the user can copy/cut/paste data from/into this widget. + field clipboard_sensitive: bool; + + reserve u27 = 0; + } + } + + struct WidgetControlMessage { + field event_type: WidgetEvent.Type; + + /// The widget-specific type of the control message. + /// Could be something like `get_property`, `set_property`, `set_text`, ... + field type: gui.WidgetControlID; + + /// Generic parameters that can be passed to the widget. + field params: [4]usize; + } + + struct WidgetNotifyEvent { + field event_type: WindowEvent.Type; + + field widget: Widget; + + /// The widget-specific type of event. + /// Could be something like `text_changed`, `clicked`, `checked_changed`, ... + field type: gui.WidgetNotifyID; + + /// Generic data associated with the event. + field data: [4]usize; + } + + + enum MessageBoxResult : u8 { + item ok = 0; + item cancel = 1; + item yes = 2; + item no = 3; + item abort = 4; + item retry = 5; + item continue = 6; + item ignore = 7; + } + + bitstruct MessageBoxButtons : u8 { + const ok: MessageBoxButtons = .{ .has_ok = true }; + const ok_cancel: MessageBoxButtons = .{ .has_ok = true, .has_cancel = true }; + const yes_no: MessageBoxButtons = .{ .has_yes = true, .has_no = true }; + const yes_no_cancel: MessageBoxButtons = .{ .has_yes = true, .has_no = true, .has_cancel = true }; + const retry_cancel: MessageBoxButtons = .{ .has_retry = true, .has_cancel = true }; + const abort_retry_ignore: MessageBoxButtons = .{ .has_abort = true, .has_retry = true, .has_ignore = true }; + + field has_ok: bool = false; + field has_cancel: bool = false; + field has_yes: bool = false; + field has_no: bool = false; + field has_abort: bool = false; + field has_retry: bool = false; + field has_continue: bool = false; + field has_ignore: bool = false; + } + + typedef DesktopEventHandler = fnptr (Desktop, *const DesktopEvent) void; + + struct DesktopDescriptor { + /// Number of bytes allocated in a Window for this desktop. + /// See @ref gui.get_desktop_data function for further information. + field window_data_size: usize; + + /// A function pointer to the event handler of a desktop. + /// The desktop will receive events via this function. + field handle_event: DesktopEventHandler; + } + + union DesktopEvent { + field event_type: Type; + + field create_window: DesktopWindowEvent; + field destroy_window: DesktopWindowEvent; + field invalidate_window: DesktopWindowInvalidateEvent; + + field show_notification: DesktopNotificationEvent; + field show_message_box: MessageBoxEvent; + + enum Type : u16 { + //? lifecycle management: + + /// A window was created on this desktop. + item create_window = 0; + + /// A window was destroyed on this desktop. + item destroy_window = 1; + + /// A window has been invalidated and must be drawn again. + item invalidate_window = 2; + + //? user interaction: + + /// `send_notification` was called and the desktop user should display + /// a notification. + item show_notification = 3; + + /// `send_notification` was called and the desktop user should display + /// a notification. + item show_message_box = 4; + + ... + } + } + + struct DesktopWindowEvent { + field event_type: DesktopEvent.Type; + field window: Window; + } + + struct DesktopWindowInvalidateEvent { + field event_type: DesktopEvent.Type; + field window: Window; + field area: Rectangle; + } + + struct DesktopNotificationEvent { + field event_type: DesktopEvent.Type; + + /// The text of the notification. + field message: str; + + /// The severity/importance of the notification. + field severity: NotificationSeverity; + } + + struct MessageBoxEvent { + field event_type: DesktopEvent.Type; + + /// The desktop-specific request id that must be passed into + /// `notify_message_box` to finish the message box request. + field request_id: RequestID; + + /// Content of the message box. + field message: str; + + /// Caption of the message box. + field caption: str; + + /// Which buttons are presented to the user? + field buttons: MessageBoxButtons; + + /// Which icon is shown? + field icon: MessageBoxIcon; + + enum RequestID : u16 { ... } + } + + union WidgetEvent { + field event_type: Type; + + field mouse: MouseEvent; + field keyboard: KeyboardEvent; + field control: WidgetControlMessage; + + //? TODO: Add event data + + enum Type : u16 { + //? lifecycle: + + /// The widget was created and attached to a window. + item create = 0; + + /// The widget is in the process of being destroyed. + /// After this event, the handle will be invalid. + item destroy = 1; + + /// The creator of the widget wants to do something widget-specific. + item control = 2; + + //? basic input: + + /// The user clicked on the widget with the primary mouse button + /// or pressed the return or space bar button on the keyboard. + /// + /// NOTE: A click with the mouse is valid when, and only when: + /// `mouse_button_down` and `mouse_button_up` with the left mouse button happen on the + /// same widget. The hovered widget *may* change in between the mouse down and mouse up, + /// but the click will still be recognized. + /// NOTE: A click with the keyboard is valid when, and only when: + /// `key_press` and `key_release` happen without changing the focused widget, and only when + /// the focus giving key (space, return, ...) was pressed without any other key interrupting. + item click = 3; + + //? keyboard input: + + /// A key was pressed on the keyboard. + item key_press = 4; + + /// A key was released on the keyboard. + item key_release = 5; + + //? mouse specific extras: + + /// The mouse was moved inside the rectangle of the widget. + /// + /// NOTE: This event can only happen when `hit_test_visible` was set + /// in the widget creation flags. + item mouse_enter = 6; + + /// The mouse was moved outside the rectangle of the widget. + /// + /// NOTE: This event can only happen when `hit_test_visible` was set + /// in the widget creation flags. + item mouse_leave = 7; + + /// The mouse stopped for some time over the widget. + /// + /// NOTE: This event can only happen when `hit_test_visible` was set + /// in the widget creation flags. + item mouse_hover = 8; + + /// A mouse button was pressed over the widget. + /// + /// NOTE: This event can only happen when `hit_test_visible` was set + /// in the widget creation flags. + item mouse_button_press = 9; + + /// A mouse button was released over the widget. + /// + /// NOTE: This event can only happen when `hit_test_visible` was set + /// in the widget creation flags. + item mouse_button_release = 10; + + /// The mouse was moved over the widget. + /// + /// NOTE: This event can only happen when `hit_test_visible` was set + /// in the widget creation flags. + item mouse_motion = 11; + + /// A vertical or horizontal scroll wheel was scrolled over the widget. + /// + /// NOTE: This event can only happen when `hit_test_visible` was set + /// in the widget creation flags. + item scroll = 12; + + //? drag&drop operations: + + /// The user dragged a payload into the rectangle of this widget. + /// + /// NOTE: This event can only happen when `allow_drop` was set in the + /// widget creation flags. + item drag_enter = 13; + + /// The user dragged a payload out of the rectangle of this widget. + /// + /// NOTE: This event can only happen when `allow_drop` was set in the + /// widget type creation flags. + item drag_leave = 14; + + /// The user dragged a payload over the rectangle of this widget. + /// + /// NOTE: This event can only happen when `allow_drop` was set in the + /// widget type creation flags. + item drag_over = 15; + + /// The user dropped a payload into this widget. + /// + /// NOTE: This event can only happen when `allow_drop` was set in the + /// widget type creation flags. + item drag_drop = 16; + + //? clipboard operations: + + /// The user requested a clipboard copy operation, usually by pressing 'Ctrl-C'. + /// + /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// the widget type creation flags. + item clipboard_copy = 17; + + /// The user requested a clipboard paste operation, usually by pressing 'Ctrl-V'. + /// + /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// the widget type creation flags. + item clipboard_paste = 18; + + /// The user requested a clipboard cut operation, usually by pressing 'Ctrl-X'. + /// + /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// the widget type creation flags. + item clipboard_cut = 19; + + //? widget specific: + + //? TODO: Implement ResizedArgs with "desired size, actual size" + /// The widget was resized with a call to `place_widget`. + /// + /// NOTE: This event will not fire if the widget was only moved. + item resized = 21; + + /// The widget should draw itself. + item paint = 20; + + /// User pressed the "context menu" button or did a + /// secondary mouse button click on the widget. + item context_menu_request = 22; + + /// The widget received focus via mouse or keyboard. + item focus_enter = 23; + + /// The widget lost focus after receiving it. + item focus_leave = 24; + + ... + } + } + + union WindowEvent { + field event_type: Type; + + field mouse: MouseEvent; + field keyboard: KeyboardEvent; + field widget_notify: WidgetNotifyEvent; + + enum Type : u16 { + item widget_notify = 0; + + item key_press = 1; + item key_release = 2; + + item mouse_enter = 3; + item mouse_leave = 4; + item mouse_motion = 7; + item mouse_button_press = 6; + item mouse_button_release = 5; + + /// The user requested the window to be closed. + item window_close = 8; + + /// The window was minimized and is not visible anymore. + item window_minimize = 9; + + /// The window was restored from minimized state. + item window_restore = 10; + + /// The window is currently moving on the screen. Query `window.bounds` to get the new position. + item window_moving = 11; + + /// The window was moved on the screen. Query `window.bounds` to get the new position. + item window_moved = 12; + + /// The window size is currently changing. Query `window.bounds` to get the new size. + item window_resizing = 13; + + /// The window size changed. Query `window.bounds` to get the new size. + item window_resized = 14; + } + } + + syscall register_widget_type { + in descriptor: *const WidgetDescriptor; + out handle: WidgetType; + error AlreadyRegistered; + error SystemResources; + } + + + + + /// Opens a message box popup window and prompts the user for response. + async_call ShowMessageBox { + in desktop: Desktop; + in message: str; + in caption: str; + in buttons: MessageBoxButtons; + in icon: MessageBoxIcon; + out result: MessageBoxResult; + } + + /// Spawns a new window. + syscall create_window { + in desktop: Desktop; + in title: str; + in min: Size; + in max: Size; + in startup: Size; + in flags: CreateWindowFlags; + out handle: Window; + error InvalidDimensions; + error InvalidHandle; + error SystemResources; + } + + syscall get_window_title { + in window: Window; + in title_buf: ?[]u8; + out title_len: usize; + error InvalidHandle; + } + + syscall get_window_size { + in window: Window; + out size: Size; + error InvalidHandle; + } + + syscall get_window_min_size { + in window: Window; + out min_size: Size; + error InvalidHandle; + } + + syscall get_window_max_size { + in window: Window; + out max_size: Size; + error InvalidHandle; + } + + syscall get_window_flags { + in window: Window; + out flags: WindowFlags; + error InvalidHandle; + } + + /// Sets the `size` of `window` and returns the new actual size. + /// NOTE: This event is meant to be used from desktop APIs and will not automatically + /// notify the window of the resize event. + syscall set_window_size { + in window: Window; + in size: Size; + out actual_size: Size; + error InvalidHandle; + } + + /// Resizes a window to the new size. + syscall resize_window { + in window: Window; + in size: Size; + error InvalidHandle; + } + + /// Changes a window title. + syscall set_window_title { + in handle: Window; + in title: str; + error InvalidHandle; + } + + /// Notifies the desktop that a window wants attention from the user. + /// This could just pop the window to the front, make it blink, show a small notification, ... + syscall mark_window_urgent { + in handle: Window; + error InvalidHandle; + } + + /// Waits for an event on the given `Window`, completing as soon as + /// an event arrived. + async_call GetWindowEvent { + in window: Window; + out event: WindowEvent; + error Cancelled; + error InProgress; + error InvalidHandle; + } + + /// Create a new widget identified by `uuid` on the given `window`. + /// Position and size of the widget are undetermined at start and a call to `place_widget` should be performed on success. + syscall create_widget { + in window: Window; + in uuid: *const UUID; + out widget: Widget; + error SystemResources; + error WidgetNotFound; + error InvalidHandle; + } + + /// Moves and resizes a widget in one. + /// + /// NOTE: The position of a widget is unrestricted, but it's size + /// may be restricted by the selected widget type. + syscall place_widget { + in widget: Widget; + + /// The desired position and size of the widget. + in desired: Rectangle; + + /// The actual position and size of the widget after the operation. + out actual: Rectangle; + + error InvalidHandle; + } + + enum WidgetControlID : u32 { ... } + + /// Triggers the `control` event of the widget with the given `message` as a payload. + syscall control_widget { + in widget: Widget; + in message: WidgetControlMessage; + error SystemResources; + error InvalidHandle; + } + + enum WidgetNotifyID : u32 { ... } + + /// Puts a `widget_notify` event into the event queue of the `Window` that owns `widget`. + /// The parameters are passed as a `WidgetNotifyEvent` to the event queue. + syscall notify_owner { + in widget: Widget; + in type: WidgetNotifyID; + in params: *const [4]usize; + error SystemResources; + error InvalidHandle; + } + + /// Returns WidgetType-associated "opaque" data for this widget. + /// + /// This is meant as a convenience tool to store additional information per widget + /// like internal state and such. + /// + /// The size of this must be known and cannot be queried. + syscall get_widget_data { + in widget: Widget; + out data: [*]align(16) u8; + error InvalidHandle; + } + + /// Returns the current location and size of the provided widget. + syscall get_widget_bounds { + in widget: Widget; + out bounds: Rectangle; + error InvalidHandle; + } + + /// Creates a new desktop with the given name. + syscall create_desktop { + /// User-visible name of the desktop. + in name: str; + in descriptor: *const DesktopDescriptor; + out desktop: Desktop; + error SystemResources; + } + + /// Returns the name of the provided desktop. + syscall get_desktop_name { + in desktop: Desktop; + in name_buf: ?[]u8; + out name_len: usize; + error InvalidHandle; + } + + /// Enumerates all available desktops. + syscall enumerate_desktops { + in serverlist: ?[]Desktop; + out count: usize; + } + + /// Returns all windows for a desktop handle. + syscall enumerate_desktop_windows { + in desktop: Desktop; + in window: ?[]Window; + out count: usize; + error InvalidHandle; + } + + /// Returns desktop-associated "opaque" data for this window. + /// + /// This is meant as a convenience tool to store additional information per window + /// like position on the screen, orientation, alignment, ... + /// + /// The size of this must be known and cannot be queried. + syscall get_desktop_data { + in window: Window; + out data: [*]align(16) u8; + error InvalidHandle; + } + + /// Notifies the system that a message box was confirmed by the user. + /// + /// NOTE: This function is meant to be implemented by a desktop server. + /// Regular GUI applications should not use this function as they have no + /// access to a `MessageBoxEvent.RequestID`. + syscall notify_message_box { + /// The desktop that completed the message box. + in source: Desktop; + /// The request id that was passed in `MessageBoxEvent`. + in request_id: MessageBoxEvent.RequestID; + /// The resulting button which the user clicked. + in result: MessageBoxResult; + error BadRequestId; + error InvalidHandle; + } + + /// Posts an event into the window event queue so the window owner + /// can handle the event. + syscall post_window_event { + in window: Window; + in event: WindowEvent; + error SystemResources; + error InvalidHandle; + } + + /// Sends a notification to the provided `desktop`. + syscall send_notification { + /// Where to show the notification? + in desktop: Desktop; + /// What text is displayed in the notification? + in message: str; + /// How urgent is the notification to the user? + in severity: NotificationSeverity; + error SystemResources; + error InvalidHandle; + } + + namespace clipboard { + /// Sets the contents of the clip board. + /// Takes a mime type as well as the value in the provided format. + syscall set { + in desktop: Desktop; + in mime: str; + in value: str; + error SystemResources; + } + + /// Returns the current type present in the clipboard, if any. + syscall get_type { + in desktop: Desktop; + in type_buf: ?[]u8; + out type_len: usize; + error InvalidHandle; + } + + /// Returns the current clipboard value as the provided mime type. + /// The os provides a conversion *if possible*, otherwise returns an error. + /// The returned memory for `value` is owned by the process and must be freed with `ashet.process.memory.release`. + syscall get_value { + in desktop: Desktop; + in mime: str; + out value: []const u8; + error InvalidHandle; + error SystemResources; + error ConversionFailed; + error ClipboardEmpty; + } + } +} + +/// The service namespace implements a kernel-mediated Object Request Broker (ORB). +/// +/// It allows processes to register "Interfaces" consisting of functions that can be +/// called by other processes. +/// +/// KEY FEATURES: +/// - **Hybrid Invocation**: Interfaces can support synchronous (blocking) and/or +/// asynchronous (overlapped) invocation models. +/// - **Type Safety**: The kernel validates that the caller passes the correct number and types +/// of arguments (integers vs resources). +/// - **Resource Marshalling**: Resources passed to/from interfaces are automatically +/// bound to the receiver's process. +/// - **Context Switching**: The kernel switches the "Resource Context" of the executing thread +/// to the Interface's owning process during the execution of the handler. +namespace service { + /// An interface is a collection of synchronous functions and overlapped operations. + /// + /// Calling the functions will directly invoke an associated function in the creating + /// process. + /// + /// Calling an overlapped operation will trigger the creating process which eventually + /// completes the operation. + /// + /// Each interface is uniquely identified by a UUID which allows identification of the + /// interface and asserts the contract for the semantics of the functions and overlapped ops. + /// + /// In addition to the UUID, each interface has a signature that asserts the compatibility + /// between the producer and the consumer of the interface. + /// + /// Interfaces can have up to 256 functions and overlapped operations each, with each + /// function having up to 8 input values and a single return value, and overlapped operations + /// having up to 8 input and 8 output values. + /// + /// NOTE: As interface resources are hold both by the service and the consumer, an interface is + /// always tied to the lifetime of to the creating process. This ensures that when the process + /// is terminated, the interface resource will be destroyed. + /// + /// This is not done through the tethering interface, but through a dedicated mechanism that + /// ensures correctness. + resource Interface { } + + /// Defines the type of argument and the potential transformations the + /// kernel performs when passing the argument between caller and callee. + enum MarshalType : u2 { + /// The argument/result is unused. + /// Unused values must be set to zero or otherwise the call/return is invalid. + item unused = 0; + + /// This value is reserved and should not be used. + item reserved = 1; + + /// The value is passed unmodified by the kernel. + /// NOTE: This value should be used for integers, enumerations, + /// raw pointers and so on. + item raw = 2; + + /// The value passed is a system resource. + /// + /// MARSHALLING rules: + /// - A zero value is never marshalled and passed verbatim. This allows passing optional resources. + /// + /// - For `invoke` / `Invoke` inputs: + /// The kernel interprets resource handles in the *calling thread's current resource context*, + /// validates them, then creates an `at_least_weak` binding for the process that owns `interface`. + /// The `Function` / `AsyncHandler` receives resource handles valid in its own resource context. + /// + /// - For `invoke` output: + /// If the signature declares a resource output, the kernel interprets the returned handle in the + /// resource context active during the `Function` call (the interface handler context). + /// If it is non-zero and valid, the kernel creates a strong binding for the `invoke` caller's + /// resource context and returns the translated handle. + /// If it is non-zero and invalid, `invoke` returns `error.BadReturnValue`. + /// + /// - For `complete_request` outputs: + /// The kernel interprets resource handles in `results` in the *calling thread's current resource context*. + /// For each non-zero valid handle, it creates a strong binding for the resource context that scheduled + /// the original `Invoke` request and returns the translated handle in `Invoke.results`. + item resource = 3; + } + + /// Defines the signature of a function or overlapped operation. + /// + /// NOTE: For function signatures, `outputs[1..]` must be set to + /// `MarshalType.unused`. + /// + /// NOTE: For both `inputs` and `outputs`: As soon as an index in the + /// array is `MarshalType.unused`, all following items must also be + /// `MarshalType.unused`. + bitstruct FunctionSignature : u32 { + /// Defines the number and type of the input arguments. + /// `inputs[0]` is the first argument, `inputs[7]` is the eighth argument. + field inputs: [8]MarshalType; + + /// Defines the number and type of the result values. + /// `outputs[0]` is the first result, `outputs[7]` is the eighth result. + /// + /// NOTE: Only `outputs[0]` may be set for functions. Overlapped operations + /// can use all eight values. + field outputs: [8]MarshalType; + } + + /// A token used by the interface to identify a pending asynchronous request. + /// + /// NOTE: Request tokens are valid globally and may be passed between processes. + enum RequestToken : u32 { ... } + + /// The signature of the asynchronous request handler function registered by an interface. + /// + /// **Parameters:** + /// 1. `context`: The opaque pointer associated with the interface. + /// 2. `request`: The token to the incoming request. + /// 3. `operation`: The index of the operation being called. + /// 4. `arguments`: Pointer to the 8 input arguments provided by the caller. + /// + /// **Behavior:** + /// The handler should store the `req` and the values inside `args` (if needed) and return immediately. + /// The operation is completed later via `complete_request`. + /// + /// NOTE: A handler function should not yield the executing thread, as this will generate + /// hard to debug scenarios. + /// + /// NOTE: `arguments` must be assumed invalid after the return of the callback. + /// + /// NOTE: The handler should perform a strong binding of resources inside `arguments` if it must + /// retain independent access even if the caller later unbinds/releases its handles. + /// This still does not prevent explicit destruction of the resource. + typedef AsyncHandler = fnptr(context: ?*anyopaque, request: RequestToken, operation: u8, arguments: *const [8]usize) void; + + /// The signature of the asynchronous cancellation handler function registered by an interface. + /// + /// **Parameters:** + /// 1. `context`: The opaque pointer associated with the interface. + /// 2. `request`: The token to the incoming request. + /// + /// **Behavior:** + /// This handler is invoked inside `overlapped.cancel` when the operation wasn't completed yet. + /// The implementor shall perform potential cancellation of the request and must not call `complete_request` + /// or `fail_request` anymore, as `request` will be invalidated after the cancel handler returns. + /// + /// NOTE: A handler function should not yield the executing thread, as this will generate + /// hard to debug scenarios. + typedef CancelHandler = fnptr(context: ?*anyopaque, request: RequestToken) void; + + /// The signature of a synchronous function call registered by an interface. + /// + /// **Parameters:** + /// 1. `context`: The opaque pointer associated with the interface. + /// 2. `arguments`: Pointer to the 8 input arguments provided by the caller. + /// + /// The function may or may not return a value depending on it's signature. + /// + /// If the signature does not define a return value, the function must return zero. + /// + /// NOTE: A synchronous function should not yield the executing thread, as this will generate + /// hard to debug scenarios. + /// + /// NOTE: `arguments` must be assumed invalid after the return of the callback. + typedef Function = fnptr(context: ?*anyopaque, arguments: *const [8]usize) usize; + + /// Creates a new interface that can be invoked by other processes. + /// + /// NOTE: After creation, the interface is not yet discoverable with `enumerate`. + /// This way, private interfaces can be passed between processes. + /// + /// NOTE: If an interface should be available as a system service, it must be + /// published with `register`. + syscall create { + /// The unique identifier of the interface. + /// + /// This UUID defines the contract this interface implements. + /// + /// NOTE: The kernel copies the UUID object internally. + in uuid: *const UUID; + + /// A human-readable name for enumeration and debugging. + in name: str; + + /// Defines the signatures and count of synchronous function calls in the interface. + /// + /// NOTE: The kernel will create an internal copy of this array, so userland + /// can reuse the memory freely after this call. + in sync_signatures: []const FunctionSignature; + + /// Defines the signatures and count of overlapped operations in the interface. + /// + /// NOTE: The kernel will create an internal copy of this array, so userland + /// can reuse the memory freely after this call. + in async_signatures: []const FunctionSignature; + + /// The opaque context pointer ("this") passed to the functions in `vtable` and the `async_handler`. + /// + /// NOTE: This can be used to implement a stateful interface that allows a process to create + /// the same interface more than once and still have context which of the interfaces were + /// called. + in context: ?*anyopaque; + + /// The synchronous implementation functions (vtable). + /// + /// NOTE: Must have the same number of elements as `sync_signatures`. + /// + /// NOTE: The kernel will create an internal copy of this array, so userland + /// can reuse the memory freely after this call. + in vtable: []const Function; + + /// The asynchronous request handler. + /// + /// NOTE: All asynchronous requests go through the same function handler, and + /// dispatch must happen in userland. + /// + /// The kernel ensures that argument marshalling will be properly performed, + /// and function will never be out of range for the interface. + /// + /// NOTE: May be `null` if, and only if, `async_signatures.len == 0`. + in async_handler: ?AsyncHandler; + + /// The asynchronous cancellation handler. + /// + /// NOTE: All cancellation requests go through the same function handler, and + /// dispatch must happen in userland. + /// + /// NOTE: May be `null` even if `async_signatures.len > 0`. + /// + /// NOTE: If `null`, the kernel just invalidates the `RequestToken` on cancellation + /// and will not allow completion of the request. + /// + /// The userland process may still perform unnecessary work. + in cancel_handler: ?CancelHandler; + + /// The created interface resource. + out interface: Interface; + + /// A signature inside `sync_signatures` or `async_signatures` is invalid. + /// + /// This means either: + /// - A function has more than a single output + /// - A signature has `MarshalType.unused` between used inputs or outputs. + /// - `MarshalType.reserved` is used. + error InvalidSignature; + + /// Returned if a parameter is malformed. + /// + /// Reasons for this may be: + /// - `sync_signatures.len != vtable.len`. + /// - `uuid` is nil (all bits zero) or omni (all bits one). + /// - `async_handler` is null, but `async_signatures.len > 0`. + error InvalidValue; + + error SystemResources; + } + + /// Returns the UUID of the interface. + syscall get_interface_uuid { + in interface: Interface; + out uuid: UUID; + + /// `interface` is not a valid interface resource. + error InvalidHandle; + } + + /// Returns the name of the interface. + syscall get_interface_name { + in interface: Interface; + in name_buf: ?[]u8; + + /// If `name_buf` is null, the total length of the name. + /// If `name_buf` is not null, the number of bytes written to `name_buf`. + out name_len: usize; + + /// `interface` is not a valid interface resource. + error InvalidHandle; + } + + /// Registers an interface as a systemwide service. + /// + /// All registered interfaces can be discovered through `enumerate`. + /// + /// NOTE: Revoking the registration is not possible by design. To unpublish + /// a service, the interface resource must be destroyed. + /// + /// NOTE: Registering an `interface` twice is idempotent and does nothing. + syscall register { + /// The interface that shall be published. + in interface: Interface; + + /// `interface` is not a valid interface resource. + error InvalidHandle; + + error SystemResources; + } + + /// Enumerates all registered services. + syscall enumerate { + /// If not `null`, the enumeration returns only interfaces + /// with the given unique identifier. + /// If `null` will enumerate all interfaces. + in uuid: ?*const UUID; + + /// If not `null`, the kernel will write the registered interfaces to this array. + /// + /// NOTE: The interface handles will be bound to the calling process with `BindOperation.at_least_weak` + /// to ensure resource access. + in services: ?[]Interface; + + /// Number of elements written to `services` or total number of registered + /// interfaces. + out count: usize; + + error SystemResources; + } + + /// Invokes an interface function synchronously. + /// + /// **Execution Flow:** + /// 1. Kernel validates arguments against signature. + /// 2. Kernel marshals input resources. + /// 3. Kernel context-switches to interface process. + /// 4. Kernel calls `vtable[func_index]`. + /// 5. Kernel context-switches back. + /// 6. Kernel optionally marshals the output resource. + syscall invoke { + in interface: Interface; + + /// Index of the function which shall be invoked. + in function: u8; + + /// The arguments passed to `function`. + /// + /// NOTE: The kernel will perform marshalling as defined in the function signature. + /// + /// NOTE: The kernel will validate that all unused arguments are zero. + /// + /// NOTE: Resource handles passed here are assumed to be valid in the callers resource context. + in arguments: [8]usize; + + /// The return value of the function. + /// + /// NOTE: If a resource handle is returned, the resource will be strongly bound to the callers process. + /// + /// NOTE: If a resource is expected to be returned, but zero is returned, the kernel will pass the zero. + /// This allows returning optional resources. Userland has to validate that rules for non-zero only returns. + out result: usize; + + /// `interface` is not a valid interface resource. + error InvalidHandle; + + /// `function` does not exist. + error InvalidFunction; + + /// The kernel validation of the arguments failed. + /// + /// This can have two reasons: + /// - An unused argument is non-zero. + /// - A resource argument is not a valid resource handle. + error InvalidArg; + + /// Returned in the following cases: + /// - The invoked function returns a resource handle, but this + /// resource handle was neither valid nor zero. + /// - The invoked function return value is unused, but the + /// function returned a non-zero value. + /// + /// LORE: This error is sadly the best way to handle implementation bugs + /// in the interface. As the implementor process has already surrendered + /// control back to the kernel, there's no channel back to the implementor + /// to inform it about misbehaviour. + error BadReturnValue; + + error SystemResources; + } + + /// Schedules an overlapped interface operation. + /// + /// **Execution Flow:** + /// 1. Kernel validates arguments against signature. + /// 2. Kernel allocates an internal Request State. + /// 3. Kernel marshals input resources. + /// 4. Kernel context-switches to Interface process (temporarily). + /// 5. Kernel calls `async_handler`. + /// 6. Kernel context-switches back and returns the ARC to the caller. + async_call Invoke { + in interface: Interface; + + /// Index of the asynchronous operation that shall be invoked. + in operation: u8; + + /// The arguments passed to `operation`. + /// + /// NOTE: The kernel will perform marshalling as defined in the operation signature. + /// + /// NOTE: The kernel will validate that all unused arguments are zero. + /// + /// NOTE: Resource handles passed here are assumed to be valid in the callers resource context. + in arguments: [8]usize; + + /// The results of the operation. + /// + /// NOTE: Filled by the kernel when the interface implementor calls `complete_request`. + /// + /// NOTE: Resource handles returned here are bound strongly to the schedulers resource context. + out results: [8]usize; + + /// `interface` is not a valid interface resource. + error InvalidHandle; + + /// `operation` does not exist. + error InvalidFunction; + + /// The kernel validation of the arguments failed. + /// + /// This can have two reasons: + /// - An unused argument is non-zero. + /// - A resource argument is not a valid resource handle. + error InvalidArg; + + /// The operation was failed by a call to `fail_request`. + error RequestFailed; + + error SystemResources; + } + + /// Completes a pending asynchronous request (called by the interface implementor). + /// + /// NOTE: This consumes the `request` token and wakes the caller (completing their ARC). + syscall complete_request { + /// The request token passed to `AsyncHandler`. + in request: RequestToken; + + /// The return values/resources. + /// + /// NOTE: Resources in this array will be marshalled to the caller as specified + /// in the operation signature. + /// + /// NOTE: Resource handles passed here are assumed to be valid in the calling thread's current resource context. + in results: [8]usize; + + /// `request` is not a valid pending request. + error InvalidHandle; + + /// The kernel validation of the results failed. + /// + /// This can have two reasons: + /// - An unused result is non-zero. + /// - A resource result is not a valid resource handle. + error InvalidArg; + + error SystemResources; + } + + /// Rejects/fails a pending asynchronous request (called by the interface implementor). + /// + /// This completes the caller's ARC with a generic `RequestFailed` error. + /// + /// NOTE: This consumes the `request` token and wakes the caller (completing their ARC). + syscall fail_request { + /// The request token passed to `AsyncHandler`. + in request: RequestToken; + + /// `request` is not a valid pending request. + error InvalidHandle; + } +} + +//? TODO: Review this namespace. +/// +/// The I/O namespace contains APIs to interface with external hardware like serial ports, I²C busses and so on. +/// +namespace io { + + /// + /// Functions and types related to serial busses like RS232, RS485 or similar. + /// + namespace serial { + enum SerialPortID : u32 { + ... + } + + syscall enumerate { + in list: ?[]SerialPortID; + out count: usize; + } + + /// Queries information about the given serial port id. + syscall query_metadata { + in id: SerialPortID; + in name_buf: ?[]u8; + + out name_len: usize; + + /// The given id does not exist. + error NotFound; + } + + resource SerialPort { } + + syscall open { + in id: SerialPortID; + out port: SerialPort; + + /// The given id does not exist. + error NotFound; + + /// The resource is already opened. + error ResourceBusy; + } + + /// + /// Changes the configuration of a serial port and returns the new configuration. + /// + /// This function can also be used to query the current configuration by requesting no changes. + /// + async_call configure { + in port: SerialPort; + + in baud_rate: ?u32; + in word_size: ?u8; + in stop_bits: ?StopBits; + in parity: ?Parity; + in control_flow: ?ControlFlow; + + /// Selects which software control flow words control the transmitter + /// activity. + /// + /// NOTE: This is usually the same as `sw_control_flow_tx`. + in sw_control_flow_rx: ?SoftwareControlFlow; + + /// Selects which software control flow words are transmitted when + /// the own receive buffer is full. + /// + /// NOTE: This is usually the same as `sw_control_flow_rx`. + in sw_control_flow_tx: ?SoftwareControlFlow; + + in acceptable_baud_error: f32; + + out current_baud_rate: u32; + out current_data_bits: u8; + out current_stop_bits: StopBits; + out current_parity: Parity; + out current_control_flow: ControlFlow; + out current_sw_control_flow_rx: SoftwareControlFlow; + out current_sw_control_flow_tx: SoftwareControlFlow; + + error InvalidHandle; + + /// The actual baud rate diverges more than `acceptable_baud_error` from the requested baud rate. + error ImpreciseBaudRate; + + /// The requested word size is not supported by this serial port. + error UnsupportedDataBits; + + /// The requested number of stop bits is not supported by this serial port. + error UnsupportedStopBits; + + /// The requested parity is not supported by this serial port. + error UnsupportedParity; + + /// The requested control flow (or its configuration) is not supported by this serial port. + error UnsupportedControlFlow; + } + + /// Changes the output control lanes of the serial port. + async_call control { + in port: SerialPort; + + /// The new state that should be applied for DTR (Data Terminal Ready). + in dtr: ?bool; + + /// The new state that should be applied for RTS (Request To Send). + /// + /// NOTE: This is also called `RTR` when used for modern hardware control flow. + in rts: ?bool; + + error InvalidHandle; + + /// The serial port does not support changing the control flow mode. + error Unsupported; + + /// The control lanes cannot be changed as hardware control flow is active. + error ControlFlowActive; + } + + /// Reads all control lanes of the serial port. + async_call query_control { + in port: SerialPort; + + /// Data Terminal Ready + out dtr: bool; + + /// Data Carrier Detect + out dcd: bool; + + /// Data Set Ready + out dsr: bool; + + /// Ring Indicator + out ring: bool; + + /// Request To Send + /// + /// NOTE: This is also called `RTR` when used for modern hardware control flow. + out rts: bool; + + /// Clear To Send + out cts: bool; + + error InvalidHandle; + + /// The serial port does not support control flow lanes. + error Unsupported; + } + + /// Writes data to the serial port. + async_call Write { + in port: SerialPort; + in data: bytestr; + + /// Number of words written + out written: usize; + + error InvalidHandle; + + /// The serial port uses a word size that needs more than 8 bits per word. + error WordSizeMismatch; + } + + /// Reads data from a serial port. + async_call Read { + in port: SerialPort; + in data: bytebuf; + + /// Number of words read from the serial port. + out read: usize; + + /// This contains the reason why not *all* data was read. + out stop_reason: SerialPortError; + + error InvalidHandle; + + /// The serial port uses a word size that needs more than 8 bits per word. + error WordSizeMismatch; + } + + /// + /// Sends a break signal. + /// + /// LEARN: A break signal means that the TX line is held *low* for a given + /// duration larger than a single word. This way, the receiver can + /// recognize an event that will be transferred "out of band" and + /// allows to send an event to the receiver. + /// + /// In DMX, this is used to signal the start of a new frame. + /// + async_call Break { + in port: SerialPort; + + in duration: clock.Duration; + + error InvalidHandle; + + /// The serial port does not support sending breaks. + error Unsupported; + } + + enum SerialPortError : u8 { + item none = 0; + item break_detected = 1; + item parity_error = 2; + item framing_error = 3; + } + + enum StopBits : u8 { + item one = 1; + item one_and_half = 2; + item two = 3; + } + + enum Parity : u8 { + /// No parity will be used. + item none = 0; + + /// The parity bit will contain a `0` if the sum of all data bits are even. + item even = 1; + + /// The parity bit will contain a `0` if the sum of all data bits are odd. + item odd = 2; + + /// The parity bit will always contain a `1`. + item mark = 3; + + /// The parity bit will always contain a `0`. + item space = 4; + } + + enum ControlFlow : u8 { + /// No explicit control flow is used. + item none = 0; + + /// This mode is usually called the *hardware control flow* and uses two + /// signals that are connected cross-over between both communication partners. + /// + /// - `RTR` is an active-low signal called *Ready To Receive* which signals the + /// opposite part that we can actually receive data right now. If the signal + /// is high, the opposite part may not send data. + /// - `CTS` is an active-low signal called *Clear To Send* which receives the `RTR` + /// signal from the opposite part. When this signal is low, our transmitter is + /// allowed to send data. If the signal is high, the transmitter has to stop sending + /// and a timeout may occur. + /// + /// LORE: This is usually called RTS/CTS control flow, but + /// in that configuration the RTS (request to send) signal + /// is repurposed into a RTR (ready to receive) signal which + /// tells the communication partner that you are able to receive + /// data. + /// In Ashet OS, the technically correct term is used as we don't + /// use the legacy half-duplex control flow where a request to + /// send is performed. + item rtr_cts = 1; + + /// This mode is usually called *software control flow* and uses two special bytes + /// that can inhibit or allow transmitting bytes. + /// + /// The two special word are called `XON` (Transmitter On) and `XOFF` (Transmitter Off). + /// + /// When a receiver receives the `XOFF` word, it turns off the sender after the current + /// byte has been processed. As soon as the `XON` word is received, the sender is allowed + /// to continue sending data. + /// + /// `XON` and `XOFF` can be both sent by software or hardware. + /// + /// NOTE: Using this control flow prevents sending *raw binary* data, as `XON` and `XOFF` + /// are regular data words that can be contained in the sent data. + /// + /// Thus, this control flow mode should only be used with textual data which does + /// not conflict with the chosen control characters. + /// + item xon_xoff = 2; + } + + struct SoftwareControlFlow { + /// The data word which will turn on the transmitter. + /// + /// NOTE: This is usually the ASCII `DC1` character which is encoded as the value `0x11`. + /// + /// LORE: Serial ports usually support 5 to 8 data bits, but some serial ports + /// can also use much higher word sizes. + /// + /// This is allows a future expansion to support word sizes up to 32 bits + /// without breaking existing code. + field x_on: u32 = 0x11; //? ASCII DC1 / XON + + /// The data word which will turn off the transmitter. + /// + /// NOTE: This is usually the ASCII `DC3` character which is encoded as the value `0x13`. + /// + /// LORE: Serial ports usually support 5 to 8 data bits, but some serial ports + /// can also use much higher word sizes. + /// + /// This is allows a future expansion to support word sizes up to 32 bits + /// without breaking existing code. + field x_off: u32 = 0x13; //? ASCII DC3 / XOFF + } + } + + /// + /// Functions and types related to the I²C bus. + /// + namespace i2c { + enum BusID : u32 { + ... + } + + syscall enumerate { + in list: ?[]BusID; + + out count: usize; + } + + /// Queries information about the given I²C bus id. + syscall query_metadata { + in id: BusID; + in name_buf: ?[]u8; + + out name_len: usize; + + /// The given id does not exist. + error NotFound; + } + + resource Bus { } + + syscall open { + in id: BusID; + + out bus: Bus; + + /// The given id does not exist. + error NotFound; + + error SystemResources; + } + + /// Performs a sequence of I²C operations on the bus without interruption. + /// + /// This function allows to ping devices, read or write data from/to the devices. + /// + /// The operations are performed first-to-last without interruption. If an operation fails, the + /// sequence will be aborted. + /// + /// LORE: This function was introduced instead of separate read and write functions, as it's + /// sometimes necessary for certain I²C ICs (like EEPROMS) to have "atomic" read-after-write + /// operations. + /// + /// As both a singular read and a singular write can be expressed as a batch of one operation, + /// this is the only available function on the I²C bus. + /// + /// **Example:** + /// Typical I²C EEPROMS have an internally maintained "memory cursor" which is advanced for every + /// read operation. All write operations will update the cursor also for read operations. + /// + /// This means that in an OS context, where scheduling can interrupt our process, performing a split + /// "update cursor" write operation and "read data" read operation can be interrupted by another process + /// also scheduling writes inbetween. This means that after we've set up our memory cursor to the desired + /// address another process would change that before we read and we'll read data from the wrong memory + /// location. + /// + /// To prevent such a situation, this batch interface was introduced. + /// + async_call Execute { + in bus: Bus; + + /// A mutable sequence of I²C operations. Will be processed first-to last and + /// the pointed `Operation`s will be changed during execution to report results. + in sequence: []Operation; + + /// The number of successfully processed elements from `sequence`. + /// + /// On success, the call returns exactly the length of the sequence, otherwise + /// it returns the index of the element that failed. + out count: usize; + + error InvalidHandle; + + /// An operation in the `sequence` contained an invalid address. + error InvalidAddress; + + /// An operation that isn't `ping` was trying to process zero bytes. + error EmptyOperation; + + /// An error happened during processing. Read `sequence[].error` to see which operations failed. + error ExecutionFailed; + } + + struct Operation { + /// The 7- or 10 bit device that should be addressed. + /// + /// NOTE: Values above `1023` will always make the batch execution fail. + field address: u16; + + /// The kind of operation that should be performed. + field type: Type; + + /// The data which should either be written or read. + /// + /// NOTE: If the `operation` is `BatchOp.read`, the buffer will + /// overwritten by the OS. All other operations treat this + /// buffer as immutable. + field data: bytebuf; + + /// The number of processed bytes inside `data`. + /// + /// NOTE: This field can be left uninitialized and will be overwritten by the OS + /// with the result of the operation. + /// If the batch item wasn't scheduled, the resulting value will be zero. + field processed: usize = 0; + + /// The error that happened when processing this batch item. On success, this will + /// be set to `Error.none`. + /// + /// NOTE: This field can be left uninitialized and will be overwritten by the OS + /// with the result of the operation. + /// If the batch item wasn't scheduled, the resulting value will be `Error.aborted`. + field @"error": Error; + + enum Type : u8 { + /// The device will be addressed, but no read or write operation will be performed. + /// This can be used to detect if certain devices are present. + /// + /// NOTE: Success of this operation can be detected by the resulting error code. + item ping = 0; + + /// Reads the given amount of bytes from the device. + item read = 1; + + /// Writes the given amount of bytes to the device. + item write = 2; + } + + enum Error : u8 { + /// This batch item was fully processed. + item none = 0; + + /// No device did acknowledge the address. + item device_not_found = 1; + + /// While writing the data, the device returned a NAK. + /// + /// NOTE: The ACK/NAK during the addressing phase is handled by `device_not_found`. + item no_acknowledge = 2; + + /// A previous batch item errored and this item wasn't executed at all. + item aborted = 3; + + /// A bus participant did stretch the clock for too long. + item timeout = 4; + + /// During the processing of the operation, a hardware error occurred. + item fault = 5; + + /// The operation would've operated on a reserved I2C address. + item reserved_address = 6; + } + } + } +} + +//? +//? Global Types +//? + + +struct Point { + const zero: Point = .{ .x = 0, .y = 0 }; + + field x: i16; + field y: i16; +} + +struct Size { + const empty: Size = .{ .width = 0, .height = 0 }; + const max: Size = .{ .width = 0xFFFF, .height = 0xFFFF }; + + field width: u16; + field height: u16; +} + +struct Rectangle { + field x: i16; + field y: i16; + field width: u16; + field height: u16; +} + +/// +/// An 8-bit color value with a specialized encoding suitable for embedding +/// a practical set of 256 colors. +/// +/// The color encoding is basically a HSV (hue, saturation, value) color with 8 bits, using +/// 3 bits for the hue, 3 bits for the value and 2 bits for the saturation. +/// +/// Naively mapping out the values to the HSV values has two problems though: +/// 1. A value of 0 maps all colors to black, meaning that we would have 64 different +/// types of blacks, which all would encode have the rgb value `(0, 0, 0)`. +/// 2. A saturation of 0 maps all colors to gray, effectively ignoring the hue. +/// This creates the situation that in addition to having 64 blacks, we would also +/// have each gray tone 8 times, wasting even more encoding space. +/// +/// To address these two problems, the color scheme uses a modified mapping: +/// +/// - `hue` is used without special interpretation. +/// - `value` maps to a range of `[1:8]` instead of `[0:7]`, allowing 8 different +/// values that are all not black. +/// - `saturation` is used without special interpretation except for zero: +/// If the `saturation` field is zero, `hue` and `value` are interpreted together as a 6 bit +/// integer storing the brightness of gray. +/// +/// This yields a color space which has the following properties: +/// +/// - 64 true gray levels ranging from black to white. +/// - 8 different hues (red, yellow, lime, green, cyan, blue, purple, magenta). +/// - 3 different levels of saturation for each non-gray color. +/// - black maps to `0x00` (but white does not map to `0xFF`). +/// +/// This means we have all 256 colors mapped to a distinct, meaningful color that still allows +/// programmatic conversion from and to the color without the need of a look-up table that +/// would require searching the correct color. +/// +/// NOTE: This color encoding shall be referred to as "Ashet HSV". +/// +/// LORE: This color encoding was developed over the course of several days, playing around with +/// many different encodings. +/// The color encodings/palettes were tested on a diverse set of images, including game screenshots, +/// photographs, artificial images, vector graphics and so on. +/// +/// The "Ashet HSV" encoding showed the best visual matches for most pictures, allowing both visual +/// fidelity on the color side, but also allowing both bright and dark images to work really well. +/// +bitstruct Color : u8 { + const black: Color = .{ .hue = 0, .value = 0, .saturation = 0 }; + const white: Color = .{ .hue = 7, .value = 7, .saturation = 0 }; + const red: Color = .{ .hue = 0, .value = 7, .saturation = 3 }; + const yellow: Color = .{ .hue = 1, .value = 7, .saturation = 3 }; + const lime: Color = .{ .hue = 2, .value = 7, .saturation = 3 }; + const green: Color = .{ .hue = 3, .value = 7, .saturation = 3 }; + const cyan: Color = .{ .hue = 4, .value = 7, .saturation = 3 }; + const blue: Color = .{ .hue = 5, .value = 7, .saturation = 3 }; + const purple: Color = .{ .hue = 6, .value = 7, .saturation = 3 }; + const magenta: Color = .{ .hue = 7, .value = 7, .saturation = 3 }; + + /// The hue of the color, encoded as 0 = 0° (red), 7 = 315° (magenta). + field hue: u3; + + /// The value of the color, with 0 = 12.5% brightness and 7 = 100% brightness. + field value: u3; + + /// The saturation of the color, encoded as 0 = desaturated, and 3 = fully saturated. + /// + /// NOTE: The value is encoded as the uppermost 2 bits, so a check if saturation is 0 can be + /// performed by doing a less-than operation interpreting the color as an integer. + field saturation: u2; + + struct RGB888 { + field r: u8; + field g: u8; + field b: u8; + } + + /// 32-bit ARGB format, [31:0] A:R:G:B 8:8:8:8 little endian + /// + /// Layed out as a `u32` encoding `0xAARRGGBB`. + enum ARGB8888 : u32 { ... } + + /// 32-bit ABGR format, [31:0] A:B:G:R 8:8:8:8 little endian + /// + /// Layed out as a `u32` encoding `0xAABBGGRR`. + enum ABGR8888 : u32 { ... } +} + +//? +//? TODO: Move these types into the proper namespaces or decide they +//? are actually top-level. +//? + + +struct UUID { + field bytes: [16]u8; +} From fe05822a3fbf5b8c10b506d09b3998664ea9b6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 16:21:18 +0100 Subject: [PATCH 20/36] Implements some command line usage errors for abi-parser, adds several problems identified with the tool --- src/tools/abi-mapper/rework/findings.md | 264 ++++++++++++++++++++++++ src/tools/abi-mapper/src/abi-parser.zig | 4 + 2 files changed, 268 insertions(+) create mode 100644 src/tools/abi-mapper/rework/findings.md diff --git a/src/tools/abi-mapper/rework/findings.md b/src/tools/abi-mapper/rework/findings.md new file mode 100644 index 00000000..469c86ba --- /dev/null +++ b/src/tools/abi-mapper/rework/findings.md @@ -0,0 +1,264 @@ +# ABI Mapper Findings — ashet-1.0.abi Stress Test + +--- + +## Finding 1: Underscore digit separators in integer literals + +**File:** `tests/stress/ashet-1.0.abi:1300` + +**Code:** +``` +item infinity = 0xFFFF_FFFF_FFFF_FFFF; +``` + +**Problem:** The lexer does not support `_` as a digit separator in integer literals. +`0xFFFF` is tokenized as a number, then `_FFFF_FFFF_FFFF` is seen as an identifier, +causing an unexpected token error. + +**Accepted solution:** Add `_` digit-separator support to the lexer. Underscores +are silently skipped within numeric literals (both decimal and hex), matching +Zig/Rust conventions. + +**Workaround applied:** Removed underscores → `0xFFFFFFFFFFFFFFFF`. + +--- + +## Finding 2: Reserved keyword used as enum item name + +**File:** `tests/stress/ashet-1.0.abi:8259` + +**Code:** +``` +item resource = 3; +``` + +**Problem:** `resource` is a reserved keyword in the ABI language; using it bare as +an identifier causes an unexpected token error. + +**Note:** Typo in the `.abi` file, not an abi-mapper bug. No abi-mapper change needed. + +**Workaround applied:** Quoted the identifier → `item @"resource" = 3;`. + +--- + +## Finding 3: Named parameters in `fnptr` types not supported + +**File:** `tests/stress/ashet-1.0.abi:8308`, `8323`, `8339` + +**Code:** +``` +typedef AsyncHandler = fnptr(context: ?*anyopaque, request: RequestToken, operation: u8, arguments: *const [8]usize) void; +typedef CancelHandler = fnptr(context: ?*anyopaque, request: RequestToken) void; +typedef Function = fnptr(context: ?*anyopaque, arguments: *const [8]usize) usize; +``` + +**Problem:** The parser expects `fnptr` parameter lists to contain only types; +`name: Type` syntax causes an unexpected `:` token error. + +**Accepted solution:** Extend the `fnptr` parser to accept optional `name:` prefixes +on each parameter. Names are parsed and stored in the model so that code generators +targeting languages that require parameter names (e.g. Zig, C with named args) can +replicate them faithfully. + +**Workaround applied:** Removed parameter names, leaving bare types. + +--- + +## Finding 4: Constant used as array size before symbol resolution + +**File:** `tests/stress/ashet-1.0.abi:5948`, `5955` + +**Code:** +``` +const max_fs_name_len = 8; +... +field name: [max_fs_name_len]u8; +``` + +**Problem:** sema panics with "symbol resolution not done yet" when a named constant +is used as an array size in a struct field. Symbol resolution and type mapping happen +in the same pass; the constant may not yet be resolved when its referencing field type +is processed. A full fix would require multi-pass resolution or lazy evaluation, which +can produce complex dependency chains. + +**Accepted solution:** Require constants to be lexically defined before use. If +`resolve_value` is called on a constant that has not yet been assigned a value, emit +a proper error: +``` +error: constant 'max_fs_name_len' must be defined before it is used here +``` +This is a simple rule with low implementation cost. It is acceptable author load when +writing `.abi` files: constant declarations naturally belong near the top of the +namespace they apply to, before any types that reference them. + +**Workaround applied:** Replaced constant references with literal values. + +--- + +## Finding 5: Non-standard integer widths not supported as enum/bitstruct backing types + +**File:** `tests/stress/ashet-1.0.abi:6132`, `8225` + +**Code:** +``` +enum FileType : u2 { ... } +enum MarshalType: u2 { ... } +``` + +**Problem:** `map_decl` requires the backing type (subtype) of an `enum` or `bitstruct` +to be a `model.StandardType` (`u8`, `u16`, `u32`, `u64`, `usize`, …). Non-power-of-8 +widths like `u2` map to `model.Type.uint` instead and are rejected, causing cascading +failures up through the parent namespace. + +**Secondary bug (from cascading failure):** When `map_node` fails for a top-level +node, `map()` does `continue` leaving the pre-allocated `root.items` slot as undefined +memory. The subsequent `resolve_namespace_doc_comment_refs` pass reads garbage pointers +from those slots, crashing with a General Protection Fault. Fix: collect successful +results into a fresh list rather than pre-sizing with `resize`. + +**Accepted solution:** +- Accept any `uint`/`int` type as the backing type of an `enum` or `bitstruct`. +- Add a `bit_count: u8` field to `model.Enumeration` (matching the existing field on + `model.BitStruct`), storing the original declared bit width (e.g. 2 for `u2`). +- The ABI-surface standard type is rounded up to the next power-of-two byte width + (`u2`→`u8`, `u3`/`u4`→`u8`, `u9`…`u16`→`u16`, etc.) and stored as the + `backing_type: StandardType`. +- This lets code generators use the standard type for languages that don't support + arbitrary-width integers, while preserving `bit_count` for precise packing in + bitstructs and for languages that do support sub-byte types. + +**Workaround applied:** Changed backing type to the smallest standard width → `u8`. +Finding 11 is a cascading consequence and will be automatically resolved when +Finding 5 is implemented (FileType's bit_count will be 2, the bitstruct fits in u16). + +--- + +## Finding 6: `compute_native_params` silently drops unsupported optional types + +**File:** `src/sema.zig:596`, `sema.zig:620` + +**Problem:** In `compute_native_params`, the `.optional` branch handles only +`?*T`/`?[*]T`, `?resource`, `?anyptr`, `?anyfnptr`, and the string pseudo-types. +All other optional types (`?fnptr`, `?enum`, `?struct`, `?u8`, `?u32`, `?bool`, +etc.) fall into the `else` branch which calls `std.log.err` and **does not append +the parameter**, silently producing an incorrect native call signature. + +**Accepted solution:** +- `?fnptr` is a valid C-ABI optional: a function pointer is nullable in C, so it + should be kept as-is (added directly to the native params list). +- All other unsupported optional types (`?enum`, `?struct`, `?u8`, `?u32`, `?bool`, + etc.) should emit a proper `emit_error` diagnostic instead of the silent `std.log.err`. + +--- + +## Finding 7: `unknown_named_type` in `compute_native_fields` — unreachable was semantically correct + +**File:** `src/sema.zig:864` + +**Problem:** `resolve_named_types` emits a non-fatal error when it cannot resolve a +type reference, but leaves the type slot as `.unknown_named_type`. When +`compute_native_fields` later encounters this it hits `unreachable`, panicking. + +**Accepted solution:** The `unreachable` was semantically correct: if +`unknown_named_type` is encountered here, an error diagnostic must already have been +emitted by `resolve_named_types`. Change it to a silent `continue` (skip the field) +rather than `unreachable`. Add a test that verifies the invariant — that whenever +`unknown_named_type` is reached in this code path, `ana.errors` is non-empty — so +the silent skip cannot silently hide a real bug. + +--- + +## Finding 8: Undefined types `MouseEvent` and `KeyboardEvent` in gui unions + +**File:** `tests/stress/ashet-1.0.abi:7697-7698`, `7856-7857` + +**Note:** Human error in the `.abi` file (incomplete gui namespace). Not an +abi-mapper bug. No abi-mapper change needed. + +**Workaround applied:** Changed both field types to `input.InputEvent`. + +--- + +## Finding 9: Undefined type `InputEventPayload` + +**File:** `tests/stress/ashet-1.0.abi:2241`, `2416` + +**Note:** Human error in the `.abi` file (should have been `InputEvent.Payload`). +Not an abi-mapper bug. No abi-mapper change needed. + +**Workaround applied:** Replaced with `InputEvent.Payload`. + +--- + +## Finding 10: `anyopaque` not a recognized built-in type + +**File:** `tests/stress/ashet-1.0.abi:8308`, `8323`, `8339`, `8376` + +**Note:** Human error in the `.abi` file. `anyopaque` is a Zig-specific type name; +the correct abi-mapper spelling is `anyptr`. Not an abi-mapper bug. + +**Workaround applied:** Replaced `?*anyopaque` / `?*anyopaque` with `anyptr`. + +--- + +## Finding 11: `bitstruct Flags : u16` exceeds 16 bits (cascading from Finding 5 workaround) + +**File:** `tests/stress/ashet-1.0.abi:6137` + +**Note:** Cascading consequence of the Finding 5 workaround — `FileType`'s backing +type was widened from `u2` to `u8`, adding 6 extra bits to the bitstruct. This will +be automatically resolved when Finding 5 is properly implemented: `FileType.bit_count` +will be 2, so the bitstruct field contributes 2 bits and fits within `u16` again. + +**Workaround applied:** Changed `reserve u12 = 0` to `reserve u6 = 0`. + +--- + +## Finding 12: Array fields in `bitstruct` not supported + +**File:** `tests/stress/ashet-1.0.abi:8270` + +**Code:** +``` +bitstruct FunctionSignature : u32 { + field inputs: [8]MarshalType; + field outputs: [8]MarshalType; +} +``` + +**Problem:** `get_type_bit_size` returns `null` for array types, so array fields +cannot appear inside a `bitstruct`. The intent is to pack 8 × 2-bit `MarshalType` +values into 16 bits per field (32 bits total). + +**Accepted solution:** Allow arrays of bit-packable element types inside bitstructs. +The bit contribution of `[N]T` is `N × bit_size(T)`. The model stores the array as a +bitstruct field. Code generators that cannot express sub-byte arrays must unroll the +field into N individual fields or emit appropriate macros/accessors. + +**Workaround applied:** Changed `bitstruct` to `struct` (loses the packing semantics). + +--- + +## Finding 13: Syscall with 2+ logic outputs surfaces an incorrect assertion in `validate_constraints` + +**File:** `src/sema.zig:2081`, trigger at `tests/stress/ashet-1.0.abi:2187` + +**Code:** +```zig +std.debug.assert(sc.logic_outputs.len <= sc.native_outputs.len); +``` + +**Problem:** The assertion IS correct as a design constraint: a syscall in the C ABI +can produce at most one return value, so having 2+ logic outputs is invalid. However, +`map_any_call` does not check this early — it accepts any number of `out` parameters +for syscalls. `validate_constraints` then fires the assertion as the first enforcement +point, turning a user error into a crash rather than a diagnostic. + +**Accepted solution:** Add an explicit check in `map_any_call` (or `map_syscall`) +that emits a proper `emit_error` / `fatal_error` when a syscall declaration contains +more than one `out` parameter. This surfaces the constraint early with a good error +message, making `validate_constraints` a true internal sanity check rather than the +first line of defence. + +**Workaround applied:** Merged the two outputs (`name_len`, `unique_id_len`) into a +single `struct DeviceMetadataLengths` output. diff --git a/src/tools/abi-mapper/src/abi-parser.zig b/src/tools/abi-mapper/src/abi-parser.zig index e0f0d3c8..f26f5780 100644 --- a/src/tools/abi-mapper/src/abi-parser.zig +++ b/src/tools/abi-mapper/src/abi-parser.zig @@ -21,10 +21,14 @@ pub fn main() !u8 { defer args.deinit(); if (args.positionals.len != 1) { + std.debug.print("expects exactly one positional argument, found {}\n", .{args.positionals.len}); + std.debug.print("usage: abi-mapper [--id-db ] --output \n", .{}); return 1; } if (args.options.output.len == 0) { + std.debug.print("missing argument: --output \n", .{}); + std.debug.print("usage: abi-mapper [--id-db ] --output \n", .{}); return 1; } From 161b6d4fad44288ae2587ba722205c3aa05dbfb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 16:22:14 +0100 Subject: [PATCH 21/36] For each problem in findings.md adds a patch file that documents how to get from the unpatched abi file to a working solution --- .../tests/stress/ashet-1.0.abi.patch | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch diff --git a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch new file mode 100644 index 00000000..39bdec43 --- /dev/null +++ b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch @@ -0,0 +1,182 @@ +diff --git c/src/tools/abi-mapper/tests/stress/ashet-1.0.abi w/src/tools/abi-mapper/tests/stress/ashet-1.0.abi +index 4c795bdab0..22a81adb11 100644 +--- c/src/tools/abi-mapper/tests/stress/ashet-1.0.abi ++++ w/src/tools/abi-mapper/tests/stress/ashet-1.0.abi +@@ -1297,7 +1297,7 @@ namespace clock { + /// is sufficient to fully cover the lifetime of the electronics, + /// users and probably even countries and societies. + /// Not quite infinity, but close enough for the computer it's running on. +- item infinity = 0xFFFF_FFFF_FFFF_FFFF; ++ item infinity = 0xFFFFFFFFFFFFFFFF; + + ... + } +@@ -2184,6 +2184,11 @@ namespace input { + /// + /// It may be empty if the kernel cannot provide one. + /// ++ struct DeviceMetadataLengths { ++ field name_len: usize; ++ field unique_id_len: usize; ++ } ++ + syscall query_device_metadata { + in id: DeviceId; + in name_buf: ?[]u8; +@@ -2192,8 +2197,7 @@ namespace input { + /// If not `null`, the kernel will fill this structure with metadata for the device. + in descriptor: ?*DeviceDescriptor; + +- out name_len: usize; +- out unique_id_len: usize; ++ out lengths: DeviceMetadataLengths; + + /// `id` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; +@@ -2238,7 +2242,7 @@ namespace input { + /// If the device is present in groups, the event is enqueued into those group queues. + syscall emit_device_event { + in device: DeviceId; +- in payload: InputEventPayload; ++ in payload: InputEvent.Payload; + + /// `device` is not valid anymore (e.g. device removed) or was never valid. + error InvalidDevice; +@@ -2413,7 +2417,7 @@ namespace input { + /// except it is marked synthetic. + syscall queue_event { + in group: InputGroup; +- in payload: InputEventPayload; ++ in payload: InputEvent.Payload; + in force: bool; + + /// `group` is not a valid input group resource. +@@ -5945,14 +5949,14 @@ namespace fs { + /// Encoding: + /// - UTF-8 + /// - NUL-padded (first NUL determines length, otherwise full array). +- field name: [max_fs_name_len]u8; ++ field name: [8]u8; + + /// String identifier of a file system driver (e.g. `FAT32`, `NFS`, ...) + /// + /// Encoding: + /// - UTF-8 + /// - NUL-padded (first NUL determines length, otherwise full array). +- field filesystem: [max_fs_type_len]u8; ++ field filesystem: [32]u8; + + bitstruct Flags : u16 { + /// This is the system boot filesystem. +@@ -6129,7 +6133,7 @@ namespace fs { + /// Additional packed information. + field flags: Flags; + +- enum FileType : u2 { ++ enum FileType : u8 { + item file = 0; + item directory = 1; + } +@@ -6144,7 +6148,7 @@ namespace fs { + /// `modified_date` is valid. + field modified_date_valid: bool; + +- reserve u12 = 0; ++ reserve u6 = 0; + } + } + +@@ -6156,7 +6160,7 @@ namespace fs { + /// NOTE: `len` is always `<= max_file_name_len`. + struct FileName { + field len: u8; +- field bytes: [max_file_name_len]u8; ++ field bytes: [120]u8; + } + + /// Creates a directory enumerator for `dir`. +@@ -7694,8 +7698,8 @@ namespace gui { + union WidgetEvent { + field event_type: Type; + +- field mouse: MouseEvent; +- field keyboard: KeyboardEvent; ++ field mouse: input.InputEvent; ++ field keyboard: input.InputEvent; + field control: WidgetControlMessage; + + //? TODO: Add event data +@@ -7853,8 +7857,8 @@ namespace gui { + union WindowEvent { + field event_type: Type; + +- field mouse: MouseEvent; +- field keyboard: KeyboardEvent; ++ field mouse: input.InputEvent; ++ field keyboard: input.InputEvent; + field widget_notify: WidgetNotifyEvent; + + enum Type : u16 { +@@ -8222,7 +8226,7 @@ namespace service { + + /// Defines the type of argument and the potential transformations the + /// kernel performs when passing the argument between caller and callee. +- enum MarshalType : u2 { ++ enum MarshalType : u8 { + /// The argument/result is unused. + /// Unused values must be set to zero or otherwise the call/return is invalid. + item unused = 0; +@@ -8256,7 +8260,7 @@ namespace service { + /// The kernel interprets resource handles in `results` in the *calling thread's current resource context*. + /// For each non-zero valid handle, it creates a strong binding for the resource context that scheduled + /// the original `Invoke` request and returns the translated handle in `Invoke.results`. +- item resource = 3; ++ item @"resource" = 3; + } + + /// Defines the signature of a function or overlapped operation. +@@ -8267,7 +8271,7 @@ namespace service { + /// NOTE: For both `inputs` and `outputs`: As soon as an index in the + /// array is `MarshalType.unused`, all following items must also be + /// `MarshalType.unused`. +- bitstruct FunctionSignature : u32 { ++ struct FunctionSignature { + /// Defines the number and type of the input arguments. + /// `inputs[0]` is the first argument, `inputs[7]` is the eighth argument. + field inputs: [8]MarshalType; +@@ -8305,7 +8309,7 @@ namespace service { + /// NOTE: The handler should perform a strong binding of resources inside `arguments` if it must + /// retain independent access even if the caller later unbinds/releases its handles. + /// This still does not prevent explicit destruction of the resource. +- typedef AsyncHandler = fnptr(context: ?*anyopaque, request: RequestToken, operation: u8, arguments: *const [8]usize) void; ++ typedef AsyncHandler = fnptr(anyptr,RequestToken, u8, *const [8]usize) void; + + /// The signature of the asynchronous cancellation handler function registered by an interface. + /// +@@ -8320,7 +8324,7 @@ namespace service { + /// + /// NOTE: A handler function should not yield the executing thread, as this will generate + /// hard to debug scenarios. +- typedef CancelHandler = fnptr(context: ?*anyopaque, request: RequestToken) void; ++ typedef CancelHandler = fnptr(anyptr,RequestToken) void; + + /// The signature of a synchronous function call registered by an interface. + /// +@@ -8336,7 +8340,7 @@ namespace service { + /// hard to debug scenarios. + /// + /// NOTE: `arguments` must be assumed invalid after the return of the callback. +- typedef Function = fnptr(context: ?*anyopaque, arguments: *const [8]usize) usize; ++ typedef Function = fnptr(anyptr,*const [8]usize) usize; + + /// Creates a new interface that can be invoked by other processes. + /// +@@ -8373,7 +8377,7 @@ namespace service { + /// NOTE: This can be used to implement a stateful interface that allows a process to create + /// the same interface more than once and still have context which of the interfaces were + /// called. +- in context: ?*anyopaque; ++ in context: anyptr; + + /// The synchronous implementation functions (vtable). + /// From 0762c939eabd2a644a479bf4580d89fd53437da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 16:26:28 +0100 Subject: [PATCH 22/36] Fixes the obvious human mistakes in ashet-1.0.abi --- .../abi-mapper/tests/stress/ashet-1.0.abi | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi index 4c795bda..cf590911 100644 --- a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi +++ b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi @@ -2238,7 +2238,8 @@ namespace input { /// If the device is present in groups, the event is enqueued into those group queues. syscall emit_device_event { in device: DeviceId; - in payload: InputEventPayload; + in type: InputEvent.Type; + in payload: InputEvent.Payload; /// `device` is not valid anymore (e.g. device removed) or was never valid. error InvalidDevice; @@ -2413,7 +2414,8 @@ namespace input { /// except it is marked synthetic. syscall queue_event { in group: InputGroup; - in payload: InputEventPayload; + in type: InputEvent.Type; + in payload: InputEvent.Payload; in force: bool; /// `group` is not a valid input group resource. @@ -7691,6 +7693,16 @@ namespace gui { enum RequestID : u16 { ... } } + /// Dummy struct to satisfy the parser. + struct MouseEvent { + field event_type: Type; + }; + + /// Dummy struct to satisfy the parser. + struct KeyboardEvent { + field event_type: Type; + }; + union WidgetEvent { field event_type: Type; @@ -8256,7 +8268,7 @@ namespace service { /// The kernel interprets resource handles in `results` in the *calling thread's current resource context*. /// For each non-zero valid handle, it creates a strong binding for the resource context that scheduled /// the original `Invoke` request and returns the translated handle in `Invoke.results`. - item resource = 3; + item @"resource" = 3; } /// Defines the signature of a function or overlapped operation. @@ -8305,7 +8317,7 @@ namespace service { /// NOTE: The handler should perform a strong binding of resources inside `arguments` if it must /// retain independent access even if the caller later unbinds/releases its handles. /// This still does not prevent explicit destruction of the resource. - typedef AsyncHandler = fnptr(context: ?*anyopaque, request: RequestToken, operation: u8, arguments: *const [8]usize) void; + typedef AsyncHandler = fnptr(context: ?anyptr, request: RequestToken, operation: u8, arguments: *const [8]usize) void; /// The signature of the asynchronous cancellation handler function registered by an interface. /// @@ -8320,7 +8332,7 @@ namespace service { /// /// NOTE: A handler function should not yield the executing thread, as this will generate /// hard to debug scenarios. - typedef CancelHandler = fnptr(context: ?*anyopaque, request: RequestToken) void; + typedef CancelHandler = fnptr(context: ?anyptr, request: RequestToken) void; /// The signature of a synchronous function call registered by an interface. /// @@ -8336,7 +8348,7 @@ namespace service { /// hard to debug scenarios. /// /// NOTE: `arguments` must be assumed invalid after the return of the callback. - typedef Function = fnptr(context: ?*anyopaque, arguments: *const [8]usize) usize; + typedef Function = fnptr(context: ?anyptr, arguments: *const [8]usize) usize; /// Creates a new interface that can be invoked by other processes. /// @@ -8373,7 +8385,7 @@ namespace service { /// NOTE: This can be used to implement a stateful interface that allows a process to create /// the same interface more than once and still have context which of the interfaces were /// called. - in context: ?*anyopaque; + in context: ?anyptr; /// The synchronous implementation functions (vtable). /// From 6a67ab7b3b9e9aa55dabd3e39b8adf25bdaadc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 16:46:28 +0100 Subject: [PATCH 23/36] Removes human errors from rework/findings.md --- src/tools/abi-mapper/rework/findings.md | 91 ++++++------------------- 1 file changed, 20 insertions(+), 71 deletions(-) diff --git a/src/tools/abi-mapper/rework/findings.md b/src/tools/abi-mapper/rework/findings.md index 469c86ba..4c502990 100644 --- a/src/tools/abi-mapper/rework/findings.md +++ b/src/tools/abi-mapper/rework/findings.md @@ -23,25 +23,7 @@ Zig/Rust conventions. --- -## Finding 2: Reserved keyword used as enum item name - -**File:** `tests/stress/ashet-1.0.abi:8259` - -**Code:** -``` -item resource = 3; -``` - -**Problem:** `resource` is a reserved keyword in the ABI language; using it bare as -an identifier causes an unexpected token error. - -**Note:** Typo in the `.abi` file, not an abi-mapper bug. No abi-mapper change needed. - -**Workaround applied:** Quoted the identifier → `item @"resource" = 3;`. - ---- - -## Finding 3: Named parameters in `fnptr` types not supported +## Finding 2: Named parameters in `fnptr` types not supported **File:** `tests/stress/ashet-1.0.abi:8308`, `8323`, `8339` @@ -64,7 +46,7 @@ replicate them faithfully. --- -## Finding 4: Constant used as array size before symbol resolution +## Finding 3: Constant used as array size before symbol resolution **File:** `tests/stress/ashet-1.0.abi:5948`, `5955` @@ -95,7 +77,7 @@ namespace they apply to, before any types that reference them. --- -## Finding 5: Non-standard integer widths not supported as enum/bitstruct backing types +## Finding 4: Non-standard integer widths not supported as enum/bitstruct backing types **File:** `tests/stress/ashet-1.0.abi:6132`, `8225` @@ -128,8 +110,21 @@ results into a fresh list rather than pre-sizing with `resize`. bitstructs and for languages that do support sub-byte types. **Workaround applied:** Changed backing type to the smallest standard width → `u8`. -Finding 11 is a cascading consequence and will be automatically resolved when -Finding 5 is implemented (FileType's bit_count will be 2, the bitstruct fits in u16). +Finding 5 (bitstruct Flags overflow) is a cascading consequence and will be +automatically resolved when this finding is implemented. + +--- + +## Finding 5: `bitstruct Flags : u16` exceeds 16 bits (cascading from Finding 4 workaround) + +**File:** `tests/stress/ashet-1.0.abi:6137` + +**Note:** Cascading consequence of the Finding 4 workaround — `FileType`'s backing +type was widened from `u2` to `u8`, adding 6 extra bits to the bitstruct. This will +be automatically resolved when Finding 4 is properly implemented: `FileType.bit_count` +will be 2, so the bitstruct field contributes 2 bits and fits within `u16` again. + +**Workaround applied:** Changed `reserve u12 = 0` to `reserve u6 = 0`. --- @@ -168,53 +163,7 @@ the silent skip cannot silently hide a real bug. --- -## Finding 8: Undefined types `MouseEvent` and `KeyboardEvent` in gui unions - -**File:** `tests/stress/ashet-1.0.abi:7697-7698`, `7856-7857` - -**Note:** Human error in the `.abi` file (incomplete gui namespace). Not an -abi-mapper bug. No abi-mapper change needed. - -**Workaround applied:** Changed both field types to `input.InputEvent`. - ---- - -## Finding 9: Undefined type `InputEventPayload` - -**File:** `tests/stress/ashet-1.0.abi:2241`, `2416` - -**Note:** Human error in the `.abi` file (should have been `InputEvent.Payload`). -Not an abi-mapper bug. No abi-mapper change needed. - -**Workaround applied:** Replaced with `InputEvent.Payload`. - ---- - -## Finding 10: `anyopaque` not a recognized built-in type - -**File:** `tests/stress/ashet-1.0.abi:8308`, `8323`, `8339`, `8376` - -**Note:** Human error in the `.abi` file. `anyopaque` is a Zig-specific type name; -the correct abi-mapper spelling is `anyptr`. Not an abi-mapper bug. - -**Workaround applied:** Replaced `?*anyopaque` / `?*anyopaque` with `anyptr`. - ---- - -## Finding 11: `bitstruct Flags : u16` exceeds 16 bits (cascading from Finding 5 workaround) - -**File:** `tests/stress/ashet-1.0.abi:6137` - -**Note:** Cascading consequence of the Finding 5 workaround — `FileType`'s backing -type was widened from `u2` to `u8`, adding 6 extra bits to the bitstruct. This will -be automatically resolved when Finding 5 is properly implemented: `FileType.bit_count` -will be 2, so the bitstruct field contributes 2 bits and fits within `u16` again. - -**Workaround applied:** Changed `reserve u12 = 0` to `reserve u6 = 0`. - ---- - -## Finding 12: Array fields in `bitstruct` not supported +## Finding 8: Array fields in `bitstruct` not supported **File:** `tests/stress/ashet-1.0.abi:8270` @@ -239,7 +188,7 @@ field into N individual fields or emit appropriate macros/accessors. --- -## Finding 13: Syscall with 2+ logic outputs surfaces an incorrect assertion in `validate_constraints` +## Finding 9: Syscall with 2+ logic outputs surfaces an incorrect assertion in `validate_constraints` **File:** `src/sema.zig:2081`, trigger at `tests/stress/ashet-1.0.abi:2187` From d55e7b482478bf6b3fbab0f86c4eb59e5a416e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 20:37:20 +0100 Subject: [PATCH 24/36] Improves abi-mapper a lot and fixes long-standing issues. --- src/tools/abi-mapper/build.zig | 22 +-- src/tools/abi-mapper/src/abi-parser.zig | 13 +- src/tools/abi-mapper/src/model.zig | 25 ++- src/tools/abi-mapper/src/sema.zig | 153 ++++++++++++++---- src/tools/abi-mapper/src/syntax.zig | 93 ++++++++++- .../tests/bitstruct_array_field.zig | 60 +++++++ .../abi-mapper/tests/constant_ordering.zig | 51 ++++++ .../abi-mapper/tests/digit_separator.zig | 66 ++++++++ .../abi-mapper/tests/doc_ref_resolution.zig | 4 +- .../abi-mapper/tests/fnptr_named_params.zig | 82 ++++++++++ .../tests/nonstandard_backing_type.zig | 101 ++++++++++++ .../tests/optional_type_handling.zig | 47 ++++++ .../abi-mapper/tests/syscall_output_count.zig | 75 +++++++++ src/tools/abi-mapper/tests/testsuite.zig | 12 ++ .../abi-mapper/tests/unknown_named_type.zig | 30 ++++ 15 files changed, 767 insertions(+), 67 deletions(-) create mode 100644 src/tools/abi-mapper/tests/bitstruct_array_field.zig create mode 100644 src/tools/abi-mapper/tests/constant_ordering.zig create mode 100644 src/tools/abi-mapper/tests/digit_separator.zig create mode 100644 src/tools/abi-mapper/tests/fnptr_named_params.zig create mode 100644 src/tools/abi-mapper/tests/nonstandard_backing_type.zig create mode 100644 src/tools/abi-mapper/tests/optional_type_handling.zig create mode 100644 src/tools/abi-mapper/tests/syscall_output_count.zig create mode 100644 src/tools/abi-mapper/tests/testsuite.zig create mode 100644 src/tools/abi-mapper/tests/unknown_named_type.zig diff --git a/src/tools/abi-mapper/build.zig b/src/tools/abi-mapper/build.zig index a16875db..11b52abd 100644 --- a/src/tools/abi-mapper/build.zig +++ b/src/tools/abi-mapper/build.zig @@ -36,27 +36,13 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&b.addInstallFile(output_file, "test/coverage.json").step); - const doc_parser_mod = b.createModule(.{ - .root_source_file = b.path("tests/doc_parser.zig"), + const testsuite_mod = b.createModule(.{ + .root_source_file = b.path("tests/testsuite.zig"), .target = target, .optimize = optimize, - .imports = &.{ - .{ .name = "abi-parser", .module = abi_parser_mod }, - }, - }); - const doc_parser_tests = b.addTest(.{ .root_module = doc_parser_mod }); - test_step.dependOn(&b.addRunArtifact(doc_parser_tests).step); - - const doc_ref_resolution_mod = b.createModule(.{ - .root_source_file = b.path("tests/doc_ref_resolution.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "abi-parser", .module = abi_parser_mod }, - }, + .imports = &.{.{ .name = "abi-parser", .module = abi_parser_mod }}, }); - const doc_ref_resolution_tests = b.addTest(.{ .root_module = doc_ref_resolution_mod }); - test_step.dependOn(&b.addRunArtifact(doc_ref_resolution_tests).step); + test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = testsuite_mod })).step); } pub const Converter = struct { diff --git a/src/tools/abi-mapper/src/abi-parser.zig b/src/tools/abi-mapper/src/abi-parser.zig index f26f5780..dc15c391 100644 --- a/src/tools/abi-mapper/src/abi-parser.zig +++ b/src/tools/abi-mapper/src/abi-parser.zig @@ -64,11 +64,20 @@ pub fn main() !u8 { null; defer if (uid_database) |*db| db.deinit(); - const analyzed_document: model.Document = try sema.analyze( + var analysis_errors: std.ArrayList(sema.AnalysisError) = .empty; + defer analysis_errors.deinit(allocator); + + const analyzed_document: model.Document = sema.analyze( allocator, ast_document, if (uid_database != null) &uid_database.? else null, - ); + &analysis_errors, + ) catch |err| { + for (analysis_errors.items) |ae| { + std.log.err("{s}", .{ae.message}); + } + return err; + }; // Save UID database back if it was loaded if (uid_database != null and id_db_path.len > 0) { diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index 5e6f09da..ce8ee389 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -425,14 +425,24 @@ pub const Type = union(enum) { .slice => false, }, .optional => |inner_idx| blk: { - // ?*T and ?[*]T are C-ABI compatible (represented as nullable pointers) - const inner = types[@intFromEnum(inner_idx)]; + // ?*T, ?[*]T, ?fnptr(...) are C-ABI compatible (nullable pointers). + // Resolve aliases and typedefs to reach the concrete inner type. + var idx = inner_idx; + const inner = while (true) { + const inner_t = types[@intFromEnum(idx)]; + switch (inner_t) { + .alias => |a| idx = a, + .typedef => |td| idx = td.alias, + else => break inner_t, + } + }; break :blk switch (inner) { .ptr => |ptr| switch (ptr.size) { .one, .unknown => true, .slice => false, }, .resource => true, // resources are also represented as nullable pointers in C-ABI + .fnptr => true, // nullable function pointer is C-ABI compatible .well_known => |id| switch (id) { .anyptr, .anyfnptr => true, else => false, @@ -486,8 +496,14 @@ pub const PointerSize = enum { unknown, }; +pub const FunctionPointerParam = struct { + /// Optional parameter name. `null` means unnamed. + name: ?[]const u8, + type: TypeIndex, +}; + pub const FunctionPointer = struct { - parameters: []const TypeIndex, + parameters: []const FunctionPointerParam, return_type: TypeIndex, }; @@ -558,6 +574,9 @@ pub const Enumeration = struct { docs: DocComment, full_qualified_name: FQN, backing_type: StandardType, + /// The actual declared bit width (e.g. 2 for `u2`). May be less than + /// `backing_type.size_in_bits()` when a non-standard width was declared. + bit_count: u8, kind: Kind, items: []const EnumItem, diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index f05f4ab0..a48ceabc 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -6,7 +6,11 @@ pub const uid_db = @import("uid_db.zig"); const Location = syntax.Location; -pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document, uid_database: ?*uid_db.UidDatabase) !model.Document { +pub const AnalysisError = struct { + message: []const u8, +}; + +pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document, uid_database: ?*uid_db.UidDatabase, errors_out: *std.ArrayList(AnalysisError)) !model.Document { var analyzer: Analyzer = .{ .allocator = allocator, .scope_stack = .empty, @@ -47,8 +51,8 @@ pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document, uid_data // TODO: Compute type sizes, field offsets if (analyzer.errors.items.len > 0) { - for (analyzer.errors.items) |err| { - std.log.err("{s}", .{err}); + for (analyzer.errors.items) |msg| { + try errors_out.append(allocator, .{ .message = msg }); } return error.AnalysisFailed; } @@ -169,13 +173,13 @@ const Analyzer = struct { const current_name = ana.current_scope_name(); const current_scope = ana.scope_map.get(current_name) orelse { - std.log.err("current scope: {f}", .{dotJoin(current_name)}); + std.debug.print("current scope: {f}\n", .{dotJoin(current_name)}); @panic("BUG: No current scope found!"); }; const inserted = if (current_scope.children.get(name)) |existing_child| blk: { if (scope_type != existing_child.type) { - std.log.err("scope mismatch for scope {f}: types {s} and {s} don't match", .{ + std.debug.print("scope mismatch for scope {f}: types {s} and {s} don't match\n", .{ std.zig.fmtId(name), @tagName(scope_type), @tagName(existing_child.type), @@ -217,14 +221,14 @@ const Analyzer = struct { } fn map(ana: *Analyzer, doc: syntax.Document) error{OutOfMemory}!void { - try ana.root.resize(ana.allocator, doc.nodes.len); - for (ana.root.items, doc.nodes) |*out, node| { - out.* = ana.map_node(node) catch |err| switch (err) { + for (doc.nodes) |node| { + const decl = ana.map_node(node) catch |err| switch (err) { // swallow silently here, all nodes are independent from each other error.FatalAnalysisError => continue, error.OutOfMemory => |e| return e, }; + try ana.root.append(ana.allocator, decl); } } @@ -359,9 +363,11 @@ const Analyzer = struct { }, } + const magic_backing = convert_enum(model.StandardType, magic_type.size); const enum_id = try ana.enums.append(.{ .uid = try ana.get_uid(type_def.full_qualified_name), - .backing_type = convert_enum(model.StandardType, magic_type.size), + .backing_type = magic_backing, + .bit_count = magic_backing.size_in_bits() orelse 0, .docs = type_def.docs, .full_qualified_name = type_def.full_qualified_name, @@ -419,7 +425,7 @@ const Analyzer = struct { std.debug.assert(bitstruct.backing_type.is_integer()); std.debug.assert(bitstruct.backing_type.size_in_bits() != null); // Assert we don't use `usize` or `isize` here! - const expected_size = bitstruct.backing_type.size_in_bits().?; + const expected_size = bitstruct.bit_count; var struct_size: u8 = 0; var has_error = false; @@ -473,12 +479,16 @@ const Analyzer = struct { .well_known => |stdtype| stdtype.size_in_bits(), - .@"enum" => |idx| ana.enums.get(idx).backing_type.size_in_bits(), + .@"enum" => |idx| ana.enums.get(idx).bit_count, .bitstruct => |idx| ana.bitstructs.get(idx).bit_count, .fnptr => null, .ptr => null, - .array => null, + .array => |arr| blk: { + const elem_type = ana.get_resolved_type(arr.child); + const elem_bits = ana.get_type_bit_size(elem_type) orelse break :blk null; + break :blk std.math.cast(u8, @as(u64, elem_bits) * arr.size); + }, .optional => null, .external => null, .resource => null, @@ -593,7 +603,9 @@ const Analyzer = struct { try list.append(a.allocator, param.*); }, else => { - std.log.err("unsupported optional builtin type {}", .{inner}); + try a.emit_error(Location.empty, + "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", + .{ param.name, @tagName(id) }); }, }, .ptr => |ptr| switch (ptr.size) { @@ -616,8 +628,14 @@ const Analyzer = struct { .resource => { try list.append(a.allocator, param.*); }, + .fnptr => { + // A function pointer is nullable in C — keep as-is. + try list.append(a.allocator, param.*); + }, else => { - std.log.err("unsupported optional type {}", .{inner}); + try a.emit_error(Location.empty, + "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", + .{ param.name, @tagName(inner) }); }, } }, @@ -861,7 +879,12 @@ const Analyzer = struct { .external => .keep, .alias => unreachable, - .unknown_named_type => unreachable, + .unknown_named_type => { + // resolve_named_types already emitted an error for this type. + // Skip the field silently rather than crashing. + std.debug.assert(ana.errors.items.len > 0); + break :blk .discard; + }, .unset_magic_type => unreachable, }; @@ -897,7 +920,6 @@ const Analyzer = struct { }; fn map_node(ana: *Analyzer, node: syntax.Node) MapError!model.Declaration { - errdefer std.log.err("failed to map node at {f}", .{node.location}); return switch (node.type) { .declaration => try ana.map_decl(node), @@ -980,8 +1002,15 @@ const Analyzer = struct { const NodeInfo = struct { full_name: model.FQN, docs: model.DocComment, - sub_type: ?model.StandardType, + sub_type: ?SubTypeInfo, location: Location, + + const SubTypeInfo = struct { + /// The ABI-surface standard type (rounded up to the nearest power-of-two byte width). + backing: model.StandardType, + /// The actual declared bit width (e.g. 2 for `u2`, 32 for `u32`). + bit_count: u8, + }; }; /// Parses a raw doc comment into a structured DocComment. @@ -1397,11 +1426,23 @@ const Analyzer = struct { } } - const sub_type: ?model.StandardType = if (needs_subtype) blk: { + const sub_type: ?NodeInfo.SubTypeInfo = if (needs_subtype) blk: { const model_type = try ana.map_type_inner(decl.subtype.?); - if (model_type != .well_known) - return ana.fatal_error(node.location, "subtype must be standard type, not a {s}", .{@tagName(model_type)}); - break :blk model_type.well_known; + break :blk switch (model_type) { + .well_known => |st| .{ + .backing = st, + .bit_count = st.size_in_bits() orelse 0, + }, + .uint => |bits| .{ + .backing = round_up_to_standard_type(bits), + .bit_count = bits, + }, + .int => |bits| .{ + .backing = round_up_to_standard_type(bits), + .bit_count = bits, + }, + else => return ana.fatal_error(node.location, "subtype must be an integer type, not a {s}", .{@tagName(model_type)}), + }; } else null; const info: NodeInfo = .{ @@ -1453,7 +1494,7 @@ const Analyzer = struct { fn map_enum(ana: *Analyzer, info: NodeInfo, decl: syntax.DeclarationNode) !model.Declaration.Data { std.debug.assert(info.sub_type != null); - if (!info.sub_type.?.is_integer()) { + if (!info.sub_type.?.backing.is_integer()) { return ana.fatal_error(info.location, "enum sub-type must be an integer", .{}); } @@ -1501,7 +1542,8 @@ const Analyzer = struct { .uid = try ana.get_uid(info.full_name), .docs = info.docs, .full_qualified_name = info.full_name, - .backing_type = info.sub_type.?, + .backing_type = info.sub_type.?.backing, + .bit_count = info.sub_type.?.bit_count, .items = try items.resolve(), .kind = kind, }); @@ -1596,6 +1638,12 @@ const Analyzer = struct { try ana.emit_error(info.location, "calls that are noreturn cannot have out parameters", .{}); } + if (mode == .syscall and outputs.fields.items.len > 1) { + return ana.fatal_error(info.location, + "syscall '{s}' has {d} 'out' parameters, but syscalls can have at most one", + .{ info.full_name[info.full_name.len - 1], outputs.fields.items.len }); + } + const output: model.GenericCall = .{ .uid = try ana.get_uid(info.full_name), .docs = info.docs, @@ -1687,8 +1735,8 @@ const Analyzer = struct { fn map_bit_struct(ana: *Analyzer, info: NodeInfo, decl: syntax.DeclarationNode) !model.Declaration.Data { std.debug.assert(info.sub_type != null); - if (!info.sub_type.?.is_integer()) { - return ana.fatal_error(info.location, "enum sub-type must be an integer", .{}); + if (!info.sub_type.?.backing.is_integer()) { + return ana.fatal_error(info.location, "bitstruct sub-type must be an integer", .{}); } var fields = ana.make_collector( @@ -1745,14 +1793,13 @@ const Analyzer = struct { .docs = info.docs, .full_qualified_name = info.full_name, .fields = try fields.resolve(), - .backing_type = info.sub_type.?, - - .bit_count = info.sub_type.?.size_in_bits() orelse 0, + .backing_type = info.sub_type.?.backing, + .bit_count = info.sub_type.?.bit_count, }); return .{ .bitstruct = index }; } - const MapTypeError = error{OutOfMemory}; + const MapTypeError = error{ OutOfMemory, FatalAnalysisError }; fn map_type(ana: *Analyzer, type_node: *const syntax.TypeNode) MapTypeError!model.TypeIndex { const decl: model.Type = try ana.map_type_inner(type_node); @@ -1791,7 +1838,15 @@ const Analyzer = struct { .external => false, .typedef => false, - .fnptr => |ptr| std.mem.eql(model.TypeIndex, ptr.parameters, other.fnptr.parameters) and ptr.return_type == other.fnptr.return_type, + .fnptr => |ptr| blk: { + if (ptr.return_type != other.fnptr.return_type) break :blk false; + if (ptr.parameters.len != other.fnptr.parameters.len) break :blk false; + for (ptr.parameters, other.fnptr.parameters) |a, b| { + if (a.type != b.type) break :blk false; + // Names are not part of type identity. + } + break :blk true; + }, .ptr => |ptr| ptr.size == other.ptr.size and ptr.alignment == other.ptr.alignment and ptr.is_const == other.ptr.is_const and ptr.child == other.ptr.child, @@ -1916,13 +1971,16 @@ const Analyzer = struct { }, .fnptr => |data| { - var params: std.ArrayList(model.TypeIndex) = .empty; + var params: std.ArrayList(model.FunctionPointerParam) = .empty; defer params.deinit(ana.allocator); try params.resize(ana.allocator, data.parameters.len); - for (params.items, data.parameters) |*out, dst| { - out.* = try ana.map_type(dst); + for (params.items, data.parameters) |*out, src| { + out.* = .{ + .name = src.name, + .type = try ana.map_type(src.type), + }; } const return_type = try ana.map_type(data.return_type); @@ -1978,8 +2036,28 @@ const Analyzer = struct { .null => .null, }, .symbol_name => |symbol_name| { - std.log.err("resolve symbol '{f}'", .{std.zig.fmtString(symbol_name)}); - @panic("symbol resolution not done yet"); + // Search already-mapped constants (those defined before this point). + // The symbol may be a simple name or a dot-qualified name. + for (ana.constants.items) |constant| { + const local = model.local_name(constant.full_qualified_name); + if (std.mem.eql(u8, local, symbol_name)) { + return constant.value; + } + // Also match fully qualified dot-joined name + var buf: [256]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + for (constant.full_qualified_name, 0..) |part, i| { + if (i > 0) fbs.writer().writeByte('.') catch {}; + fbs.writer().writeAll(part) catch {}; + } + const fqn_str = fbs.getWritten(); + if (std.mem.eql(u8, fqn_str, symbol_name)) { + return constant.value; + } + } + return ana.fatal_error(Location.empty, + "constant '{s}' must be defined before it is used here", + .{symbol_name}); }, .uint => |int| .{ .int = int }, .compound => |compound| { @@ -2218,6 +2296,7 @@ const Analyzer = struct { .well_known => |id| std.debug.assert(id.size_in_bits() != null), .@"enum", .bitstruct => {}, .uint, .int => {}, + .array => {}, else => std.debug.panic("Unsupported bit type: {t}", .{fld_type}), } @@ -2326,6 +2405,10 @@ fn Collector(comptime I: type) type { }; } +fn round_up_to_standard_type(bits: u8) model.StandardType { + return if (bits <= 8) .u8 else if (bits <= 16) .u16 else if (bits <= 32) .u32 else .u64; +} + fn convert_enum(comptime T: type, src: anytype) T { return switch (src) { inline else => |tag| @field(T, @tagName(tag)), diff --git a/src/tools/abi-mapper/src/syntax.zig b/src/tools/abi-mapper/src/syntax.zig index a2be6c1f..176519a4 100644 --- a/src/tools/abi-mapper/src/syntax.zig +++ b/src/tools/abi-mapper/src/syntax.zig @@ -55,6 +55,49 @@ const TokenType = enum { } }; +fn matchHexDigits(str: []const u8) ?usize { + var i: usize = 0; + var has_digit = false; + while (i < str.len) : (i += 1) { + const c = str[i]; + if (std.ascii.isHex(c)) { + has_digit = true; + } else if (c == '_') { + // digit separator — allowed + } else break; + } + return if (has_digit) i else null; +} + +fn matchBinaryDigits(str: []const u8) ?usize { + var i: usize = 0; + var has_digit = false; + while (i < str.len) : (i += 1) { + const c = str[i]; + if (c == '0' or c == '1') { + has_digit = true; + } else if (c == '_') { + // digit separator — allowed + } else break; + } + return if (has_digit) i else null; +} + +fn matchDecimalDigits(str: []const u8) ?usize { + if (str.len == 0) return null; + if (!std.ascii.isDigit(str[0])) return null; + var i: usize = 1; + while (i < str.len) : (i += 1) { + const c = str[i]; + if (std.ascii.isDigit(c)) { + // ok + } else if (c == '_') { + // digit separator — allowed + } else break; + } + return i; +} + pub fn match_identifier(str: []const u8) ?usize { const first_char = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const all_chars = first_char ++ "0123456789."; @@ -117,9 +160,9 @@ const patterns = blk: { .create(.comment, match.sequenceOf(.{ match.literal("//?"), match.takeNoneOf("\r\n") })), .create(.comment, match.literal("//?")), - .create(.number, match.sequenceOf(.{ match.literal("0x"), match.hexadecimalNumber })), - .create(.number, match.sequenceOf(.{ match.literal("0b"), match.binaryNumber })), - .create(.number, match.decimalNumber), + .create(.number, match.sequenceOf(.{ match.literal("0x"), matchHexDigits })), + .create(.number, match.sequenceOf(.{ match.literal("0b"), matchBinaryDigits })), + .create(.number, matchDecimalDigits), .create(.whitespace, match.whitespace), }; @@ -435,7 +478,15 @@ pub const Parser = struct { const tok = try parser.accept(.number); - const num = std.fmt.parseInt(u64, tok.text, 0) catch unreachable; + var stripped_buf: [80]u8 = undefined; + var stripped_len: usize = 0; + for (tok.text) |c| { + if (c != '_') { + stripped_buf[stripped_len] = c; + stripped_len += 1; + } + } + const num = std.fmt.parseInt(u64, stripped_buf[0..stripped_len], 0) catch unreachable; return .{ .uint = num, @@ -565,7 +616,7 @@ pub const Parser = struct { } if (try parser.try_accept(.fnptr)) |_| { - var params: std.ArrayList(*const TypeNode) = .empty; + var params: std.ArrayList(FnPtrParam) = .empty; defer params.deinit(parser.allocator); try parser.expect(.@"("); @@ -573,9 +624,29 @@ pub const Parser = struct { if (try parser.try_accept(.@")")) |_| break :blk; while (true) { - const param = try parser.accept_type(); + // Speculatively try to consume "name :" for a named parameter. + const save = parser.core.saveState(); + const param_name: ?[]const u8 = name_blk: { + const id_tok = parser.accept_any(&.{.identifier}) catch { + parser.core.restoreState(save); + break :name_blk null; + }; + if (try parser.try_accept(.@":")) |_| { + // Named parameter — strip @"…" escaping if present. + const raw = id_tok.text; + break :name_blk if (std.mem.startsWith(u8, raw, "@\"")) + raw[2 .. raw.len - 1] + else + raw; + } + // No colon — what we consumed was the start of the type. + // Restore and fall through to accept_type(). + parser.core.restoreState(save); + break :name_blk null; + }; - try params.append(parser.allocator, param); + const param_type = try parser.accept_type(); + try params.append(parser.allocator, .{ .name = param_name, .type = param_type }); if (try parser.try_accept(.@")")) |_| break :blk; @@ -800,6 +871,12 @@ pub const NamedValue = enum { true, }; +pub const FnPtrParam = struct { + /// Optional parameter name. `null` means the parameter is unnamed. + name: ?[]const u8, + type: *const TypeNode, +}; + pub const TypeNode = union(enum) { builtin: BuiltinType, named: []const u8, @@ -810,7 +887,7 @@ pub const TypeNode = union(enum) { size: *const ValueNode, }, fnptr: struct { - parameters: []const *const TypeNode, + parameters: []const FnPtrParam, return_type: *const TypeNode, }, unsigned_int: u8, diff --git a/src/tools/abi-mapper/tests/bitstruct_array_field.zig b/src/tools/abi-mapper/tests/bitstruct_array_field.zig new file mode 100644 index 00000000..1b66a9d4 --- /dev/null +++ b/src/tools/abi-mapper/tests/bitstruct_array_field.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "array of 2-bit enum in bitstruct packs correctly" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // 8 × 2-bit MarshalType = 16 bits per field, two fields = 32 bits = u32. + const source = + \\enum MarshalType : u2 { + \\ item none = 0; + \\ item small = 1; + \\ item large = 2; + \\ item ptr = 3; + \\} + \\bitstruct FunctionSignature : u32 { + \\ field inputs: [8]MarshalType; + \\ field outputs: [8]MarshalType; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.bitstructs.len); + const bs = doc.bitstructs[0]; + try std.testing.expectEqual(@as(u8, 32), bs.bit_count); + try std.testing.expectEqual(@as(usize, 2), bs.fields.len); + // inputs: bit_shift=0, bit_count=16 (8 × 2) + try std.testing.expectEqual(@as(?u8, 0), bs.fields[0].bit_shift); + try std.testing.expectEqual(@as(?u8, 16), bs.fields[0].bit_count); + // outputs: bit_shift=16, bit_count=16 + try std.testing.expectEqual(@as(?u8, 16), bs.fields[1].bit_shift); + try std.testing.expectEqual(@as(?u8, 16), bs.fields[1].bit_count); +} + +test "array of non-packable type in bitstruct emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // [4]*u8 contains pointers which have no fixed bit size. + const source = + \\bitstruct Bad : u32 { + \\ field ptrs: [4]*u8; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} diff --git a/src/tools/abi-mapper/tests/constant_ordering.zig b/src/tools/abi-mapper/tests/constant_ordering.zig new file mode 100644 index 00000000..c0d0a2f6 --- /dev/null +++ b/src/tools/abi-mapper/tests/constant_ordering.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "constant used as array size after definition succeeds" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Constant defined before the struct that uses it. + const source = + \\const max_len = 8; + \\struct Buffer { + \\ field data: [max_len]u8; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.structs.len); + // The field's type is an array with size 8 + const fld = doc.structs[0].logic_fields[0]; + const fld_type = doc.get_type(fld.type); + try std.testing.expect(fld_type.* == .array); + try std.testing.expectEqual(@as(u32, 8), fld_type.array.size); +} + +test "constant used as array size before definition emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Struct references constant that isn't declared yet. + const source = + \\struct Buffer { + \\ field data: [max_len]u8; + \\} + \\const max_len = 8; + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} diff --git a/src/tools/abi-mapper/tests/digit_separator.zig b/src/tools/abi-mapper/tests/digit_separator.zig new file mode 100644 index 00000000..f393fd14 --- /dev/null +++ b/src/tools/abi-mapper/tests/digit_separator.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "hex digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Foo : u64 { + \\ item infinity = 0xFFFF_FFFF_FFFF_FFFF; + \\ item half = 0x7FFF_FFFF_FFFF_FFFF; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + try std.testing.expectEqual(@as(usize, 2), doc.enums[0].items.len); + try std.testing.expectEqual(@as(i65, @bitCast(@as(u65, 0xFFFF_FFFF_FFFF_FFFF))), doc.enums[0].items[0].value); + try std.testing.expectEqual(@as(i65, 0x7FFF_FFFF_FFFF_FFFF), doc.enums[0].items[1].value); +} + +test "decimal digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Counts : u32 { + \\ item million = 1_000_000; + \\ item billion = 1_000_000_000; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + try std.testing.expectEqual(@as(i65, 1_000_000), doc.enums[0].items[0].value); + try std.testing.expectEqual(@as(i65, 1_000_000_000), doc.enums[0].items[1].value); +} + +test "binary digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Bits : u8 { + \\ item pattern = 0b1111_0000; + \\ item nibble = 0b0000_1111; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + try std.testing.expectEqual(@as(i65, 0b1111_0000), doc.enums[0].items[0].value); + try std.testing.expectEqual(@as(i65, 0b0000_1111), doc.enums[0].items[1].value); +} diff --git a/src/tools/abi-mapper/tests/doc_ref_resolution.zig b/src/tools/abi-mapper/tests/doc_ref_resolution.zig index c5fb7eca..947518f0 100644 --- a/src/tools/abi-mapper/tests/doc_ref_resolution.zig +++ b/src/tools/abi-mapper/tests/doc_ref_resolution.zig @@ -19,7 +19,9 @@ test "doc references resolve to contained syscall elements" { .core = .init(&tokenizer), }; const ast_document = try parser.accept_document(); - const analyzed_document = try abi_parser.sema.analyze(allocator, ast_document, null); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + const analyzed_document = try abi_parser.sema.analyze(allocator, ast_document, null, &errors); const await_completion = find_syscall_by_fqn( analyzed_document.syscalls, diff --git a/src/tools/abi-mapper/tests/fnptr_named_params.zig b/src/tools/abi-mapper/tests/fnptr_named_params.zig new file mode 100644 index 00000000..6b38cfe7 --- /dev/null +++ b/src/tools/abi-mapper/tests/fnptr_named_params.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +fn find_fnptr(doc: abi_parser.model.Document) ?abi_parser.model.FunctionPointer { + for (doc.types) |t| { + switch (t) { + .fnptr => |fp| return fp, + else => {}, + } + } + return null; +} + +test "named parameters in fnptr are stored" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Handler = fnptr(context: *u8, value: u32) void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 2), fp.parameters.len); + try std.testing.expectEqualStrings("context", fp.parameters[0].name.?); + try std.testing.expectEqualStrings("value", fp.parameters[1].name.?); +} + +test "unnamed parameters in fnptr store null" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Callback = fnptr(*u8, u32) void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 2), fp.parameters.len); + try std.testing.expectEqual(@as(?[]const u8, null), fp.parameters[0].name); + try std.testing.expectEqual(@as(?[]const u8, null), fp.parameters[1].name); +} + +test "mixed named and unnamed parameters" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Mixed = fnptr(ctx: *u8, u32) void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 2), fp.parameters.len); + try std.testing.expectEqualStrings("ctx", fp.parameters[0].name.?); + try std.testing.expectEqual(@as(?[]const u8, null), fp.parameters[1].name); +} + +test "fnptr with no parameters" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Thunk = fnptr() void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 0), fp.parameters.len); +} diff --git a/src/tools/abi-mapper/tests/nonstandard_backing_type.zig b/src/tools/abi-mapper/tests/nonstandard_backing_type.zig new file mode 100644 index 00000000..9eb5dab7 --- /dev/null +++ b/src/tools/abi-mapper/tests/nonstandard_backing_type.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "enum with u2 backing type" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum FileType : u2 { + \\ item unknown = 0; + \\ item file = 1; + \\ item dir = 2; + \\ item symlink = 3; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + const e = doc.enums[0]; + // bit_count must reflect the declared u2 width + try std.testing.expectEqual(@as(u8, 2), e.bit_count); + // backing_type must be rounded up to the next standard type (u8) + try std.testing.expectEqual(abi_parser.model.StandardType.u8, e.backing_type); + try std.testing.expectEqual(@as(usize, 4), e.items.len); +} + +test "enum with u10 backing type" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Wide : u10 { + \\ item a = 0; + \\ item b = 1023; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + const e = doc.enums[0]; + try std.testing.expectEqual(@as(u8, 10), e.bit_count); + try std.testing.expectEqual(abi_parser.model.StandardType.u16, e.backing_type); +} + +test "enum with non-integer backing type emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\struct BadType { field x: u8; } + \\enum Bad : BadType { + \\ item a = 0; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} + +test "bitstruct with u2 enum field packs correctly" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // FileType : u2 → bit_count = 2; bitstruct uses 2+2 = 4 bits, fits in u8. + const source = + \\enum FileType : u2 { + \\ item unknown = 0; + \\ item file = 1; + \\} + \\bitstruct Pair : u8 { + \\ field a: FileType; + \\ field b: FileType; + \\ reserve u4 = 0; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.bitstructs.len); + const bs = doc.bitstructs[0]; + // Two 2-bit fields + 4-bit reserve = 8 bits total + try std.testing.expectEqual(@as(u8, 8), bs.bit_count); + try std.testing.expectEqual(@as(usize, 3), bs.fields.len); + // First field: bit_shift=0, bit_count=2 + try std.testing.expectEqual(@as(?u8, 0), bs.fields[0].bit_shift); + try std.testing.expectEqual(@as(?u8, 2), bs.fields[0].bit_count); + // Second field: bit_shift=2, bit_count=2 + try std.testing.expectEqual(@as(?u8, 2), bs.fields[1].bit_shift); + try std.testing.expectEqual(@as(?u8, 2), bs.fields[1].bit_count); +} diff --git a/src/tools/abi-mapper/tests/optional_type_handling.zig b/src/tools/abi-mapper/tests/optional_type_handling.zig new file mode 100644 index 00000000..4ade294f --- /dev/null +++ b/src/tools/abi-mapper/tests/optional_type_handling.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "optional fnptr parameter passes through to native params" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Callback = fnptr() void; + \\syscall do_thing { + \\ in cb: ?Callback; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls.len); + const sc = doc.syscalls[0]; + // The optional fnptr should pass through to native_inputs unchanged. + try std.testing.expectEqual(@as(usize, 1), sc.native_inputs.len); + try std.testing.expectEqualStrings("cb", sc.native_inputs[0].name); +} + +test "optional u32 parameter emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall bad_call { + \\ in x: ?u32; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} diff --git a/src/tools/abi-mapper/tests/syscall_output_count.zig b/src/tools/abi-mapper/tests/syscall_output_count.zig new file mode 100644 index 00000000..19841607 --- /dev/null +++ b/src/tools/abi-mapper/tests/syscall_output_count.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "syscall with zero out params is valid" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall do_work { + \\ in x: u32; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls.len); + try std.testing.expectEqual(@as(usize, 0), doc.syscalls[0].logic_outputs.len); +} + +test "syscall with one out param is valid" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall get_value { + \\ out result: u32; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls.len); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls[0].logic_outputs.len); +} + +test "syscall with two out params emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall two_outputs { + \\ out a: u32; + \\ out b: u32; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} + +test "async_call with multiple out params is valid (not a syscall)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\async_call read_data { + \\ out bytes_read: u32; + \\ out eof: bool; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.async_calls.len); + try std.testing.expectEqual(@as(usize, 2), doc.async_calls[0].logic_outputs.len); +} diff --git a/src/tools/abi-mapper/tests/testsuite.zig b/src/tools/abi-mapper/tests/testsuite.zig new file mode 100644 index 00000000..7547d9ae --- /dev/null +++ b/src/tools/abi-mapper/tests/testsuite.zig @@ -0,0 +1,12 @@ +comptime { + _ = @import("doc_parser.zig"); + _ = @import("doc_ref_resolution.zig"); + _ = @import("digit_separator.zig"); + _ = @import("constant_ordering.zig"); + _ = @import("nonstandard_backing_type.zig"); + _ = @import("optional_type_handling.zig"); + _ = @import("unknown_named_type.zig"); + _ = @import("bitstruct_array_field.zig"); + _ = @import("syscall_output_count.zig"); + _ = @import("fnptr_named_params.zig"); +} diff --git a/src/tools/abi-mapper/tests/unknown_named_type.zig b/src/tools/abi-mapper/tests/unknown_named_type.zig new file mode 100644 index 00000000..e838fa54 --- /dev/null +++ b/src/tools/abi-mapper/tests/unknown_named_type.zig @@ -0,0 +1,30 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "struct with undefined field type emits error without crashing" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // References a type that does not exist — should produce an error, not a panic. + const source = + \\struct Broken { + \\ field x: NonExistentType; + \\ field y: u32; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} From 2547d28bfa94729a46e99461933480473b17594f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 21:17:10 +0100 Subject: [PATCH 25/36] Fixes code gen to handle new type changes. --- src/abi/utility/render_zig_code.zig | 7 +++++-- src/website/src/syscalls-gen.zig | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/abi/utility/render_zig_code.zig b/src/abi/utility/render_zig_code.zig index 94ccbaed..48c0a406 100644 --- a/src/abi/utility/render_zig_code.zig +++ b/src/abi/utility/render_zig_code.zig @@ -1138,11 +1138,14 @@ const ZigRenderer = struct { .fnptr => |fptr| { try writer.writeAll("*const fn("); - for (fptr.parameters, 0..) |ptype, i| { + for (fptr.parameters, 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } - try writer.print("{f}", .{zr.fmt_type(ptype)}); + if (param.name) |name| { + try writer.print("{f}: ", .{std.zig.fmtId(name)}); + } + try writer.print("{f}", .{zr.fmt_type(param.type)}); } try writer.writeAll(") callconv(.c) "); diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index 54ec1a9d..5c9003c5 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -639,8 +639,11 @@ const PageRenderer = struct { for (fnptr.parameters, 0..) |param, index| { if (index > 0) try writer.writeAll(", "); + if (param.name) |name| { + try writer.print("{f}: ", .{std.zig.fmtId(name)}); + } try writer.print("{f}", .{ - self.html.fmt_type(param), + self.html.fmt_type(param.type), }); } From 8d427a37558a5cac3dc2560b7e505b73336f6039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 22:23:19 +0100 Subject: [PATCH 26/36] For the decl lists, only renders the main docs and skips all other kinds of sections. --- src/website/src/syscalls-gen.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index 5c9003c5..72ca5282 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -577,7 +577,7 @@ const PageRenderer = struct { \\
      {f}
      \\ , .{ - html.fmt_docs(child.docs), + DocFmt{ .html = html, .docs = child.docs, .mode = .only_main }, }); } @@ -816,12 +816,19 @@ fn format_fqn(fqn: []const []const u8, writer: *std.Io.Writer) !void { const DocFmt = struct { docs: model.DocComment, html: *PageRenderer, + mode: enum { default, only_main } = .default, pub fn format(self: DocFmt, writer: *std.Io.Writer) !void { if (self.docs.is_empty()) return; for (self.docs.sections) |section| { + switch (self.mode) { + .default => {}, + .only_main => if (section.kind != .main) + continue, + } + try writer.print("
      \n", .{section.kind}); for (section.blocks) |block| { @@ -933,4 +940,3 @@ const DocFmt = struct { return ref_fqn[pos..]; } }; - From 4edf2b14043598c0f68f3a7dd46be91258d14af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 4 Mar 2026 23:06:19 +0100 Subject: [PATCH 27/36] Fixes src/abi/utility/render_zig_code.zig to properly handle zig name escapes. --- src/abi/build.zig | 19 ++++++- src/abi/tests/escaping.abi | 36 +++++++++++++ src/abi/utility/render_zig_code.zig | 78 ++++++++++++++++++++--------- src/tools/abi-mapper/build.zig | 6 ++- 4 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 src/abi/tests/escaping.abi diff --git a/src/abi/build.zig b/src/abi/build.zig index f8263cd3..c1134284 100644 --- a/src/abi/build.zig +++ b/src/abi/build.zig @@ -88,9 +88,26 @@ pub fn build(b: *std.Build) void { const abi_tests_run = b.addRunArtifact(abi_tests_exe); test_step.dependOn(&abi_tests_run.step); + + const escaping_tests_json = abi_mapper.get_json_dump(null, b.path("tests/escaping.abi")); + + for (std.enums.values(ConversionMode)) |mode| { + const escaping_tests_zig = convert_abi_file(b, render_zig_exe, escaping_tests_json, null, mode); + + const escaping_tests_exe = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = escaping_tests_zig, + .target = b.graph.host, + .optimize = .Debug, + }), + }); + const escaping_tests_run = b.addRunArtifact(escaping_tests_exe); + test_step.dependOn(&escaping_tests_run.step); + } } -pub fn convert_abi_file(b: *std.Build, render: *std.Build.Step.Compile, input: std.Build.LazyPath, patch: ?std.Build.LazyPath, mode: enum { kernel, definition }) std.Build.LazyPath { +const ConversionMode = enum { kernel, definition }; +pub fn convert_abi_file(b: *std.Build, render: *std.Build.Step.Compile, input: std.Build.LazyPath, patch: ?std.Build.LazyPath, mode: ConversionMode) std.Build.LazyPath { const generate_core_abi = b.addRunArtifact(render); generate_core_abi.addArg(@tagName(mode)); generate_core_abi.addFileArg(input); diff --git a/src/abi/tests/escaping.abi b/src/abi/tests/escaping.abi new file mode 100644 index 00000000..24b78458 --- /dev/null +++ b/src/abi/tests/escaping.abi @@ -0,0 +1,36 @@ +typedef Syscall_ID = <>; +enum SystemResource : usize +{ + ... + + typedef Type = <>; +} + +namespace resources { + syscall release { + in @"resource": SystemResource; + } + +} + +resource Thread {} + +//? Must be properly escaped as @"suspend" +syscall suspend { + in target: ?Thread; + error InvalidHandle; + error ThreadStopped; +} + +struct resume {} + +namespace embedded { + //? Must not be as @"escaped_suspend" + syscall suspend { + in target: ?Thread; + error InvalidHandle; + error ThreadStopped; + } + + struct resume {} +} diff --git a/src/abi/utility/render_zig_code.zig b/src/abi/utility/render_zig_code.zig index 48c0a406..3074835a 100644 --- a/src/abi/utility/render_zig_code.zig +++ b/src/abi/utility/render_zig_code.zig @@ -142,7 +142,7 @@ pub fn render_kernel(writer: *CodeWriter, allocator: std.mem.Allocator, schema: defer writer.dedent(); for (schema.syscalls) |syscall| { - try writer.print("pub export fn {s}{f}(", .{ renderer.symbol_prefix, fmt_fqn(syscall.full_qualified_name, "_") }); + try writer.print("pub export fn {f}(", .{renderer.fmt_sym_name(syscall.full_qualified_name, .with_prefix)}); for (syscall.native_inputs, 0..) |input, index| { if (index > 0) @@ -174,8 +174,8 @@ pub fn render_kernel(writer: *CodeWriter, allocator: std.mem.Allocator, schema: writer.indent(); defer writer.dedent(); - try writer.println("Callbacks.before_syscall(.{f});", .{fmt_fqn(syscall.full_qualified_name, "_")}); - try writer.println("defer Callbacks.after_syscall(.{f});", .{fmt_fqn(syscall.full_qualified_name, "_")}); + try writer.println("Callbacks.before_syscall(.{f});", .{renderer.fmt_sym_name(syscall.full_qualified_name, .no_prefix)}); + try writer.println("defer Callbacks.after_syscall(.{f});", .{renderer.fmt_sym_name(syscall.full_qualified_name, .no_prefix)}); if (has_errors) { // the return value must be an error union @@ -206,7 +206,7 @@ pub fn render_kernel(writer: *CodeWriter, allocator: std.mem.Allocator, schema: } try writer.println(" = Impl.{f}(", .{ - fmt_fqn(syscall.full_qualified_name, null), + fmt_fqn(syscall.full_qualified_name), }); { writer.indent(); @@ -386,7 +386,7 @@ const ZigRenderer = struct { try zr.writer.println("pub const {f} = {s}{f};", .{ fmt_id(model.local_name(child.full_qualified_name)), zr.scope_prefix, - fmt_fqn(child.full_qualified_name, null), + fmt_fqn(child.full_qualified_name), }); }, @@ -395,6 +395,38 @@ const ZigRenderer = struct { } } + fn fmt_sym_name(zr: *ZigRenderer, sym: model.FQN, mode: FmtSymName.Mode) FmtSymName { + return .{ + .zr = zr, + .sym = sym, + .mode = mode, + }; + } + + const FmtSymName = struct { + const Mode = enum { with_prefix, no_prefix }; + zr: *ZigRenderer, + sym: model.FQN, + mode: FmtSymName.Mode, + + pub fn format(fmt: FmtSymName, writer: *std.Io.Writer) !void { + var full_name_buf: [512]u8 = undefined; + var full_name_writer: std.Io.Writer = .fixed(&full_name_buf); + + switch (fmt.mode) { + .with_prefix => try full_name_writer.writeAll(fmt.zr.symbol_prefix), + .no_prefix => {}, + } + for (fmt.sym, 0..) |node, i| { + if (i > 0) + try full_name_writer.writeAll("_"); + try full_name_writer.writeAll(node); + } + + try writer.print("{f}", .{std.zig.fmtId(full_name_writer.buffered())}); + } + }; + fn render_userland_call(zr: *ZigRenderer, syscall: *const model.GenericCall, children: []const model.Declaration) !void { std.debug.assert(children.len == 0); @@ -480,9 +512,8 @@ const ZigRenderer = struct { } } - try writer.println("const __result = {s}{f}(", .{ - zr.symbol_prefix, - fmt_fqn(syscall.full_qualified_name, "_"), + try writer.println("const __result = {f}(", .{ + zr.fmt_sym_name(syscall.full_qualified_name, .with_prefix), }); writer.indent(); @@ -538,7 +569,7 @@ const ZigRenderer = struct { }); } try writer.println("else => return __handle_unexpected(.{f}, __result),", .{ - fmt_fqn(syscall.full_qualified_name, "_"), + zr.fmt_sym_name(syscall.full_qualified_name, .no_prefix), }); writer.dedent(); try writer.writeln("}"); @@ -632,9 +663,8 @@ const ZigRenderer = struct { .local_name => try zr.writer.println("pub extern fn {f}(", .{ fmt_id(model.local_name(syscall.full_qualified_name)), }), - .full_name => try zr.writer.println("extern fn {s}{f}(", .{ - zr.symbol_prefix, - fmt_fqn(syscall.full_qualified_name, "_"), + .full_name => try zr.writer.println("extern fn {f}(", .{ + zr.fmt_sym_name(syscall.full_qualified_name, .with_prefix), }), } @@ -687,7 +717,7 @@ const ZigRenderer = struct { try zr.writer.writeln("pub const Inputs = extern struct {"); zr.writer.indent(); - try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name, null)}); + try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name)}); for (arc.native_inputs) |field| { try zr.render_docs(field.docs); try zr.writer.print("{f}: {f}", .{ @@ -705,7 +735,7 @@ const ZigRenderer = struct { try zr.writer.writeln("pub const Outputs = extern struct {"); zr.writer.indent(); - try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name, null)}); + try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name)}); for (arc.native_outputs) |field| { try zr.render_docs(field.docs); try zr.writer.print("{f}: {f}", .{ @@ -1019,8 +1049,8 @@ const ZigRenderer = struct { } fn render_docs(zr: *ZigRenderer, docs: model.DocComment) !void { - _=zr; - _=docs; + _ = zr; + _ = docs; // TODO: COnsider if it's worth to include the doc strings inside the generated zig code // for (docs) |line| { // try zr.writer.println("/// {s}", .{line}); @@ -1105,32 +1135,32 @@ const ZigRenderer = struct { .@"enum" => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_enum(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_enum(index).full_qualified_name), }), .@"struct" => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_struct(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_struct(index).full_qualified_name), }), .@"union" => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_union(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_union(index).full_qualified_name), }), .bitstruct => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_bitstruct(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_bitstruct(index).full_qualified_name), }), .resource => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_resource(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_resource(index).full_qualified_name), }), .typedef => |typedef| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(typedef.full_qualified_name, null), + fmt_fqn(typedef.full_qualified_name), }), .uint => |size| try writer.print("u{}", .{size}), @@ -1228,10 +1258,10 @@ const ZigRenderer = struct { } }; -fn fmt_fqn(fqn: []const []const u8, sep: ?[]const u8) FqnFmt { +fn fmt_fqn(fqn: []const []const u8) FqnFmt { return .{ .fqn = fqn, - .sep = sep orelse ".", + .sep = ".", }; } diff --git a/src/tools/abi-mapper/build.zig b/src/tools/abi-mapper/build.zig index 11b52abd..deb4f6e3 100644 --- a/src/tools/abi-mapper/build.zig +++ b/src/tools/abi-mapper/build.zig @@ -49,9 +49,11 @@ pub const Converter = struct { b: *std.Build, executable: *std.Build.Step.Compile, - pub fn get_json_dump(cc: Converter, id_database: std.Build.LazyPath, input: std.Build.LazyPath) std.Build.LazyPath { + pub fn get_json_dump(cc: Converter, id_database: ?std.Build.LazyPath, input: std.Build.LazyPath) std.Build.LazyPath { const generate_json = cc.b.addRunArtifact(cc.executable); - generate_json.addPrefixedFileArg("--id-db=", id_database); + if (id_database) |db_path| { + generate_json.addPrefixedFileArg("--id-db=", db_path); + } const abi_json = generate_json.addPrefixedOutputFileArg("--output=", "abi.json"); generate_json.addFileArg(input); return abi_json; From a45b4313102c1ffd6d1ba4758b465082f72f3c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 15 Jun 2026 22:20:57 +0200 Subject: [PATCH 28/36] Codex: Introduces a tighter validation of FQN parsing. --- src/tools/abi-mapper/tests/doc_parser.zig | 10 + .../abi-mapper/tests/doc_ref_emission.abi | 28 +++ .../abi-mapper/tests/doc_ref_emission.zig | 206 ++++++++++++++++++ src/tools/abi-mapper/tests/testsuite.zig | 1 + 4 files changed, 245 insertions(+) create mode 100644 src/tools/abi-mapper/tests/doc_ref_emission.abi create mode 100644 src/tools/abi-mapper/tests/doc_ref_emission.zig diff --git a/src/tools/abi-mapper/tests/doc_parser.zig b/src/tools/abi-mapper/tests/doc_parser.zig index 52bb0cd5..0c856c64 100644 --- a/src/tools/abi-mapper/tests/doc_parser.zig +++ b/src/tools/abi-mapper/tests/doc_parser.zig @@ -118,6 +118,16 @@ test "cross-reference @`fqn`" { try std.testing.expectEqualStrings(" for details.", content[2].text.value); } +test "legacy @ref syntax stays plain text" { + var parsed = try parse_doc(&.{" See @ref foo.bar.Baz for details."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 1), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("See @ref foo.bar.Baz for details.", content[0].text.value); +} + test "emphasis *text*" { var parsed = try parse_doc(&.{" This is *important* text."}); defer parsed.deinit(); diff --git a/src/tools/abi-mapper/tests/doc_ref_emission.abi b/src/tools/abi-mapper/tests/doc_ref_emission.abi new file mode 100644 index 00000000..48e51f99 --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_ref_emission.abi @@ -0,0 +1,28 @@ +namespace resources { + syscall destroy { + in @"resource": usize; + } + + enum BindOperation : u8 { + item weak = 0; + + /// This operation ensures that the binding is at least @`weak`. + item at_least_weak = 1; + } + + /// NOTE: @`destroy` always succeeds; destroying an invalid handle is a no-op. + syscall bind {} +} + +namespace link { + struct Route { + field prefix_len: u8; + } + + /// NOTE: The derived route is configured as follows: + /// - @`link.Route.prefix_len` set to `0`. + /// - `::` remains inline code. + struct Router { + field address: u8; + } +} diff --git a/src/tools/abi-mapper/tests/doc_ref_emission.zig b/src/tools/abi-mapper/tests/doc_ref_emission.zig new file mode 100644 index 00000000..d257882a --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_ref_emission.zig @@ -0,0 +1,206 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); +const model = abi_parser.model; + +test "doc references survive JSON roundtrip emission" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var roundtrip = try analyze_and_roundtrip_json(allocator, "tests/doc_ref_emission.abi"); + defer roundtrip.deinit(); + + const bind = find_syscall_by_fqn(roundtrip.value.syscalls, "resources.bind") orelse + return error.TestUnexpectedResult; + try std.testing.expect(has_ref_fqn(bind.docs, "resources.destroy")); + + const bind_operation = find_enum_by_fqn(roundtrip.value.enums, "resources.BindOperation") orelse + return error.TestUnexpectedResult; + const at_least_weak = find_enum_item_by_name(bind_operation.items, "at_least_weak") orelse + return error.TestUnexpectedResult; + try std.testing.expect(has_ref_fqn(at_least_weak.docs, "resources.BindOperation.weak")); + + const router = find_struct_by_fqn(roundtrip.value.structs, "link.Router") orelse + return error.TestUnexpectedResult; + try std.testing.expect(has_ref_fqn(router.docs, "link.Route.prefix_len")); + try std.testing.expect(has_code_value(router.docs, "::")); +} + +test "stress fixture serializes to valid JSON" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var roundtrip = try analyze_and_roundtrip_json(allocator, "tests/stress/ashet-1.0.abi"); + defer roundtrip.deinit(); + + try std.testing.expect(roundtrip.value.root.len > 0); +} + +fn analyze_and_roundtrip_json(allocator: std.mem.Allocator, path: []const u8) !std.json.Parsed(model.Document) { + const analyzed_document = try analyze_file(allocator, path); + + var json: std.ArrayList(u8) = .empty; + defer json.deinit(allocator); + try model.to_json_str(analyzed_document, json.writer(allocator)); + + return model.from_json_str(allocator, json.items); +} + +fn analyze_file(allocator: std.mem.Allocator, path: []const u8) !model.Document { + const abi_source = try std.fs.cwd().readFileAlloc(allocator, path, 1 << 20); + + var tokenizer: abi_parser.syntax.Tokenizer = .init(abi_source, path); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast_document = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast_document, null, &errors); +} + +fn find_syscall_by_fqn( + syscalls: []const model.GenericCall, + expected: []const u8, +) ?model.GenericCall { + for (syscalls) |syscall| { + if (fqn_equals(syscall.full_qualified_name, expected)) { + return syscall; + } + } + return null; +} + +fn find_struct_by_fqn( + structs: []const model.Struct, + expected: []const u8, +) ?model.Struct { + for (structs) |item| { + if (fqn_equals(item.full_qualified_name, expected)) { + return item; + } + } + return null; +} + +fn find_enum_by_fqn( + enums: []const model.Enumeration, + expected: []const u8, +) ?model.Enumeration { + for (enums) |item| { + if (fqn_equals(item.full_qualified_name, expected)) { + return item; + } + } + return null; +} + +fn find_enum_item_by_name(items: []const model.EnumItem, name: []const u8) ?model.EnumItem { + for (items) |item| { + if (std.mem.eql(u8, item.name, name)) { + return item; + } + } + return null; +} + +fn fqn_equals(fqn: model.FQN, expected: []const u8) bool { + var parts = std.mem.splitScalar(u8, expected, '.'); + var index: usize = 0; + while (parts.next()) |part| { + if (part.len == 0 or index >= fqn.len) { + return false; + } + if (!std.mem.eql(u8, fqn[index], part)) { + return false; + } + index += 1; + } + return index == fqn.len; +} + +fn has_ref_fqn(docs: model.DocComment, expected: []const u8) bool { + for (docs.sections) |section| { + for (section.blocks) |block| { + switch (block) { + .paragraph => |paragraph| { + if (inlines_have_ref_fqn(paragraph.content, expected)) return true; + }, + .unordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .ordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .code_block => {}, + } + } + } + return false; +} + +fn inlines_have_ref_fqn(inlines: []const model.DocComment.Inline, expected: []const u8) bool { + for (inlines) |inl| { + switch (inl) { + .ref => |r| { + if (std.mem.eql(u8, r.fqn, expected)) return true; + }, + .emphasis => |e| { + if (inlines_have_ref_fqn(e.content, expected)) return true; + }, + .link => |l| { + if (inlines_have_ref_fqn(l.content, expected)) return true; + }, + .text, .code => {}, + } + } + return false; +} + +fn has_code_value(docs: model.DocComment, expected: []const u8) bool { + for (docs.sections) |section| { + for (section.blocks) |block| { + switch (block) { + .paragraph => |paragraph| { + if (inlines_have_code_value(paragraph.content, expected)) return true; + }, + .unordered_list => |list| { + for (list.items) |item| { + if (inlines_have_code_value(item, expected)) return true; + } + }, + .ordered_list => |list| { + for (list.items) |item| { + if (inlines_have_code_value(item, expected)) return true; + } + }, + .code_block => {}, + } + } + } + return false; +} + +fn inlines_have_code_value(inlines: []const model.DocComment.Inline, expected: []const u8) bool { + for (inlines) |inl| { + switch (inl) { + .code => |code| { + if (std.mem.eql(u8, code.value, expected)) return true; + }, + .emphasis => |e| { + if (inlines_have_code_value(e.content, expected)) return true; + }, + .link => |l| { + if (inlines_have_code_value(l.content, expected)) return true; + }, + .text, .ref => {}, + } + } + return false; +} diff --git a/src/tools/abi-mapper/tests/testsuite.zig b/src/tools/abi-mapper/tests/testsuite.zig index 7547d9ae..f5fd0877 100644 --- a/src/tools/abi-mapper/tests/testsuite.zig +++ b/src/tools/abi-mapper/tests/testsuite.zig @@ -1,5 +1,6 @@ comptime { _ = @import("doc_parser.zig"); + _ = @import("doc_ref_emission.zig"); _ = @import("doc_ref_resolution.zig"); _ = @import("digit_separator.zig"); _ = @import("constant_ordering.zig"); From a56ddd39a22058d2509329ad8ee117c98edda3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 20:37:53 +0200 Subject: [PATCH 29/36] Fixes several bugs in the abi-mapper tool. --- src/os/build.zig | 3 +- src/tools/abi-mapper/src/doc_comment.zig | 153 ++++++++++---- src/tools/abi-mapper/src/sema.zig | 200 ++++++++++++++---- src/tools/abi-mapper/src/syntax.zig | 26 ++- .../abi-mapper/tests/digit_separator.zig | 19 ++ src/tools/abi-mapper/tests/doc_parser.zig | 67 ++++++ .../abi-mapper/tests/doc_ref_resolution.zig | 41 ++++ .../abi-mapper/tests/stress/ashet-1.0.abi | 200 +++++++++--------- src/website/src/syscalls-gen.zig | 4 +- 9 files changed, 519 insertions(+), 194 deletions(-) diff --git a/src/os/build.zig b/src/os/build.zig index 24de2d2e..133326ee 100644 --- a/src/os/build.zig +++ b/src/os/build.zig @@ -47,8 +47,7 @@ pub fn build(b: *std.Build) void { }); const assets_dep = b.dependency("assets", .{}); - // const disk_image_dep = b.dependency("dimmer", .{ .release = true }); - const disk_image_dep = b.dependency("dimmer", .{}); + const disk_image_dep = b.dependency("dimmer", .{ .release = true }); const limine_dep = b.dependency("zig_limine_install", .{ .target = b.graph.host, .optimize = .ReleaseSafe }); diff --git a/src/tools/abi-mapper/src/doc_comment.zig b/src/tools/abi-mapper/src/doc_comment.zig index 546a39af..c47c1d46 100644 --- a/src/tools/abi-mapper/src/doc_comment.zig +++ b/src/tools/abi-mapper/src/doc_comment.zig @@ -14,6 +14,26 @@ pub const ParseOptions = struct { ref_lookup_context: ?*anyopaque = null, }; +pub const ParseError = error{ + UnclosedCodeFence, + UnclosedInlineReference, + UnclosedInlineCode, + UnclosedInlineLink, + MalformedInlineLink, + UnclosedAutolink, +}; + +pub fn describe_parse_error(err: ParseError) []const u8 { + return switch (err) { + error.UnclosedCodeFence => "unclosed fenced code block", + error.UnclosedInlineReference => "unclosed inline reference", + error.UnclosedInlineCode => "unclosed inline code span", + error.UnclosedInlineLink => "unclosed inline link", + error.MalformedInlineLink => "malformed inline link", + error.UnclosedAutolink => "unclosed autolink", + }; +} + /// A parsed doc comment together with the arena that owns its memory. /// Call deinit() when the DocComment is no longer needed. pub const ParsedDocComment = struct { @@ -29,7 +49,7 @@ pub const ParsedDocComment = struct { /// The caller owns the result and must call deinit() to release memory. /// /// Each raw line should be `token.text[3..]` where token.text starts with `///`. -pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8, options: ParseOptions) !ParsedDocComment { +pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8, options: ParseOptions) (ParseError || error{OutOfMemory})!ParsedDocComment { var result: ParsedDocComment = .{ .arena = std.heap.ArenaAllocator.init(backing_allocator), .comment = undefined, @@ -43,7 +63,7 @@ pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8 /// The caller owns the arena and is responsible for its lifetime. /// /// Each raw line should be `token.text[3..]` where token.text starts with `///`. -pub fn parse_into_arena(arena: *std.heap.ArenaAllocator, raw_lines: []const []const u8, options: ParseOptions) !DocComment { +pub fn parse_into_arena(arena: *std.heap.ArenaAllocator, raw_lines: []const []const u8, options: ParseOptions) (ParseError || error{OutOfMemory})!DocComment { if (raw_lines.len == 0) return .empty; var ctx: ParseContext = .{ @@ -61,7 +81,7 @@ const ParseContext = struct { ref_lookup: ?RefLookupFn, ref_lookup_context: ?*anyopaque, - fn parse_doc(ctx: *ParseContext, raw_lines: []const []const u8) !DocComment { + fn parse_doc(ctx: *ParseContext, raw_lines: []const []const u8) (ParseError || error{OutOfMemory})!DocComment { // Normalize lines: strip one optional leading space (the /// separator), right-trim. var norm_lines: std.ArrayList([]const u8) = .empty; defer norm_lines.deinit(ctx.allocator); @@ -185,6 +205,10 @@ const ParseContext = struct { try para_lines.append(ctx.allocator, std.mem.trimLeft(u8, line, " \t")); } + if (in_fence) { + return error.UnclosedCodeFence; + } + // Flush whatever remains try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); @@ -204,7 +228,7 @@ const ParseContext = struct { acc_kind: *AccKind, para_lines: *std.ArrayList([]const u8), list_items: *std.ArrayList(std.ArrayList([]const u8)), - ) !void { + ) (ParseError || error{OutOfMemory})!void { switch (acc_kind.*) { .none => {}, .paragraph => { @@ -233,7 +257,65 @@ const ParseContext = struct { acc_kind.* = .none; } - fn parse_inline(ctx: *ParseContext, text: []const u8) ![]const DocComment.Inline { + fn is_escapable_inline_char(c: u8) bool { + return switch (c) { + '`', '*', '[', '<', '@', '\\' => true, + else => false, + }; + } + + fn find_inline_code_end(_: *ParseContext, text: []const u8, code_start: usize) ?usize { + var i = code_start; + while (i < text.len) { + if (text[i] == '\\' and i + 1 < text.len and is_escapable_inline_char(text[i + 1])) { + if (text[i + 1] == '`') { + if (i + 2 < text.len and text[i + 2] == '`') { + i += 2; + continue; + } + return i + 1; + } + i += 2; + continue; + } + if (text[i] == '`') { + return i; + } + i += 1; + } + return null; + } + + fn unescape_inline_text(ctx: *ParseContext, text: []const u8) error{OutOfMemory}![]const u8 { + var i: usize = 0; + while (i + 1 < text.len) : (i += 1) { + if (text[i] == '\\' and is_escapable_inline_char(text[i + 1])) break; + } + if (i + 1 >= text.len) { + return text; + } + + var unescaped: std.ArrayList(u8) = .empty; + defer unescaped.deinit(ctx.allocator); + + var text_start: usize = 0; + i = 0; + while (i < text.len) { + if (i + 1 < text.len and text[i] == '\\' and is_escapable_inline_char(text[i + 1])) { + try unescaped.appendSlice(ctx.allocator, text[text_start..i]); + try unescaped.append(ctx.allocator, text[i + 1]); + i += 2; + text_start = i; + continue; + } + i += 1; + } + + try unescaped.appendSlice(ctx.allocator, text[text_start..]); + return unescaped.toOwnedSlice(ctx.allocator); + } + + fn parse_inline(ctx: *ParseContext, text: []const u8) (ParseError || error{OutOfMemory})![]const DocComment.Inline { var result: std.ArrayList(DocComment.Inline) = .empty; defer result.deinit(ctx.allocator); @@ -246,11 +328,7 @@ const ParseContext = struct { // Escape sequence: \` \* \[ \< \@ \\ if (c == '\\' and i + 1 < text.len) { const next = text[i + 1]; - const escapable = switch (next) { - '`', '*', '[', '<', '@', '\\' => true, - else => false, - }; - if (escapable) { + if (is_escapable_inline_char(next)) { if (i > text_start) { try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } @@ -277,8 +355,7 @@ const ParseContext = struct { i = ref_start + rel_end + 1; text_start = i; } else { - // Unmatched backtick — treat as literal text - i += 1; + return error.UnclosedInlineReference; } continue; } @@ -289,13 +366,13 @@ const ParseContext = struct { try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } const code_start = i + 1; - if (std.mem.indexOfScalar(u8, text[code_start..], '`')) |rel_end| { - const code_val = text[code_start .. code_start + rel_end]; + if (ctx.find_inline_code_end(text, code_start)) |code_end| { + const code_val = try ctx.unescape_inline_text(text[code_start..code_end]); try result.append(ctx.allocator, .{ .code = .{ .value = code_val } }); - i = code_start + rel_end + 1; + i = code_end + 1; text_start = i; } else { - i += 1; + return error.UnclosedInlineCode; } continue; } @@ -338,18 +415,18 @@ const ParseContext = struct { if (c == '[') { if (std.mem.indexOfScalarPos(u8, text, i + 1, ']')) |close_bracket| { if (close_bracket + 1 < text.len and text[close_bracket + 1] == '(') { - if (std.mem.indexOfScalarPos(u8, text, close_bracket + 2, ')')) |close_paren| { - if (i > text_start) { - try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); - } - const display = text[i + 1 .. close_bracket]; - const url = text[close_bracket + 2 .. close_paren]; - const content = try ctx.parse_inline(display); - try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); - i = close_paren + 1; - text_start = i; - continue; + const close_paren = std.mem.indexOfScalarPos(u8, text, close_bracket + 2, ')') orelse + return error.UnclosedInlineLink; + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } + const display = text[i + 1 .. close_bracket]; + const url = text[close_bracket + 2 .. close_paren]; + const content = try ctx.parse_inline(display); + try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); + i = close_paren + 1; + text_start = i; + continue; } } } @@ -367,18 +444,18 @@ const ParseContext = struct { } } if (matched_scheme) { - if (std.mem.indexOfScalarPos(u8, text, i + 1, '>')) |close_angle| { - if (i > text_start) { - try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); - } - const url = text[i + 1 .. close_angle]; - const content = try ctx.allocator.alloc(DocComment.Inline, 1); - content[0] = .{ .text = .{ .value = url } }; - try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); - i = close_angle + 1; - text_start = i; - continue; + const close_angle = std.mem.indexOfScalarPos(u8, text, i + 1, '>') orelse + return error.UnclosedAutolink; + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); } + const url = text[i + 1 .. close_angle]; + const content = try ctx.allocator.alloc(DocComment.Inline, 1); + content[0] = .{ .text = .{ .value = url } }; + try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); + i = close_angle + 1; + text_start = i; + continue; } } diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index a48ceabc..0826cbc3 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -50,17 +50,14 @@ pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document, uid_data // TODO: Compute type sizes, field offsets - if (analyzer.errors.items.len > 0) { - for (analyzer.errors.items) |msg| { - try errors_out.append(allocator, .{ .message = msg }); - } - return error.AnalysisFailed; - } + try analyzer.fail_if_errors(errors_out); // TODO: Implement garbage collection for unreferenced things try analyzer.validate_constraints(); + try analyzer.fail_if_errors(errors_out); + return .{ .root = try analyzer.root.toOwnedSlice(analyzer.allocator), @@ -603,9 +600,7 @@ const Analyzer = struct { try list.append(a.allocator, param.*); }, else => { - try a.emit_error(Location.empty, - "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", - .{ param.name, @tagName(id) }); + try a.emit_error(Location.empty, "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", .{ param.name, @tagName(id) }); }, }, .ptr => |ptr| switch (ptr.size) { @@ -633,9 +628,7 @@ const Analyzer = struct { try list.append(a.allocator, param.*); }, else => { - try a.emit_error(Location.empty, - "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", - .{ param.name, @tagName(inner) }); + try a.emit_error(Location.empty, "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", .{ param.name, @tagName(inner) }); }, } }, @@ -901,6 +894,16 @@ const Analyzer = struct { return error.FatalAnalysisError; } + fn fail_if_errors(ana: *Analyzer, errors_out: *std.ArrayList(AnalysisError)) !void { + if (ana.errors.items.len == 0) { + return; + } + for (ana.errors.items) |msg| { + try errors_out.append(ana.allocator, .{ .message = msg }); + } + return error.AnalysisFailed; + } + fn emit_error(ana: *Analyzer, location: Location, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { const msg = try std.fmt.allocPrint( ana.allocator, @@ -920,7 +923,6 @@ const Analyzer = struct { }; fn map_node(ana: *Analyzer, node: syntax.Node) MapError!model.Declaration { - return switch (node.type) { .declaration => try ana.map_decl(node), .typedef => try ana.map_typedef(node), @@ -1019,7 +1021,21 @@ const Analyzer = struct { return doc_comment_parser.parse_into_arena(&arena, raw_lines, .{ .ref_lookup = lookup_doc_comment_ref, .ref_lookup_context = @ptrCast(ana), - }); + }) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.UnclosedCodeFence, + error.UnclosedInlineReference, + error.UnclosedInlineCode, + error.UnclosedInlineLink, + error.MalformedInlineLink, + error.UnclosedAutolink, + => |parse_err| { + try ana.emit_error(Location.empty, "invalid doc comment markup: {s}", .{ + doc_comment_parser.describe_parse_error(parse_err), + }); + return .empty; + }, + }; } fn lookup_doc_comment_ref(context: ?*anyopaque, allocator: std.mem.Allocator, local_qn: []const u8) error{OutOfMemory}!?[]const u8 { @@ -1067,20 +1083,12 @@ const Analyzer = struct { return null; } - if (ana.resolve_scope_prefix(declared_scope, local_parts.items)) |resolved| { - const resolved_fqn = try ana.scope_to_fqn_string(allocator, resolved.scope); - if (resolved.matched_parts == local_parts.items.len) { - return @as(?[]const u8, resolved_fqn); - } - - var full: std.ArrayList(u8) = .empty; - defer full.deinit(allocator); - try full.appendSlice(allocator, resolved_fqn); - for (local_parts.items[resolved.matched_parts..]) |part| { - try full.append(allocator, '.'); - try full.appendSlice(allocator, part); - } - return @as(?[]const u8, try full.toOwnedSlice(allocator)); + if (try ana.resolve_prefixed_doc_reference( + allocator, + declared_scope, + local_parts.items, + )) |resolved| { + return resolved; } if (try ana.resolve_contained_doc_reference( @@ -1100,12 +1108,12 @@ const Analyzer = struct { return null; } - const ScopePrefixMatch = struct { - scope: *Scope, - matched_parts: usize, - }; - - fn resolve_scope_prefix(ana: *Analyzer, declared_scope: []const []const u8, local_parts: []const []const u8) ?ScopePrefixMatch { + fn resolve_prefixed_doc_reference( + ana: *Analyzer, + allocator: std.mem.Allocator, + declared_scope: []const []const u8, + local_parts: []const []const u8, + ) error{OutOfMemory}!?[]const u8 { var search_scope: ?*Scope = ana.resolve_declared_scope_or_parent(declared_scope); while (search_scope) |base_scope| : (search_scope = base_scope.parent) { @@ -1118,16 +1126,52 @@ const Analyzer = struct { } if (matched_parts > 0) { - return .{ - .scope = resolved_scope, - .matched_parts = matched_parts, - }; + if (try ana.build_doc_reference_from_scope( + allocator, + resolved_scope, + local_parts[matched_parts..], + )) |resolved| { + return resolved; + } } } return null; } + fn build_doc_reference_from_scope( + ana: *Analyzer, + allocator: std.mem.Allocator, + scope: *Scope, + remaining_parts: []const []const u8, + ) error{OutOfMemory}!?[]const u8 { + const scope_fqn = try ana.scope_to_fqn_string(allocator, scope); + errdefer allocator.free(scope_fqn); + + if (remaining_parts.len == 0) { + return scope_fqn; + } + + if (remaining_parts.len != 1) { + return null; + } + + const link = scope.link orelse return null; + if (!ana.link_contains_doc_reference_target(link, remaining_parts[0])) { + return null; + } + + var full: std.ArrayList(u8) = .empty; + defer full.deinit(allocator); + if (scope_fqn.len > 0) { + try full.appendSlice(allocator, scope_fqn); + try full.append(allocator, '.'); + } + try full.appendSlice(allocator, remaining_parts[0]); + allocator.free(scope_fqn); + return @as(?[]const u8, try full.toOwnedSlice(allocator)); + } + fn resolve_declared_scope_or_parent(ana: *Analyzer, declared_scope: []const []const u8) ?*Scope { var scope_len = declared_scope.len; while (true) { @@ -1639,9 +1683,7 @@ const Analyzer = struct { } if (mode == .syscall and outputs.fields.items.len > 1) { - return ana.fatal_error(info.location, - "syscall '{s}' has {d} 'out' parameters, but syscalls can have at most one", - .{ info.full_name[info.full_name.len - 1], outputs.fields.items.len }); + return ana.fatal_error(info.location, "syscall '{s}' has {d} 'out' parameters, but syscalls can have at most one", .{ info.full_name[info.full_name.len - 1], outputs.fields.items.len }); } const output: model.GenericCall = .{ @@ -2055,9 +2097,7 @@ const Analyzer = struct { return constant.value; } } - return ana.fatal_error(Location.empty, - "constant '{s}' must be defined before it is used here", - .{symbol_name}); + return ana.fatal_error(Location.empty, "constant '{s}' must be defined before it is used here", .{symbol_name}); }, .uint => |int| .{ .int = int }, .compound => |compound| { @@ -2430,3 +2470,75 @@ const DotJoin = struct { } } }; + +test "validate_constraints failures are surfaced through fail_if_errors" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var analyzer: Analyzer = .{ + .allocator = allocator, + .scope_stack = .empty, + .scope_map = .init(allocator), + .errors = .empty, + .root = .empty, + .structs = .init(allocator), + .unions = .init(allocator), + .enums = .init(allocator), + .bitstructs = .init(allocator), + .syscalls = .init(allocator), + .async_calls = .init(allocator), + .resources = .init(allocator), + .constants = .init(allocator), + .types = .init(allocator), + .uid_db = null, + }; + + const u8_type = try analyzer.types.append(.{ .well_known = .u8 }); + const ptr_type = try analyzer.types.append(.{ .ptr = .{ + .child = u8_type, + .is_const = true, + .alignment = null, + .size = .unknown, + } }); + + const logic_fields = try allocator.alloc(model.StructField, 1); + logic_fields[0] = .{ + .docs = .empty, + .name = "actual", + .type = u8_type, + .default = null, + .role = .default, + }; + + const native_fields = try allocator.alloc(model.StructField, 1); + native_fields[0] = .{ + .docs = .empty, + .name = "broken_ptr", + .type = ptr_type, + .default = null, + .role = .{ .slice_ptr = "missing" }, + }; + + _ = try analyzer.structs.append(.{ + .uid = @enumFromInt(1), + .docs = .empty, + .full_qualified_name = &.{"Broken"}, + .logic_fields = logic_fields, + .native_fields = native_fields, + }); + + try analyzer.validate_constraints(); + try std.testing.expectEqual(@as(usize, 1), analyzer.errors.items.len); + + var errors_out: std.ArrayList(AnalysisError) = .empty; + defer errors_out.deinit(allocator); + + try std.testing.expectError(error.AnalysisFailed, analyzer.fail_if_errors(&errors_out)); + try std.testing.expectEqual(@as(usize, 1), errors_out.items.len); + try std.testing.expect(std.mem.indexOf( + u8, + errors_out.items[0].message, + "native field 'broken_ptr' (slice_ptr) references unknown logic field 'missing'", + ) != null); +} diff --git a/src/tools/abi-mapper/src/syntax.zig b/src/tools/abi-mapper/src/syntax.zig index 1233c073..aafeff35 100644 --- a/src/tools/abi-mapper/src/syntax.zig +++ b/src/tools/abi-mapper/src/syntax.zig @@ -98,6 +98,20 @@ fn matchDecimalDigits(str: []const u8) ?usize { return i; } +fn parse_number_token(text: []const u8) u64 { + var stripped_buf: [128]u8 = undefined; + std.debug.assert(text.len <= stripped_buf.len); + + var stripped_len: usize = 0; + for (text) |c| { + if (c != '_') { + stripped_buf[stripped_len] = c; + stripped_len += 1; + } + } + return std.fmt.parseInt(u64, stripped_buf[0..stripped_len], 0) catch unreachable; +} + pub fn match_identifier(str: []const u8) ?usize { const first_char = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const all_chars = first_char ++ "0123456789."; @@ -479,15 +493,7 @@ pub const Parser = struct { const tok = try parser.accept(.number); - var stripped_buf: [80]u8 = undefined; - var stripped_len: usize = 0; - for (tok.text) |c| { - if (c != '_') { - stripped_buf[stripped_len] = c; - stripped_len += 1; - } - } - const num = std.fmt.parseInt(u64, stripped_buf[0..stripped_len], 0) catch unreachable; + const num = parse_number_token(tok.text); return .{ .uint = num, @@ -527,7 +533,7 @@ pub const Parser = struct { const num_tok = try parser.accept(.number); try parser.expect(.@")"); - break :blk std.fmt.parseInt(u64, num_tok.text, 0) catch unreachable; + break :blk parse_number_token(num_tok.text); } else null; const child = try parser.accept_type(); diff --git a/src/tools/abi-mapper/tests/digit_separator.zig b/src/tools/abi-mapper/tests/digit_separator.zig index f393fd14..7be161e4 100644 --- a/src/tools/abi-mapper/tests/digit_separator.zig +++ b/src/tools/abi-mapper/tests/digit_separator.zig @@ -64,3 +64,22 @@ test "binary digit separators" { try std.testing.expectEqual(@as(i65, 0b1111_0000), doc.enums[0].items[0].value); try std.testing.expectEqual(@as(i65, 0b0000_1111), doc.enums[0].items[1].value); } + +test "pointer alignment accepts digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\struct Buffer { + \\ field data: [*]align(1_6_4) u8; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.structs.len); + + const field = doc.structs[0].logic_fields[0]; + const field_type = doc.types[@intFromEnum(field.type)]; + try std.testing.expect(field_type == .ptr); + try std.testing.expectEqual(@as(?u64, 164), field_type.ptr.alignment); +} diff --git a/src/tools/abi-mapper/tests/doc_parser.zig b/src/tools/abi-mapper/tests/doc_parser.zig index 0c856c64..7ca8ab3b 100644 --- a/src/tools/abi-mapper/tests/doc_parser.zig +++ b/src/tools/abi-mapper/tests/doc_parser.zig @@ -104,6 +104,40 @@ test "inline code span" { try std.testing.expectEqualStrings(" now.", content[2].text.value); } +test "inline code span applies escapes" { + var parsed = try parse_doc(&.{" Call `\\`` now."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("`", content[1].code.value); +} + +test "inline code span keeps lone backslash" { + var parsed = try parse_doc(&.{" Path `\\` separator."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("\\", content[1].code.value); +} + +test "adjacent inline code spans do not swallow a backslash span" { + var parsed = try parse_doc(&.{" Keys `\\` `|`."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 5), content.len); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("\\", content[1].code.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" ", content[2].text.value); + try std.testing.expect(content[3] == .code); + try std.testing.expectEqualStrings("|", content[3].code.value); +} + test "cross-reference @`fqn`" { var parsed = try parse_doc(&.{" See @`foo.bar.Baz` for details."}); defer parsed.deinit(); @@ -202,6 +236,32 @@ test "autolink " { try std.testing.expectEqualStrings("mailto:foo@example.com", content[1].link.url); } +test "unclosed inline code span is rejected" { + try std.testing.expectError(error.UnclosedInlineCode, parse_doc(&.{" Broken `code"})); +} + +test "unclosed inline reference is rejected" { + try std.testing.expectError(error.UnclosedInlineReference, parse_doc(&.{" Broken @`Type.member"})); +} + +test "unclosed titled link is rejected" { + try std.testing.expectError(error.UnclosedInlineLink, parse_doc(&.{" Broken [docs](https://example.com"})); +} + +test "unclosed autolink is rejected" { + try std.testing.expectError(error.UnclosedAutolink, parse_doc(&.{" Broken 0); + try std.testing.expect(error_messages_contain(errors.items, "unknown doc reference '@`Type.not_a_member`'")); +} + fn find_syscall_by_fqn( syscalls: []const model.GenericCall, expected: []const u8, @@ -148,6 +180,15 @@ fn has_ref_fqn(docs: model.DocComment, expected: []const u8) bool { return false; } +fn error_messages_contain(errors: []const abi_parser.sema.AnalysisError, needle: []const u8) bool { + for (errors) |item| { + if (std.mem.indexOf(u8, item.message, needle) != null) { + return true; + } + } + return false; +} + fn inlines_have_ref_fqn(inlines: []const model.DocComment.Inline, expected: []const u8) bool { for (inlines) |inl| { switch (inl) { diff --git a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi index cf590911..36194d2b 100644 --- a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi +++ b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi @@ -78,7 +78,7 @@ namespace resources { /// NOTE: This immediately triggers tether chains and destroys /// all tethered resources as well. /// - /// NOTE: `destroy` always succeeds; destroying an invalid or already-destroyed handle is a no-op. + /// NOTE: @`destroy` always succeeds; destroying an invalid or already-destroyed handle is a no-op. syscall destroy { in @"resource": SystemResource; } @@ -95,7 +95,7 @@ namespace resources { /// The resource will be bound weakly to the process. item weak = 2; - /// This operation ensures that the binding is at least `weak`, + /// This operation ensures that the binding is at least @`weak`, /// but may never be downgraded from `strong`. /// /// This gives the ability to ensure a process can definitely access a resource @@ -1175,9 +1175,9 @@ namespace process { /// RANGE: 0 .. 12 in alignment_shift: u8; - /// A non-`null` pointer that points to exactly @ref size bytes. + /// A non-`null` pointer that points to exactly @`size` bytes. /// - /// NOTE: In practise, this might point to more than @ref size bytes, + /// NOTE: In practise, this might point to more than @`size` bytes, /// but the code must not assume *any* excess bytes may exist. out pointer: [*]u8; @@ -2174,6 +2174,11 @@ namespace input { field product_id: u16; } + struct DeviceMetadataLengths { + field name_len: usize; + field unique_id_len: usize; +} + /// Queries metadata about an input device. /// /// If `name_buf` is `null`, no name is written but `name_len` is still returned. @@ -2192,8 +2197,7 @@ namespace input { /// If not `null`, the kernel will fill this structure with metadata for the device. in descriptor: ?*DeviceDescriptor; - out name_len: usize; - out unique_id_len: usize; + out lengths: DeviceMetadataLengths; /// `id` is not valid anymore (e.g. device removed) or was never valid. error InvalidDevice; @@ -4352,24 +4356,24 @@ namespace network { error InvalidInterface; } - /// Completes when a link changes its state. - async_call WaitForLinkState { - /// The interface for which a link state shall be awaited. - in interface: InterfaceId; + //? /// Completes when a link changes its state. + //? async_call WaitForLinkState { + //? /// The interface for which a link state shall be awaited. + //? in interface: InterfaceId; - /// If not `null`, the operation completes when the link becomes `desired`. - /// Otherwise, the operation completes on the next interface state change. - in desired: ?LinkState; + //? /// If not `null`, the operation completes when the link becomes `desired`. + //? /// Otherwise, the operation completes on the next interface state change. + //? in desired: ?LinkState; - /// The new link state. - out current: LinkState; + //? /// The new link state. + //? out current: LinkState; - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; + //? /// `interface` is not a valid interface id (anymore). + //? error InvalidInterface; - /// The underlying `interface` was removed during this operation. - error Gone; - } + //? /// The underlying `interface` was removed during this operation. + //? error Gone; + //? } /// Sends an ICMP or ICMPv6 echo request. /// @@ -5750,25 +5754,25 @@ namespace network { error SubsystemDisabled; } - /// Completes when new lease was set, old lease was released or expired. - /// - /// NOTE: Multiple `WaitForUpdate` can be issued which will all complete at the - /// same time when the status changes. - async_call WaitForUpdate { - /// The interface for which a new DHCP state shall be awaited. - in interface: link.InterfaceId; + //? /// Completes when new lease was set, old lease was released or expired. + //? /// + //? /// NOTE: Multiple `WaitForUpdate` can be issued which will all complete at the + //? /// same time when the status changes. + //? async_call WaitForUpdate { + //? /// The interface for which a new DHCP state shall be awaited. + //? in interface: link.InterfaceId; - /// The new lease or `null` if the lease was removed. - out info: ?Lease; + //? /// The new lease or `null` if the lease was removed. + //? out info: ?Lease; - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; + //? /// `interface` is not a valid interface id (anymore). + //? error InvalidInterface; - /// The underlying `interface` was removed during this operation. - error Gone; + //? /// The underlying `interface` was removed during this operation. + //? error Gone; - error SystemResources; - } + //? error SystemResources; + //? } } namespace dhcp6 { @@ -7515,7 +7519,7 @@ namespace gui { field uuid: UUID; /// Number of bytes allocated in a Widget for this widget type. - /// See @ref gui.get_widget_data function for further information. + /// See @`gui.get_widget_data` function for further information. field data_size: usize; field flags: Flags; @@ -7606,7 +7610,7 @@ namespace gui { struct DesktopDescriptor { /// Number of bytes allocated in a Window for this desktop. - /// See @ref gui.get_desktop_data function for further information. + /// See @`gui.get_desktop_data` function for further information. field window_data_size: usize; /// A function pointer to the event handler of a desktop. @@ -7695,13 +7699,13 @@ namespace gui { /// Dummy struct to satisfy the parser. struct MouseEvent { - field event_type: Type; - }; + field event_type: input.InputEvent.Type; + } /// Dummy struct to satisfy the parser. struct KeyboardEvent { - field event_type: Type; - }; + field event_type: input.InputEvent.Type; + } union WidgetEvent { field event_type: Type; @@ -8696,80 +8700,80 @@ namespace io { error ResourceBusy; } - /// - /// Changes the configuration of a serial port and returns the new configuration. - /// - /// This function can also be used to query the current configuration by requesting no changes. - /// - async_call configure { - in port: SerialPort; + //? /// + //? /// Changes the configuration of a serial port and returns the new configuration. + //? /// + //? /// This function can also be used to query the current configuration by requesting no changes. + //? /// + //? async_call configure { + //? in port: SerialPort; - in baud_rate: ?u32; - in word_size: ?u8; - in stop_bits: ?StopBits; - in parity: ?Parity; - in control_flow: ?ControlFlow; + //? in baud_rate: ?u32; + //? in word_size: ?u8; + //? in stop_bits: ?StopBits; + //? in parity: ?Parity; + //? in control_flow: ?ControlFlow; - /// Selects which software control flow words control the transmitter - /// activity. - /// - /// NOTE: This is usually the same as `sw_control_flow_tx`. - in sw_control_flow_rx: ?SoftwareControlFlow; + //? /// Selects which software control flow words control the transmitter + //? /// activity. + //? /// + //? /// NOTE: This is usually the same as `sw_control_flow_tx`. + //? in sw_control_flow_rx: ?SoftwareControlFlow; - /// Selects which software control flow words are transmitted when - /// the own receive buffer is full. - /// - /// NOTE: This is usually the same as `sw_control_flow_rx`. - in sw_control_flow_tx: ?SoftwareControlFlow; + //? /// Selects which software control flow words are transmitted when + //? /// the own receive buffer is full. + //? /// + //? /// NOTE: This is usually the same as `sw_control_flow_rx`. + //? in sw_control_flow_tx: ?SoftwareControlFlow; - in acceptable_baud_error: f32; + //? in acceptable_baud_error: f32; - out current_baud_rate: u32; - out current_data_bits: u8; - out current_stop_bits: StopBits; - out current_parity: Parity; - out current_control_flow: ControlFlow; - out current_sw_control_flow_rx: SoftwareControlFlow; - out current_sw_control_flow_tx: SoftwareControlFlow; + //? out current_baud_rate: u32; + //? out current_data_bits: u8; + //? out current_stop_bits: StopBits; + //? out current_parity: Parity; + //? out current_control_flow: ControlFlow; + //? out current_sw_control_flow_rx: SoftwareControlFlow; + //? out current_sw_control_flow_tx: SoftwareControlFlow; - error InvalidHandle; + //? error InvalidHandle; - /// The actual baud rate diverges more than `acceptable_baud_error` from the requested baud rate. - error ImpreciseBaudRate; + //? /// The actual baud rate diverges more than `acceptable_baud_error` from the requested baud rate. + //? error ImpreciseBaudRate; - /// The requested word size is not supported by this serial port. - error UnsupportedDataBits; + //? /// The requested word size is not supported by this serial port. + //? error UnsupportedDataBits; - /// The requested number of stop bits is not supported by this serial port. - error UnsupportedStopBits; + //? /// The requested number of stop bits is not supported by this serial port. + //? error UnsupportedStopBits; - /// The requested parity is not supported by this serial port. - error UnsupportedParity; + //? /// The requested parity is not supported by this serial port. + //? error UnsupportedParity; - /// The requested control flow (or its configuration) is not supported by this serial port. - error UnsupportedControlFlow; - } + //? /// The requested control flow (or its configuration) is not supported by this serial port. + //? error UnsupportedControlFlow; + //? } - /// Changes the output control lanes of the serial port. - async_call control { - in port: SerialPort; + //? /// Changes the output control lanes of the serial port. + //? async_call control { + //? in port: SerialPort; - /// The new state that should be applied for DTR (Data Terminal Ready). - in dtr: ?bool; + //? /// The new state that should be applied for DTR (Data Terminal Ready). + //? in dtr: ?bool; - /// The new state that should be applied for RTS (Request To Send). - /// - /// NOTE: This is also called `RTR` when used for modern hardware control flow. - in rts: ?bool; + //? /// The new state that should be applied for RTS (Request To Send). + //? /// + //? /// NOTE: This is also called `RTR` when used for modern hardware control flow. + //? in rts: ?bool; - error InvalidHandle; + //? error InvalidHandle; - /// The serial port does not support changing the control flow mode. - error Unsupported; + //? /// The serial port does not support changing the control flow mode. + //? error Unsupported; - /// The control lanes cannot be changed as hardware control flow is active. - error ControlFlowActive; - } + //? /// The control lanes cannot be changed as hardware control flow is active. + //? error ControlFlowActive; + //? } /// Reads all control lanes of the serial port. async_call query_control { diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index 72ca5282..dcd96630 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -879,8 +879,8 @@ const DocFmt = struct { fn format_inlines(self: DocFmt, inlines: []const model.DocComment.Inline, writer: *std.Io.Writer) !void { for (inlines) |span| { switch (span) { - .text => |text| try writer.writeAll(text.value), - .code => |code| try writer.print("{s}", .{code.value}), + .text => |text| try writer.print("{f}", .{fmt_html(text.value)}), + .code => |code| try writer.print("{f}", .{fmt_html(code.value)}), .emphasis => |emphasis| { try writer.writeAll(""); try self.format_inlines(emphasis.content, writer); From 11e553bc475ae16e857bebb64ee77eb02ed6f34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 20:55:53 +0200 Subject: [PATCH 30/36] Codex: Improves CI script with improved caching behavior. --- .github/workflows/build.yml | 16 +++++++++-- .github/workflows/pages.yml | 8 +++++- .github/workflows/smoketest.yml | 8 +++++- scripts/zig_package_cache_key.py | 49 ++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 scripts/zig_package_cache_key.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23bc1682..8a508784 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,12 +34,18 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true @@ -73,12 +79,18 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index c12d08a8..ef5b293a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -38,12 +38,18 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index b107c427..6bb5fe74 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -44,12 +44,18 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true diff --git a/scripts/zig_package_cache_key.py b/scripts/zig_package_cache_key.py new file mode 100644 index 00000000..3ed2531b --- /dev/null +++ b/scripts/zig_package_cache_key.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import hashlib +import pathlib +import subprocess +import sys + + +digest = hashlib.sha256() +entries: set[str] = set() + +git_files = subprocess.run( + ["git", "ls-files", "-z"], + check=True, + capture_output=True, +).stdout.split(b"\0") + +manifest_paths = sorted( + pathlib.Path(raw.decode("utf-8")) + for raw in git_files + if raw and (raw == b"build.zig.zon" or raw.endswith(b"/build.zig.zon")) +) + +for path in manifest_paths: + print(path.as_posix(), file=sys.stderr) + +for path in manifest_paths: + for line in path.read_text().splitlines(): + stripped = line.strip() + if stripped.startswith("//"): + continue + + for key in (".url", ".hash"): + prefix = f"{key} =" + if not stripped.startswith(prefix): + continue + + _, value = stripped.split("=", 1) + value = value.strip() + + assert value.startswith('"'), f"{path}: malformed {key} entry: {line!r}" + assert value.endswith('",'), f"{path}: malformed {key} entry: {line!r}" + + entries.add(f"{key}={value}") + +for entry in sorted(entries): + digest.update(f"{entry}\n".encode("utf-8")) + +print(f"key={digest.hexdigest()}") From c01b8d85c4ac143087c054f1439008ac4fb1bb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 21:12:54 +0200 Subject: [PATCH 31/36] Fixes CI for tooling, and makes .abi files use LF line endings in git --- .gitattributes | 1 + .github/workflows/build.yml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.gitattributes b/.gitattributes index 0cb064ae..98aff87e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.zig text=auto eol=lf +*.abi text=auto eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a508784..779fc8b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -95,6 +95,12 @@ jobs: zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true + - name: Install Linux GUI dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev + - name: Build Tools run: | zig build --global-cache-dir "$ZIG_GLOBAL_CACHE_DIR" tools From ec431f8adc026db2f2b31eb62292cf3b4a3a30f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 21:20:25 +0200 Subject: [PATCH 32/36] removes ashet 1.0 abi file, as it was only for testing --- .../abi-mapper/tests/stress/ashet-1.0.abi | 9242 ----------------- .../tests/stress/ashet-1.0.abi.patch | 182 - 2 files changed, 9424 deletions(-) delete mode 100644 src/tools/abi-mapper/tests/stress/ashet-1.0.abi delete mode 100644 src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch diff --git a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi deleted file mode 100644 index 36194d2b..00000000 --- a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi +++ /dev/null @@ -1,9242 +0,0 @@ - -/// Enumeration of all syscall numbers. -typedef Syscall_ID = <>; - -/// The base type for all system resources. -enum SystemResource : usize -{ - ... - - typedef Type = <>; -} - -/// All syscalls related to generic resource management. -/// -/// - Resources are created through various calls in the kernel api, but their -/// lifetime and availability is managed through calls inside this namespace. -/// - After creation, a resource is strongly bound to the process that created the -/// resource. -/// - When a resource is destroyed, it becomes unusable from userland. -/// - As long as a resource is strongly bound to at least a single process, it is -/// not automatically destroyed. -/// - As soon as a resource has no strong bindings anymore, it is destroyed by the kernel. -/// - A process can only access the resources bound to the process. -/// - In addition to strong bindings, weak bindings also exist. -/// - Weak bindings allow a process access to a resource, but don't keep the resource alive. -/// - This allows processes to access resources they don't own. -/// - Resources can be tethered to other resources. -/// - If a tethered resource is destroyed, the associated resource is also destroyed. -/// -/// NOTE: Every kernel object the userland can interact with is a resource. -/// -/// NOTE: If a resource is destroyed by any means (zero strong bindings, manual destruction, tethering), -/// it destroys all resources tethered to it (i.e. where it is the `source`). -/// This may lead to a cascade called "tether chain". -/// -/// NOTE: Tethering can form cycles. This allows resources to be tightly bound together and if one -/// resource dies, the other one also dies. -/// -/// NOTE: The order in which tether destruction is executed is unspecified. Implementors must not assume -/// any order of destruction. -/// -/// NOTE: Tethering does not affect resource lifetimes. A tether will never keep -/// other resources alive. Tethers are removed if the `target` resource is destroyed -/// and all of its tether effects are resolved. -/// -/// LORE: Originally, Ashet OS had no concept of bindings, but only of ownership. -/// But this quickly lead to problems like "the desktop server also owns the window -/// so even if the application releases the window, it is not destroyed". -/// Cycles like this could only be resolved by an explicit call to `destroy` instead -/// of `release`. But this yields brittle code that expects correct application shutdown. -/// In cases of crashes, the resource would only be released, but not destroyed. -/// The idea of allowing a process to access a resource, but not keeping it alive solves -/// this problem completely and also allows some other patterns to work well. -/// -/// LORE: The idea of resource tethering came after the idea of bindings. -/// Tethering allows resolving a problem most other operating systems have, which is: -/// What happens if a thread dies unexpectedly. -/// In operating systems without tethering, the application has to monitor the threads -/// and if a thread dies, it has to manually clean up the resources of that thread -/// assuming it has properly registered the resources in a global management structure. -/// With tethering, we can tether the lifetime of a file handle to the lifetime of its -/// owning thread, meaning: If the thread dies, the file is closed. -/// As this is a very useful property, i've decided to implement it as a broader general -/// concept instead of tying it to threads only. -namespace resources { - /// Returns the type of the system resource. - syscall get_type { - in @"resource": SystemResource; - out type: SystemResource.Type; - error InvalidHandle; - } - - /// Immediately destroys the resource and releases its memory. - /// - /// NOTE: This will *always* destroy the resource, even if it's - /// still strongly bound by a process. - /// - /// NOTE: This immediately triggers tether chains and destroys - /// all tethered resources as well. - /// - /// NOTE: @`destroy` always succeeds; destroying an invalid or already-destroyed handle is a no-op. - syscall destroy { - in @"resource": SystemResource; - } - - /// Defines the possible kinds of bind operations we have. - enum BindOperation : u8 - { - /// The resource shall be unbound from the process. - item unbind = 0; - - /// The resource will be bound strongly to the process. - item strong = 1; - - /// The resource will be bound weakly to the process. - item weak = 2; - - /// This operation ensures that the binding is at least @`weak`, - /// but may never be downgraded from `strong`. - /// - /// This gives the ability to ensure a process can definitely access a resource - /// without force-downgrading it to a `weak` binding if the process already has - /// a `strong` binding. - item at_least_weak = 3; - } - - /// Binds a resource to a process. - /// - /// The success of this operation allows `target` to access `resource`, and optionally - /// gain/lose a strong binding of the resource. - /// - /// NOTE: This function can be used to up/downgrade the binding of a resource. - /// - /// NOTE: If `binding` is `unbind`, the resource is instead unbound from the process and - /// may be released. - /// - /// If all strong bindings of a resource are removed, the resource will be destroyed. - /// - /// NOTE: The following operations are idempotent: - /// - `binding=weak` when already bound weak. - /// - `binding=strong` when already bound strong. - /// - `binding=unbind` when not bound. - /// - `binding=at_least_weak` when already bound weak or strong. - syscall bind { - in @"resource": SystemResource; - - /// The process which the resource should be bound to. If `null`, uses the current process. - in target: ?process.Process; - - /// The type of binding operation that shall be performed. - in binding: BindOperation; - - /// The resource or process handle was invalid. - error InvalidHandle; - - /// The `target` process is dead, but still has an alive handle. - /// NOTE: Cannot happen when `binding` is `unbind`. - error ZombieProcess; - - /// The system ran out of resources when handling the request. - /// NOTE: Cannot happen when `binding` is `unbind`. - error SystemResources; - } - - - /// Defines the possible kinds of binding a resource can have. - enum Binding : u8 - { - /// The resource is not bound to the process. - /// This means the process cannot access the resource and also does not keep - /// the resource alive. - item unbound = 0; - - /// The resource is strongly bound to the process. - /// As long as a single strong binding exists, the resource is - /// valid. - item strong = 1; - - /// The resource is weakly bound to the process. - /// This means the process can access the resource, but does not - /// keep the resource alive. - item weak = 2; - } - - /// Returns the binding for a resource on a process. - syscall get_binding { - in @"resource": SystemResource; - - /// The process for which the resource binding shall be queried. If `null`, uses the current process. - in target: ?process.Process; - - /// The kind of binding the resource has on `target`. - out binding: Binding; - - /// The resource or process handle was invalid. - error InvalidHandle; - } - - /// Returns the current bindings of a resource. - /// - /// NOTE: The order in which processes and bindings are returned are not guaranteed to be - /// stable between two calls. - /// - /// NOTE: When `processes` is `null`, `bindings` can be used to count strong vs weak bindings. - /// - /// NOTE: When `resource` is the currently executing process, that process is always returned - /// in `processes`. - syscall get_bindings { - /// The resource which should be queried. - in @"resource": SystemResource; - - /// If not `null`, will receive the process handles that have a binding - /// on `resource`. - /// - /// NOTE: The process handles will be bound to the calling process with `BindOperation.at_least_weak` - /// to ensure resource access. - in processes: ?[]process.Process; - - /// If not `null`, will receive the binding types of all bindings on `resource`. - /// If `processes` is also provided, `bindings[i]` corresponds to `processes[i]`. - /// Otherwise, the order is unspecified. - /// - /// NOTE: No value written in this will ever be `unbound`. - in bindings: ?[]Binding; - - /// The number of bindings for the resource if `processes` and `bindings` is `null`. - /// Otherwise the number of elements written to `processes` and/or `bindings`. - /// - /// NOTE: If `processes` and `bindings` have different lengths, `min(processes.len, bindings.len)` is chosen. - out count: usize; - - /// The resource or process handle was invalid. - error InvalidHandle; - - /// The system ran out of resources when handling the request. - error SystemResources; - } - - /// Defines the possible ways of how a resource is tethered to another resource. - enum TetherMode : u8 - { - /// The resources are not tethered. - item untethered = 0; - - /// If the source resource is destroyed, the target resource will be destroyed as well. - item strong = 1; - } - - /// Binds the lifetime of the 'target' resource to the lifetime of the 'source' resource. - /// - /// If `mode` is `strong` and the `source` resource is destroyed, the `target` resource - /// will implicitly be destroyed as well. - syscall tether { - /// The resource which destruction will trigger the tether event. - in source: SystemResource; - - /// The resource that will receive the tether event. - in target: SystemResource; - - /// The kind of tethering performed. - in mode: TetherMode; - - /// One of the resource handles was invalid. - error InvalidHandle; - - /// The system ran out of resources when handling the request. - error SystemResources; - } - - /// Queries the tethering between two resources. - syscall get_tether { - /// The resource which destruction will trigger the tether event. - in source: SystemResource; - - /// The resource that will receive the tether event. - in target: SystemResource; - - /// The kind of tethering performed between the two resources. - out mode: TetherMode; - - /// One of the resource handles was invalid. - error InvalidHandle; - } - - //? TODO: Potentially define get_tethers? - - /// An anchor is a system resource without any properties or functions - /// besides the basic properties of resources. - /// - /// Anchors can be used as groups for tether objects or as tokens passed - /// between processes. - resource Anchor { } - - /// Creates a new `Anchor` resource. - syscall create_anchor { - /// The newly created anchor. - out anchor: Anchor; - - /// The system ran out of resources when handling the request. - error SystemResources; - } -} - -/// This namespace is related to asynchronous running (sys)calls, which -/// is the heart of the operating system I/O. -/// -/// All system calls in Ashet OS are non-blocking except `process.thread.yield`, -/// `overlapped.await_any` and `overlapped.await_any_of`. -/// This means that all regular system calls will return as soon as possible -/// without ever waiting on external events or other operations. -/// -/// But as each operating system requires slow/blocking operations, Ashet OS -/// provides a single way of handling long-running operations: -/// -/// The Asynchronously Running system Call (ARC). -/// -/// Each ARC represents an operation that might not complete immediately, and must -/// be scheduled to the kernel. -/// Later on, the ARC is returned from the kernel in an await operation or cancelled. -/// -/// NOTE: Overlapped operations that are cancelled before completion return `error.Cancelled`. -/// -/// NOTE: Completion may also be observed via `cancel` returning `error.Completed`. -/// -/// NOTE: This concept is typically called *completion queue*. -/// -/// NOTE: The `ARC` structure must always be embedded in the associated structure type -/// for `ARC.type`, as the kernel will cast the pointer to the `ARC` structure -/// to the associated structure type. -/// -/// NOTE: Between `schedule` and the await or cancel of the ARC, the userland -/// must keep the scheduled `ARC` object and the struct containing valid and unchanged. -/// -/// The kernel will modify the `output` and `error` fields of the ARC enclosing structure. -/// -/// NOTE: If the owning process is terminated, all scheduled ARCs of that process are implicitly -/// cancelled (best-effort) and removed from the kernel; they will not be returned to userland. -/// -/// NOTE: Completion delivery is exactly-once. -/// If `cancel` returns `Completed`, the operation will not be returned by `await_any` / `await_any_of`. -/// If an operation was returned by an await syscall, later `cancel` will return `Unscheduled`. -/// -/// NOTE: If an overlapped operation has a system resource as an input, and the system resource is destroyed -/// during the operation, the operation is cancelled. -namespace overlapped { - /// Handle to an asynchronously running (system) call. - /// - /// NOTE: This struct must always be embedded in the associated - /// structure for `ARC.type` at the offset 0. - /// - /// The kernel will derive the actual structure type from `ARC.type` - /// and will cast a pointer to an `ARC` into the actual call structure type. - struct ARC - { - /// The type of operation that is performed. - /// - /// NOTE: This field is never changed by the kernel. - field type: Type; - - /// A user-specified array of pointer-sized fields which may - /// contain userland information associated with the ARC. - /// - /// NOTE: This is primarily meant for event loop systems so - /// they can associate their own data structures with - /// ARCs returned from `await_any`. - /// - /// NOTE: This field is never changed by the kernel. - field tag: [3]usize; - - typedef Type = <>; - } - - /// Starts a new asynchronous operation. - /// - /// NOTE: Until the operation has successfully completed or was - /// cancelled, the ARC structure must be considered owned - /// by the kernel and must not be changed from userspace. - /// - /// The kernel may modify the enclosing structure's `output` and `error` fields - /// while scheduled. The kernel never modifies `ARC.type` and `ARC.tag`. - /// - /// NOTE: When scheduling an ARC, the kernel associates the calling - /// thread with the operation, so the awaiter can choose whether - /// to await only its own thread's ARCs or not. - syscall schedule { - /// The call that should be scheduled. - in @"arc": *ARC; - - /// Returned when the kernel already has an active async operation - /// with the same address. - error AlreadyScheduled; - - //? TODO: Consider "InProgress" or "Conflict" as a new error which - //? would be returned when an ARC cannot be scheduled due to conflicts - //? instead of going through the whole overlapped dance just to return - //? error.InProgress. - - error SystemResources; - } - - /// Cancels an asynchronous call. - /// - /// NOTE: Cancellation leaves the operation in an undefined state. This means - /// that cancellation isn't necessarily atomic and writes may have been - /// performed halfway, nearly completely or not at all. - /// - /// Example: A tiled renderer may have completed all draw commands for - /// half of the picture tiles when being cancelled. - /// In comparison, a linear renderer would have performed - /// half of the commands on the whole picture. - /// - /// NOTE: If the operation has already completed, an error will be returned. - /// - /// NOTE: A cancelled operation cannot be awaited anymore. - /// - /// NOTE: On success or the `Completed` error, the operation will not - /// be owned by the kernel anymore and cannot be awaited anymore by - /// `await_any` or `await_any_of`. - syscall cancel { - /// The operation to cancel. - in arc: *ARC; - - /// Returned when the ARC has already run to completion. - /// - /// NOTE: This means the operation was not cancelled because it already completed - /// with or without an error. Userland should handle this case gracefully. - /// - /// NOTE: If this error is returned, the operation will not be owned by the kernel anymore - /// and shall be treated as if it was returned from `await_any` or `await_any_of` as - /// completed. - error Completed; - - /// The kernel does not know the `arc` operation. - error Unscheduled; - } - - enum Thread_Affinity : u8 { - /// Waits for ARCs scheduled from *any* thread in the current process. - item all_threads = 0; - - /// Waits for ARCs scheduled from *this* thread. - item this_thread = 1; - } - - enum BlockMode : u8 { - /// Don't wait for any additional calls to complete, just return - /// whatever was completed in the meantime. - /// - /// NOTE: This mode does NOT suspend or yield the current thread - /// and keeps the active time slice. So spinning with this mode - /// without other yield points will lock up the system! - item dont_block = 0; - - /// Wait for at least a single call to complete operation. - item wait_one = 1; - - /// Wait until all scheduled operations have completed. - /// - /// This will only wait so long until either - /// a) all scheduled ops are stored into the result array - /// or - /// b) the result array is full - /// - /// NOTE: If `thread_affinity` is `.all_threads`, other threads can still - /// schedule more operations and make this function block longer. - item wait_all = 2; - } - - /// Awaits some scheduled asynchronous operations and returns the - /// number of `completed` elements. - /// - /// The kernel will fill `completed` up to the returned number of elements. - /// All other values are left untouched. - /// - /// NOTE: For blocking operations, this function will suspend the current - /// thread until the request has been completed. This means that other - /// threads can continue their work. - /// - /// NOTE: If `completed.len` is zero, the operation will never suspend and - /// immediately return zero for `completed_count`. - /// - /// NOTE: The completed ARCs are not owned by the kernel anymore and may be scheduled again. - syscall await_any { - /// A caller-provided array which will be filled with pointers to - /// the completed ARCs. - /// NOTE: Kernel will only touch the first `completed_count` elements and - /// keeps the rest unchanged. - in completed: []*ARC; - - /// Defines how the operation will suspend. - in block_mode: BlockMode; - - /// Defines the thread affinity for the await option. - /// This allows awaiting ARCs that were scheduled by the calling thread - /// and ignoring all others. - in thread_affinity: Thread_Affinity; - - /// The number of elements filled into `completed` by the kernel. - out completed_count: usize; - } - - /// Awaits one or more ARCs from a set and returns the number of completed operations. - /// - /// The kernel will only await elements provided in `events` and all of those events must - /// not be awaited by another `await_any_of`. - /// - /// When the call returns, the kernel will have partitioned events into two parts: - /// - The head (`events[0..completed_count]`) elements will be completed. - /// - The tail (`events[completed_count..]`) elements are still in progress. - /// - /// This allows the caller to invoke the syscall again later with the tail part of the - /// array in order to await the rest. - /// - /// NOTE: This syscall will always return as soon as a single event has finished. - /// - /// NOTE: It is invalid to await the same operation with two concurrent calls to `await_any_of`. - /// - /// NOTE: Elements awaited with this function will be guaranteed to not be returned by - /// a concurrent call to `await_any`. - /// - /// NOTE: This function will suspend the current thread and yield to other threads - /// if, and only if none of the events are completed. - /// - /// NOTE: The order in which the elements in `events` will be partitioned by the kernel - /// is implementation-defined and must not be assumed to have any meaningful order. - /// - /// NOTE: The completed ARCs are not owned by the kernel anymore and may be scheduled again. - syscall await_any_of { - /// A pre-filled list of events that shall be awaited. - /// On completion of the call, this list will be reordered by the kernel into - /// two halves, the first `completed_count` containing the completed events. - in events: []*ARC; - - /// The number of ARCs completed inside `events`. - out completed_count: usize; - - /// Another `await_any_of` already awaits an event from `events`. - error InvalidOperation; - - /// The kernel does not know an operation inside `events`. - /// When this error is returned, no ARCs will be removed from the completion queue - /// and `events` is left unmodified. - error Unscheduled; - } -} - -/// Syscalls related to processes, threads and execution control. -namespace process { - /// A process is first and foremost a context for resource resolution. - /// - /// In addition to that, each process has an additional initial thread - /// that is created and launched with `Spawn`. - /// - /// Resources in threads are always resolved in regard to their owning - /// process. - /// - /// A process has two states: - /// - Active: The process is loaded and not terminated yet. - /// - Zombie: The process resource still exists, but the process itself is already terminated. - /// - /// When a process is terminated or killed, all associated threads are exited, and all bound resources - /// are unbound. This may trigger the destruction of these resources. - /// - /// In addition to that, a termination reason is stored informing observers of the process - /// how the process was terminated. - /// - /// If a process terminates itself, it can provide a hint if it was successfully terminated ("clean exit") - /// or if it failed ("error during execution"). - /// - /// A process becomes Zombie on termination and remains until its `Process` resource is destroyed. - /// Destroying the Process resource reaps the zombie and frees remaining bookkeeping. - /// - /// NOTE: Destroying an active process resource will terminate the process and immediately reap the - /// zombie. - /// - /// NOTE: A process may be of two kinds: - /// - regular - /// - daemon - /// A daemon process may have zero associated foreground threads, but still be active. If it has - /// zero threads total, it can keep resources alive but cannot execute code until a new thread is - /// spawned into it. - /// - /// NOTE: A regular process is terminated when its last foreground thread is exited. - /// If all foreground threads have exited without calling `terminate`, the process - /// is terminated with `TerminationReason.regular_exit` and `success = true`. - /// - /// LORE: Ashet OS only has a single boolean for communicating success or failure to the outside. - /// This was chosen as most applications in the wild either use `EXIT_SUCCESS (0)` or `EXIT_FAILURE (1)` - /// in C libraries anyways and don't use the exit code to communicate meaning. - /// If a more complex communication to the outside is required, there are better options like pipes or shared memory - /// passed into the process. - resource Process { } - - /// A thread is the base unit of execution. - /// - /// Each thread operates concurrently to the other threads - /// in a cooperative manner: - /// - /// This means that threads voluntarily yield execution to the - /// OS scheduler in order to let other threads do their work. - /// - /// NOTE: A thread is always associated with a process and syscalls - /// invoked by a thread resolve resources always in regard to - /// that process. - /// - /// LORE: In contrast to other operating systems, threads do not expose - /// a success/failure state. - /// This was chosen as applications can use internal signalling - /// to better communicate failure/success than having a single integer - /// for that. - resource Thread { } - - /// Terminates the current process and marks it as "controlled exit". - syscall terminate { - /// Provides the information if the process terminated successfully or not. - in success: bool; - noreturn; - } - - /// Terminates a given process and marks it as "killed". - /// - /// NOTE: A killed process is never considered successful as it was terminated - /// from the outside. - /// - /// NOTE: If the current process is passed, this function will not return. - /// - /// NOTE: If the `target` process is already terminated, this operation is idempotent. - syscall kill { - /// The process that should be killed. - in target: Process; - - /// `target` is not a valid process resource. - error InvalidHandle; - } - - - /// An argument passed to a process. - struct SpawnArg { - /// The associated name of the argument. - field name: str; - - /// The type of the argument. - field type: Type; - - /// The associated value for `type`. - field value: Value; - - enum Type : u8 { - /// The argument is a mere flag and carries no value. - /// Its existence itself already carries semantics. - item flag = 0; - - /// The argument has a string value associated. - /// NOTE: `Value.text` is active. - item string = 1; - - /// The argument has a resource value associated. - /// NOTE: The spawned process receives a strong binding for the passed resource. - /// NOTE: `Value.resource` is active. - item @"resource" = 2; - } - - union Value { - field text: String; - field @"resource": SystemResource; - } - - struct String { - field text: str; - } - } - - /// Spawns a new regular process. - /// - /// NOTE: The kernel will perform a copy of all strings inside `overlapped.schedule`, so it is safe to - /// reuse the string memory after the operation is successfully scheduled. - /// - /// This prevents unwanted use-after-free by the kernel. - /// - /// NOTE: `Spawn` will create a single initial thread, the main thread. This thread is a - /// foreground thread which will use the executables entry point as its thread function. - async_call Spawn { - /// Relative base directory for `path`. - in dir: fs.Directory; - - /// File name of the executable relative to `dir`. - in path: str; - - /// The arguments passed to the process. - /// It is safe to release the resource binding to the current process as soon as this operation returns. - in arguments: []const SpawnArg; - - /// Handle to the spawned process. - out process: Process; - - error BadExecutable; - error IoError; - error FileNotFound; - error InvalidHandle; - error InvalidPath; - error SystemResources; - } - - /// Creates a new, empty daemon process. - /// - /// NOTE: The created process will be of `ProcessKind.daemon`. - /// - /// NOTE: The kernel will not create a main thread for the process. - syscall create_empty_process { - /// The arguments passed to the process. - /// It is safe to release the resource binding to the current process as soon as this syscall returns. - /// - /// NOTE: The kernel copies all argument strings and stores them in the process object before returning, - /// so the caller may reuse/free argument memory immediately after the syscall returns. - in arguments: []const SpawnArg; - - /// Size of the memory allocated for the process. - /// - /// NOTE: The memory allocated by the kernel will not have well-defined contents. - /// The kernel may zero the memory, or just assign it without change. - /// Userland must not assume contents of the memory without previously writing it. - /// - /// NOTE: This address for this memory will be returned by `get_base_address`. - /// - /// NOTE: The kernel may allocate more than the requested memory. Userland must assume - /// exactly `image_size` are valid and may not write beyond `image_size` - /// bytes after the address returned by `get_base_address`. - /// - /// NOTE: If `0` is passed, no memory will be allocated and `get_base_address` will return an error. - in image_size: usize; - - /// Handle to the created process. - out process: Process; - - /// A handle in `arguments` is not a valid resource handle. - error InvalidHandle; - - error SystemResources; - } - - enum TerminationReason : u8 { - /// The process terminated properly through a call to `terminate`. - item regular_exit = 0; - - /// The process was killed with `kill`. - item killed = 1; - - /// The process was shut down by the kernel in order to protect the system - /// from a crash. - /// This may include execution of invalid instructions, division by zero or - /// other platform illegal behaviour. - item faulted = 2; - } - - /// Completes when the given process terminates. - /// - /// NOTE: The call will immediately complete if `target` is already terminated. - /// - /// NOTE: Awaiting termination of the same process multiple times is idempotent. - /// - /// NOTE: Multiple `WaitForTermination` operations can be scheduled at once and - /// will all complete when the process terminates. - async_call WaitForTermination { - in target: Process; - - /// The reason why the process was terminated. - out reason: TerminationReason; - - /// Contains the success of the process termination. - /// This value is: - /// - The value passed to `terminate.success`. - /// - True if terminated by all foreground threads exiting. - /// - `false` otherwise. - out successful: bool; - - /// `target` is not a valid process resource. - error InvalidHandle; - } - - /// Returns the arguments that were passed to this process in `Spawn`. - syscall get_arguments { - /// The process for which the arguments shall be returned. - /// If `null` is passed, the current process will be used. - in target: ?Process; - - /// A constant slice of the process' arguments. - /// - /// NOTE: The returned memory and all interior pointers are valid as long - /// as the `target` process resource is not destroyed. - /// - /// NOTE: If an argument refers to a `SystemResource`, the resource will be bound - /// to the calling process with an `at_least_weak` bind operation to ensure - /// resource access. - out argv: []const SpawnArg; - - /// `target` is not a valid process resource. - error InvalidHandle; - - error SystemResources; - } - - /// Returns a pointer to the file name of the process. - syscall get_file_name { - /// The process for which the file name shall be returned. - /// If `null` is passed, the current process will be used. - in target: ?Process; - - /// The file name of the process passed to `Spawn` or the empty string if no - /// file name exists. - /// - /// NOTE: This is only the basename of the file and not the full path as - /// the information about the path is not helpful without the associated - /// directory handle. - /// - /// NOTE: The returned memory and all interior pointers are valid as long - /// as the `target` process resource is not destroyed. - out file_name: str; - - /// `target` is not a valid process resource. - error InvalidHandle; - } - - /// Returns the base address of the process. - /// - /// This is the address at which the executable image is loaded and relocated to. - /// - /// NOTE: The memory is valid until the process is terminated. - syscall get_base_address { - /// The process for which the base address shall be returned. - /// If `null` is passed, the current process will be used. - in target: ?Process; - - /// The base address of the process. - out base_address: usize; - - /// `target` is not a valid process resource. - error InvalidHandle; - - /// `target` process has no assigned memory region. - error NoMemory; - } - - /// Enumeration of the different process kinds that exist. - enum ProcessKind : u8 { - /// A regular process is automatically terminated when all foreground threads have exited. - item regular = 0; - - /// A daemon process does not automatically exit when all foreground threads have exited. - /// It stays alive until it is explicitly terminated. - item daemon = 1; - } - - /// Changes the kind of a process. - /// - /// NOTE: If a process is changed to `ProcessKind.regular` and has no active foreground - /// threads, the process is automatically terminated and this function may not return. - syscall set_kind { - /// The process for which the kind shall be updated. - /// If `null` is passed, the current process will be used. - in target: ?Process; - - /// The new kind of the `target` process. - in new_kind: ProcessKind; - - /// `target` is not a valid process resource. - error InvalidHandle; - - /// The `target` process is dead, but still has an alive handle. - error ZombieProcess; - } - - /// Queries the kind of a process. - syscall get_kind { - /// The process for which the kind shall be returned. - /// If `null` is passed, the current process will be used. - in target: ?Process; - - /// The kind of the `target` process. - out kind: ProcessKind; - - /// `target` is not a valid process resource. - error InvalidHandle; - } - - namespace thread { - /// Returns control to the scheduler. Returns when the scheduler - /// schedules the process again. - syscall yield { - } - - /// Gets the resource handle for the currently executing thread. - syscall get_current { - /// The handle to the current thread. - /// NOTE: This handle is ensured to be at least weakly bound to the current process. - out handle: Thread; - } - - /// Gets the process for a given thread. - syscall get_process { - /// The handle for which the process shall be queried. - /// If `null`, will yield the process for the current thread. - in handle: ?Thread; - - /// The handle to the process owning `handle`. - /// NOTE: This handle is ensured to be at least weakly bound to the current process. - out proc: Process; - - /// `handle` is not a valid thread resource. - error InvalidHandle; - - /// The system ran out of resources when handling the request. - /// - /// NOTE: This error can only when `handle` is not `null`. - error SystemResources; - } - - /// Terminates the current thread without returning from the thread function. - /// - /// NOTE: This does not perform any stack unwinding and no code will be executed - /// after a call to this function. - /// - /// NOTE: Exiting a thread stops the execution, but it does not destroy or release the - /// thread resource. - syscall exit { - noreturn; - } - - /// Defines the signature of a thread entry point. - /// The parameter is the `arg` value passed to `spawn`. - typedef ThreadFunction = fnptr (?anyptr) void; - - /// Enumeration of the available thread kinds. - enum ThreadKind : u8 { - /// A foreground thread keeps a regular process alive. - /// - /// As long as a single foreground thread exists, a process is not - /// automatically terminated. - item foreground = 0; - - /// A background thread does not keep a regular process alive. - /// - /// This means that all background threads are automatically exited - /// when the owning process is terminated. - item background = 1; - } - - /// Spawns a new thread with `function` passing `arg` to it. - /// - /// NOTE: A spawned thread will always be associated with the current - /// process. - syscall spawn { - /// The target process for which the thread shall be spawned. - /// If `null`, will use the current process. - /// - /// NOTE: Spawning a thread in a foreign process is a valid strategy for daemons - /// and IPC services, but the implementor has to keep in mind that - /// the memory for `function` and all of the code it invokes has to outlive - /// the threads lifetime. - /// This can be ensured by tethering the `target` thread to the owner of the - /// memory so it is automatically killed if the memory is returned to the OS. - in target: ?Process; - - /// The function that the thread will execute. - in function: ThreadFunction; - - /// The argument passed to `function`. - in arg: ?anyptr; - - /// The kernel will allocate at least this amount of bytes for the threads stack. - /// If zero is passed, the kernel will chose an implementation-defined amount of - /// stack for the thread. - /// - /// NOTE: There is no guarantee that the stack won't be larger than `stack_size` - /// bytes. - in stack_size: usize; - - /// The kind of thread that is created. - in kind: ThreadKind; - - /// The thread that was created. - /// NOTE: This thread handle will be bound to the calling process with the `at_least_weak` bind operation - /// to ensure access. - /// NOTE: The created thread will live logically inside the `target` process. - out thread: Thread; - - /// `target` is not a valid process resource. - error InvalidHandle; - - /// The `target` process is dead, but still has an alive handle. - error ZombieProcess; - - error SystemResources; - } - - /// Kills the given thread. - /// - /// This is equivalent to the `target` thread executing `exit`, but triggered - /// from the outside. - /// - /// NOTE: This does not perform any stack unwinding and no code will be executed - /// in the `target` thread after a call to this function. - /// - /// NOTE: Passing in the current thread as `target` will make this function behave - /// like `exit` and it won't return. - /// - /// NOTE: Killing a thread stops the execution, but it does not destroy or release the - /// thread resource. - /// - /// NOTE: Killing an already exited thread is idempotent. - syscall kill { - in target: Thread; - error InvalidHandle; - } - - /// Waits for the thread to exit. - /// - /// NOTE: The operation will complete immediately if `target` is already exited. - /// - /// NOTE: Awaiting the exit of the same thread multiple times is idempotent. - /// - /// NOTE: Multiple `WaitForExit` operations can be scheduled at once and - /// will all complete when the thread exits. - async_call WaitForExit { - in target: Thread; - - /// Informs how the thread exited: - /// - `true`: The thread exited by its thread function returning. - /// - `false`: The thread exited by invoking `exit` or `kill`. - out regular_exit: bool; - - error InvalidHandle; - } - - /// Suspends the execution of a thread. - /// - /// This means that a thread won't be scheduled for execution - /// until it is resumed. - /// - /// NOTE: If `target` is the current thread, this syscall - /// also yields implicitly and the syscall will - /// return when the thread is resumed. - syscall suspend { - /// The thread that shall be suspended. - /// If this value is `null`, the current thread will be suspended. - in target: ?Thread; - - /// Returned when `target` is not a valid thread resource. - error InvalidHandle; - - /// `target` is a thread that already exited. - error ThreadStopped; - } - - /// Resumes the execution of a thread. - /// - /// This means that the thread will be scheduled by the - /// operating system and continues execution. - /// - /// NOTE: Resuming an already active thread is idempotent and does nothing. - syscall resume { - /// The thread that should be resumed. - in target: Thread; - - /// Returned when `target` is not a valid thread resource. - error InvalidHandle; - - /// `target` is a thread that already exited. - error ThreadStopped; - } - - /// Changes the kind of a thread after creation. - /// - /// NOTE: If the last foreground thread of a regular process is changed - /// to `ThreadKind.background`, the process will be terminated and - /// this syscall may not return. - syscall set_kind { - /// The thread that shall be updated. - /// If this value is `null`, the current thread will be updated. - in target: ?Thread; - - /// The new kind this thread is. - in kind: ThreadKind; - - /// Returned when `target` is not a valid thread resource. - error InvalidHandle; - - /// `target` is a thread that already exited. - error ThreadStopped; - } - - /// Queries the thread kind. - syscall get_kind { - /// The thread that shall be queried. - /// If this value is `null`, the current thread will be queried. - in target: ?Thread; - - /// The kind of thread `target` is. - out kind: ThreadKind; - - /// Returned when `target` is not a valid thread resource. - error InvalidHandle; - - /// `target` is a thread that already exited. - error ThreadStopped; - } - } - - namespace debug { - enum LogLevel : u8 { - /// The log message is about a critical, terminating error. The process or thread usually - /// cannot continue after such a log message. - item critical = 0; - - /// The log message is about a non-critical error. This means the process is not terminating - /// due to the error, but it might still be relevant to the user. - item err = 1; - - /// The log message is not an error, but informs about things that might still be relevant - /// to understand higher level failures like exceeded retries or failed connections. - item warn = 2; - - /// The log message informs the user about regular operations. - /// Nothing critical shall be logged with this level. - item notice = 3; - - /// The log message is only useful for debugging the process. - item debug = 4; - } - - /// Writes to the system debug log. - syscall write_log { - /// The level of severity this log message has. - in log_level: LogLevel; - - /// The message that shall be printed. - /// NOTE: `message` must be terminated with a `LF` to append a new line. - /// Log messages will be concatenated without a joining symbol, - /// so without a `LF` character, all log messages would appear on the same line. - in message: str; - } - - /// Stops the process and allows debugging. - /// - /// When this syscall returns, the process will continue execution normally. - /// - /// NOTE: If the kernel has debugging disabled, this operation may be a no-op. - /// - /// LORE: This syscall is explicitly left under-defined as the debugging style - /// may change over time. At the time of writing (2026-02-06), this basically - /// just triggers a hardware breakpoint which will crash the kernel if no - /// hardware debugger is attached. - /// As this is designed as a low-level debug facility, it is fine for development - /// and semantics can later be improved. - /// The important part is that the syscall takes no arguments and returns neither - /// a value nor an error. - syscall breakpoint { - } - } - - /// - /// Each process has it's own memory heap managed by the kernel. This removes the - /// requirement that each process has to ship their own memory allocator and the kernel - /// may choose a system-optimal allocation strategy. - /// - /// LORE: Ashet OS is tailored for systems with tiny memory footprints. - /// The typical way of memory management in operating systems is that each process - /// has their own virtual memory space and the os can do "append only"-style memory - /// management. - /// - /// The problem with this design is that it requires a memory manangement unit that - /// supports virtual memory, and usually works with page granularity. This means - /// the smallest chunk of memory a process can allocate is a single page, usually 4096 bytes. - /// - /// For the systems we target, this is 0.5‰ of the total system memory assuming 8 MiB of RAM. - /// Thus, Ashet OS provides a finer grained kernel memory allocator which internally allows sharing - /// allocations in a much finer process memory assignments than chunks of 4096 bytes. - /// - namespace memory { - /// Allocates a chunk of memory from the process heap. - syscall allocate { - /// The size of the allocated memory block in bytes. - /// - /// NOTE: Passing 0 will never succeed. - /// - /// NOTE: The kernel ensures at least `size` bytes will be - /// usable in the returned `memory`. - in size: usize; - - /// The alignment of the pointer encoded as the number of - /// left-shifts on a one. - /// - /// This gives us a safer encoding as we only accept powers of two - /// anyways. - /// - /// RANGE: 0 .. 12 - in alignment_shift: u8; - - /// A non-`null` pointer that points to exactly @`size` bytes. - /// - /// NOTE: In practise, this might point to more than @`size` bytes, - /// but the code must not assume *any* excess bytes may exist. - out pointer: [*]u8; - - /// Is returned when the system is out of memory. - error SystemResources; - } - - /// Returns previously allocated memory back to the process heap. - /// - /// NOTE: If `pointer` is not exactly a pointer previously returned by `allocate.pointer`, - /// the behaviour is undefined and could corrupt the system. - /// - /// NOTE: If `pointer` is released multiple times, the behaviour is undefined - /// and could corrupt the system. - syscall release { - /// The exact pointer previously returned in `allocate.pointer`. - in pointer: [*]u8; - } - } - - namespace monitor { - /// Queries all existing process resources. - /// - /// NOTE: The order of processes is not necessarily stable between calls. - syscall enumerate_processes { - /// An array that, if not `null`, will receive the list of processes available. - /// - /// NOTE: The process handles will be bound to the calling process with the `at_least_weak` bind operation - /// to ensure access. - in processes: ?[]Process; - - /// The number of elements written inside `processes` or the total number of processes if `processes` is `null`. - /// - /// NOTE: If `processes` is not null, not more than `processes.len` is returned. - out count: usize; - - error SystemResources; - } - - /// Queries all bound resources by a process. - /// - /// NOTE: The order of resources is not necessarily stable between calls. - syscall query_bound_resources { - /// The process for which the resources should be queried. - in proc: Process; - - /// An array that, if not `null`, will receive the list of resources available. - /// - /// NOTE: The return resources will be bound to the calling process with the `at_least_weak` bind operation - /// to ensure access. - in reslist: ?[]SystemResource; - - /// The number of elements written to `reslist` or the total number of resources if `reslist` is `null`. - /// - /// NOTE: If `reslist` is not null, not more than `reslist.len` is returned. - out count: usize; - - /// `proc` is not a valid process resource. - error InvalidHandle; - - error SystemResources; - } - - /// Returns the total number of bytes the process takes up in RAM. - syscall query_total_memory_usage { - in proc: Process; - - /// The total number of bytes the process currently allocates in RAM. - out size: usize; - - /// `proc` is not a valid process resource. - error InvalidHandle; - } - - /// Returns the number of dynamically allocated bytes for this process. - syscall query_dynamic_memory_usage { - in proc: Process; - - /// The total number of heap bytes the process currently allocates in RAM. - out size: usize; - - /// `proc` is not a valid process resource. - error InvalidHandle; - } - - /// Returns the number of total memory objects this process has right now. - syscall query_active_allocation_count { - in proc: Process; - - /// The total number of heap allocations the process currently has active. - out count: usize; - - /// `proc` is not a valid process resource. - error InvalidHandle; - } - } -} - -/// This namespace contains functions and types related to a monotonic time base. -/// -/// NOTE: Functions inside this namespace are useful for measuring time or awaiting -/// timeouts. -namespace clock { - //? TODO: Consider making `Absolute` based on a 960 kHz timer instead of a - //? 1 MHz/GHz timer precision. This would allow having a global unique - //? "audio clock compatible" time base in the system, allowing perfect - //? syntonization and correlation of different OS events. - - /// Time in nanoseconds since system startup. - enum Absolute : u64 { - /// The very first time point measurable. Nothing can happen before this point. - item system_start = 0; - - /// The very last point of system runtime that can be expressed in Ashet OS. - /// - /// LORE: This is a bit over 580 years of system runtime, which - /// is sufficient to fully cover the lifetime of the electronics, - /// users and probably even countries and societies. - /// Not quite infinity, but close enough for the computer it's running on. - item infinity = 0xFFFF_FFFF_FFFF_FFFF; - - ... - } - - /// A duration in nanoseconds. - enum Duration : u64 { - ... - } - - /// Returns the time in nanoseconds since system startup. - /// - /// NOTE: This clock never goes backwards (non-decreasing). - /// - /// NOTE: The returned value is expressed in nanoseconds, but the underlying hardware - /// may have a coarser resolution. In that case the value advances in steps. - syscall monotonic { - out time: Absolute; - } - - /// Completes when `clock.monotonic() >= deadline`. - /// - /// NOTE: The timer completes immediately if `deadline` is already reached. - async_call Timer { - /// Monotonic timestamp in nanoseconds at which the operation completes. - in deadline: Absolute; - } -} - -/// This namespace contains functions and types related to wall clock and calendar operations. -/// -/// NOTE: Leap seconds are implemented by stretching the last millisecond of a day -/// by an additional second, so the millisecond 23:59:59.999 is 1001 ms long. -/// The encoded `DateTime` value does not gain an extra representable millisecond; -/// the duration of the final millisecond is extended by the kernel when applicable. -/// -/// NOTE: Ashet OS uses [Coordinated Universal Time (UTC)](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), not [International Atomic Time (TAI)](https://en.wikipedia.org/wiki/International_Atomic_Time). -/// -/// NOTE: The local time offset is always in minutes relative to the UTC time. -/// This means that it is computed by `local = utc + offset`. -/// As an example, consider `Europe/Berlin` (Winter), which is UTC+01:00: -/// - `offset = 60` -/// - `local = utc + 60` -/// - `utc = local - 60` -/// -/// NOTE: By default, the kernel uses UTC. Use `set_timezone_offset` or `load_timezone_data` to change that. -/// -/// -/// NOTE: The kernel maintains the local time zone in exactly one of two mutually exclusive modes: -/// - Manual fixed offset (configured via `set_timezone_offset`) -/// - Rule-based offset from tzdata (configured via `load_timezone_data`) -/// The most recently invoked of these two syscalls selects the active mode. -/// -/// LORE: The decision to use minute offsets instead of seconds is that -/// there is no real reason to support this level of precision, -/// as all current real time zones are only having quarter-hour -/// steps. Minutes allow a higher precision than that, but are not -/// unnecessarily high. -/// -/// LORE: Leap seconds are explicitly not part of the kernel API to keep the API -/// simple and predictable. -/// Having to consider leap seconds in each and every API will break more programs -/// than the OS pretending they don't exist on the API boundary. -/// There are two typical implementations for this behaviour: -/// - Leap smearing: The seconds of a day with a leap second are ever so slightly slower/faster, -/// so at the end of the day, the switch from 23:59:59 to 00:00:00 will be "steadily" (with -/// just a tiny fraction of pace difference). -/// - Duplicate second: The time between the wall clock displaying 23:59:59 and 00:00:00 is -/// two seconds instead of one. This means that the last second is taking twice as long as -/// a standard second. -/// For Ashet OS, which uses millisecond precision, the decision is to just make the last -/// millisecond of the day (23:59:59.999) be 1001 ms long. -namespace datetime { - //? - //? Basic date/time management and query - //? - - /// Encodes a packed structure that encodes a calendar date + wall clock time - /// into a single integer. - /// - /// NOTE: A DateTime value is always a UTC value. - /// - /// NOTE: The DateTime value is only valid when the `milliseconds_of_day` field - /// is in the defined range between 0 and 86,399,999 (both inclusive). - /// - /// For any value outside that range the DateTime value is considered *invalid*. - /// - /// NOTE: DateTime values form a *discrete linear order*; the encoding is an *injective*, - /// *strictly monotone order-embedding* into `i64`, whose image is a *gapped (non-contiguous) subset*; - /// decoding is a *partial function* on `i64`. - /// - /// This means not all `i64` values are valid DateTime values, - /// but we can trivially compare them as `i64` as the values - /// compare naturally (earlier points in time are smaller ints). - bitstruct DateTime : i64 { - /// The number of milliseconds inside the encoded day. - /// - /// RANGE: 0 to 86,399,999 - /// - /// LORE: Millisecond precision was chosen as it's the smallest - /// discrete time step in SI prefix units that fits into a u32 - /// value. - field milliseconds_of_day: u32; - - /// The number of days since the epoch, which is the `2000-01-01`. - /// - /// RANGE: 2000-01-01 = 0 - /// - /// LORE: The epoch was chosen to be the first January of 2000 as for - /// a non-UNIX timestamp system it doesn't necessarily make sense - /// to use the same epoch. - /// Using 2000 as the base year is kinda fun though, as it's a leap - /// year. - field days_since_epoch: i32; - } - - /// Returns the current date/time value. - /// - /// NOTE: The value returned by `now` may not be steady nor continuous. - /// As the wall clock can be adjusted by `set`, the value returned - /// by `now` can change abruptly, both in negative and positive - /// direction. - /// - /// NOTE: Precision of timing depends on the current hardware, - /// but should always be at least in seconds precision. - /// - /// NOTE: During a leap second adjustment, the final millisecond of a day - /// (23:59:59.999) may be extended, so repeated calls to `now` may - /// return the same `DateTime` value for longer than 1 ms. - syscall now { - /// The current date and time of the system. - out dt: DateTime; - } - - /// Updates the system's current date/time value. - /// - /// NOTE: Invoking `set` will make the value returned by `now` immediately jump - /// to the newly set value. - syscall set { - /// The new date and time of the system. - in dt: DateTime; - - /// Returned when `dt` does not encode a valid DateTime. - error InvalidValue; - } - - /// Completes when `datetime.now() >= when`. - /// - /// NOTE: A call to `set` may trigger all active alarm calls that - /// are now satisfied. - async_call Alarm { - /// Earliest possible date time of when the alarm triggers. - in when: DateTime; - - /// Returned when `when` does not encode a valid DateTime. - error InvalidValue; - } - - //? - //? Timezone Management - //? - - /// Gets the current offset between local time and UTC. - syscall get_timezone_offset - { - /// The offset of the local time to UTC in minutes. - /// - /// RANGE: -1440 - 1440 - /// - /// NOTE: In tzdata mode, this returns the offset that applies at `datetime.now()` - /// and may change over time as time zone rules change. - out minutes: i16; - } - - /// Sets the current offset between local time and UTC. - syscall set_timezone_offset - { - /// The offset of the local time to UTC in minutes. - /// - /// RANGE: -1440 - 1440 - in minutes: i16; - - /// The provided 'minutes' offset was not in the legal range. - error InvalidZoneOffset; - } - - /// Loads a "timezone data" file according to the [tz database](https://en.wikipedia.org/wiki/Tz_database). - /// - /// This allows setting the automatic update of the current local time zone offset according to - /// the rules encoded in the timezone data. - /// - /// LORE: The decision to use tzdata was pretty simple: It's a standardized format - /// that has proven over time and solves pretty much all of our issues already - /// in a good way. - syscall load_timezone_data - { - /// The binary TZif blob of the timezone data file. - /// - /// NOTE: [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536) specifies the - /// format accepted by this syscall. - /// - /// NOTE: TODO: Specify exact supported version and features of TZif. - in data: bytestr; - - /// The system is out of resources and cannot load the timezone data. - error SystemResources; - - /// The data is not a valid timezone data file. - error InvalidData; - } - - /// Queries the timezone offset for a given point in time. - /// - /// NOTE: This function returns either: - /// - the fixed manual offset set via `set_timezone_offset` (Manual mode), or - /// - the `dt`-dependent offset determined from tzdata (Tzdata mode). - syscall get_timezone_offset_at - { - /// The point in time to query the zone offset for. - in dt: DateTime; - - /// The local time offset in minutes. - out minutes: i16; - - /// Returned when `dt` does not encode a valid DateTime. - error InvalidValue; - } - - //? - //? Gregorian Calendar APIs - //? - - /// A structure encoding a date in the [Proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar), - /// which means it can encode dates prior to 1582. - /// - /// LORE: This structure isn't a necessity for the kernel API, but is introduced - /// as a way of saving code size and memory, as most western applications - /// will use the Gregorian calendar, so it makes sense to share the implementation - /// for a conversion from/to `DateTime` in the kernel. - struct GregorianDate { - /// The astronomical year of the date. - /// NOTE: This means that `year = 0` means 1 BCE. - /// RANGE: -32768 - 32767 - field year: i16; - - /// RANGE: 1-12 - field month: u8; - - /// RANGE: 1-31 - field day: u8; - - /// RANGE: 0-23 - field hour: u8; - - /// RANGE: 0-59 - field minute: u8; - - /// RANGE: 0-59 - /// NOTE: 60 is *not allowed*. See the note on `datetime` for more information. - field second: u8; - - /// RANGE: 0-999 - field millis: u16; - } - - /// Converts a gregorian date into a DateTime. - syscall from_gregorian { - /// The gregorian date that shall be converted into a date time. - in gregorian: GregorianDate; - - /// The offset to UTC in minutes for the date. - /// NOTE: Use 0 for UTC. - /// RANGE: -1440 - 1440 - in local_offset: i16; - - /// The resulting datetime value for `gregorian`. - out dt: DateTime; - - /// The gregorian date contains an invalidly specified date. - error InvalidValue; - - /// The local time zone offset is not in the legal range. - error InvalidZoneOffset; - } - - /// Converts a DateTime value into a gregorian date. - syscall to_gregorian { - /// The DateTime value that shall be converted into a gregorian date. - in dt: DateTime; - - /// The offset to UTC in minutes for the date. - /// NOTE: Use 0 for UTC. - /// RANGE: -1440 - 1440 - in local_offset: i16; - - /// The resulting gregorian date. - out gregorian: GregorianDate; - - /// The date/time value contains an invalid value. - error InvalidValue; - - /// The local time zone offset is not in the legal range. - error InvalidZoneOffset; - - /// `dt` points to a year not representable by `GregorianDate`. - error OutOfRange; - } - - /// Converts a date/time value into the current local gregorian date. - /// - /// NOTE: This function utilizes the current time zone information and - /// may have a dynamic offset to UTC. - syscall to_gregorian_local { - /// The DateTime value that shall be converted into a gregorian date. - in dt: DateTime; - - /// The local date. - out gregorian: GregorianDate; - - /// The date/time value contains an invalid value. - error InvalidValue; - - /// `dt` points to a year not representable by `GregorianDate`. - error OutOfRange; - } - - /// Converts a local gregorian date/time into a generic date/time value. - /// - /// NOTE: This function utilizes the current time zone information and - /// may have a dynamic offset to UTC. - /// - /// NOTE: If the local time is non-existent, `occurrence` is ignored. - /// If the local time is ambiguous, `adjustment` is ignored. - syscall from_gregorian_local { - /// The gregorian date in the local time zone. - in gregorian: GregorianDate; - - /// How to handle a well-formed local wall clock time that cannot be mapped - /// to a UTC timestamp because it does not exist in the current time zone rules. - /// - /// NOTE: This may happen due to daylight saving time or similar rules. - in adjustment: MissingTimeAdjustment; - - /// How to resolve ambiguities when a wall clock time appears multiple times. - /// - /// NOTE: This may happen due to daylight saving time or similar rules. - in occurrence: DuplicateTimeOccurrence; - - /// The date/time value representing the given date. - out dt: DateTime; - - /// The gregorian date contains an invalidly specified date. - error InvalidValue; - - /// The gregorian time maps a non-existing wall clock time and - /// `adjustment` was `reject`. - error NonexistentLocalTime; - - /// The gregorian time maps an ambiguous wall clock time and - /// `occurrence` was `reject`. - error AmbiguousLocalTime; - } - - /// Enumeration of the variants how missing wall clock times will be resolved. - enum MissingTimeAdjustment : u8 { - /// The time is not adjusted, but rejected and yields an error. - item reject = 0; - - /// The time is adjusted to the first possible past point in time. - /// EXAMPLE: `02:30:00.000` is mapped to `01:59:59.999`. - item past = 1; - - /// The time is adjusted to the first possible future point in time. - /// EXAMPLE: `02:30:00.000` is mapped to `03:00:00.000`. - item future = 2; - - /// The time is adjusted to the closest possible point in time. - /// EXAMPLE: `02:29:00.000` is mapped to `01:59:59.999`. - /// EXAMPLE: `02:30:00.000` is mapped to `03:00:00.000`. - /// EXAMPLE: `02:31:00.000` is mapped to `03:00:00.000`. - item closer = 3; - } - - /// Enumeration of the variants how a wall clock time that can occur multiple times - /// is handled. - enum DuplicateTimeOccurrence : u8 { - /// The time is not adjusted, but rejected and yields an error. - item reject = 0; - - /// If the time is ambiguous, assume the earlier variant. - /// EXAMPLE: `02:30` is 2.5 hours past midnight. - item earlier = 1; - - /// If the time is ambiguous, assume the later variant. - /// EXAMPLE: `02:30` is 3.5 hours past midnight. - item later = 2; - } -} - -/// This namespace contains items related to presenting video data. -namespace video { - /// Index of the systems video outputs. - enum VideoOutputID : u8 { - /// The primary video output - item primary = 0; - ... - } - - /// Returns a list of all video outputs. - /// - /// If `ids` is `null`, the total number of available outputs is returned; - /// otherwise, up to `ids.len` elements are written into the provided array - /// and the number of written elements is returned. - syscall enumerate { - in ids: ?[]VideoOutputID; - out count: usize; - } - - /// The video output resource is an exclusive access token to a - /// video output. - /// - /// It allows updating the displayed pixel data and waiting for the - /// vertical blanking interval of the display data. - resource VideoOutput { } - - /// Acquire exclusive access to a video output. - syscall acquire { - in output_id: VideoOutputID; - - /// The resource created from `output_id`. - out output: VideoOutput; - - /// Exclusive access is already held for the video output identified by `output_id`. - error AlreadyExists; - - /// `output_id` is not a valid video output id. - error InvalidId; - - error SystemResources; - } - - /// Returns the resolution of `output` in pixels. - syscall get_resolution { - in output: VideoOutput; - - out resolution: Size; - - /// `output` is not a valid video output resource. - error InvalidHandle; - } - - /// Completes when the video output has fully scanned out an image and is now performing the v-blanking. - /// - /// This allows frame-synchronized presentation of video data. - /// - /// NOTE: All scheduled `WaitForVBlank` operations complete at the start of the next vertical blanking period. - /// - /// This means that a schedule during the current vertical blanking period does not immediately complete - /// the operation, but delays by nearly a full frame. - async_call WaitForVBlank { - in output: VideoOutput; - - /// `output` is not a valid video output resource. - error InvalidHandle; - } - - /// Specifies how `WritePixels` will upload the pixels. - enum PresentMode : u8 { - /// The pixel data is written immediately. - /// - /// NOTE: This mode will immediately upload the pixel data and - /// will not await a vertical blanking period. This means the - /// upload is likely to create visual glitches or tearing. - item immediate = 0; - - /// The kernel attempts a tearing free upload of the pixel data. - /// - /// This means the kernel attempts to align the upload with the - /// vertical blanking period. - /// - /// NOTE: This mode is best-effort, and does not guarantee the - /// video data is uploaded tearing-free. - item vblank = 1; - } - - /// Uploads pixels to a video output. - /// - /// NOTE: If `destination` would update a zero-sized area (`width` or `height` is zero), - /// the operation is a no-op and completes immediately. - /// - /// LORE: Originally, we had the ability to directly get a pointer - /// to the video outputs buffer. - /// As convenient as it is, it implicitly imposed the requirement - /// for the kernel to potentially allocate a pixel buffer if the - /// video output cannot actually provide the video memory inside - /// the systems main memory. - /// - /// This forced the kernel to periodically upload an allocated buffer - /// to external video devices, which is both inefficient and error prone. - /// - /// This syscall + `Buffer` sidestep this problem by making the access of a - /// memory-mapped video memory fallible without removing the ability for a generic - /// upload procedure. - async_call WritePixels { - /// The output which should receive the pixel data. - in output: VideoOutput; - - /// The portion of the video buffer that should be updated. - in destination: Rectangle; - - /// Pointer to the top-left pixel of `destination`. - /// - /// NOTE: The order inside this array is row-major. - /// This means that `pixels[1]` is the pixel at `(destination.x + 1, destination.y)` - /// and `pixels[stride]` is the pixel at `(destination.x, destination.y + 1)`. - /// - /// NOTE: Each scanline starts at `y * stride` elements apart and the buffer must contain - /// at least `destination.height` scanlines. - in pixels: []const Color; - - /// The length of a scanline in `pixels` in elements. - in stride: usize; - - /// Determines when to perform the pixel data write. - in mode: PresentMode; - - /// `output` is not a valid video output resource. - error InvalidHandle; - - /// Returned when `pixels` does not hold enough pixels to update `destination`. - /// - /// This means that `pixels.len` is less than `stride * max(0, destination.height - 1) + destination.width`. - /// - /// NOTE: This error is only returned if `destination.height > 0`. - error BufferSize; - - /// `stride` is less than `destination.width`. - error InvalidStride; - - /// `destination` is outside the actual video buffer resolution. - error InvalidRegion; - } - - /// A buffer mapping provides a memory-mapped view into a - /// front- or backbuffer of a video output. - /// - /// This allows uploading pixel data without the need for a `WritePixels` operation. - /// - /// NOTE: Not every `VideoOutput` supports a buffer mapping. - resource BufferMapping { } - - enum BufferKind : u8 { - /// A front buffer uses the same data as the scanout mechanism. - /// This means that any write to this buffer is *directly* visible - /// as soon as the video output scans out the written pixel locations. - /// - /// NOTE: This means that writes may produce tearing or other visual - /// glitches. - /// - /// NOTE: `Present` is not required to make the changes visible. - item front_buffer = 0; - - /// A back buffer is a second buffer that is not used for scanning out - /// pixel data. - /// - /// This means that writes to a back buffer will never appear on the - /// video output unless the buffer is swapped/copied to the front buffer. - /// - /// To perform this copy/swap, the `Present` operation shall be used. - /// - /// NOTE: It is possible, but not recommended to perform a manual copy - /// from a back buffer mapping to a front buffer mapping. - item back_buffer = 1; - } - - /// Creates a memory mapping for the front or the back buffer of a video output. - /// - /// NOTE: Not every video output supports memory mappings at all. Some video outputs - /// only support a single mode of memory mapping. - /// - /// The supported combinations are: - /// - No mapping support. - /// - Only front buffer. - /// - Only back buffer. - /// - Both front and back buffer. - /// - /// When a buffer type is not supported, `Unsupported` is returned. - /// - /// NOTE: There can be only a single mapping for the front and the back buffer. - /// This means for each video output, a maximum of two `BufferMapping` resources - /// can exist. - /// - /// NOTE: A buffer mapping is implicitly destroyed when its associated video output is - /// destroyed. This is necessary as the destruction of the video output resource - /// revokes access to the video device, and thus also revokes access through memory - /// mappings. - syscall create_buffer_mapping { - in output: VideoOutput; - - /// Which buffer should be mapped. - in requested_kind: BufferKind; - - out buffer: BufferMapping; - - /// `output` is not a valid video output resource. - error InvalidHandle; - - /// The requested buffer type is not supported by the `output` device. - error Unsupported; - - /// A buffer mapping for the `requested_kind` of the video output - /// already exists. - error AlreadyExists; - - error SystemResources; - } - - /// Applies the changes inside `buffer` and guarantees they - /// are visible afterwards. - /// - /// NOTE: For a front buffer, no data movement will happen, but - /// `mode` may still make `Present` await the next vertical blanking - /// period. - /// - /// NOTE: It is not specified if a `Present` for a back buffer is performing a - /// buffer swap operation or a buffer copy operation. - /// - /// NOTE: If `mode == PresentMode.immediate` and `buffer` is a front buffer, the - /// operation completes immediately. - async_call Present - { - /// The buffer mapping that shall be presented. - in buffer: BufferMapping; - - /// Determines when to perform the pixel data update. - in mode: PresentMode; - - /// `buffer` is not a valid buffer mapping resource. - error InvalidHandle; - } - - /// A descriptor of memory-accessible pixel buffer. - /// - /// It is laid out row-major and `base[0]` is the top-left pixel - /// of the mapped image. - struct VideoMemory { - /// Pointer to the first pixel of the first scanline. - /// - /// Each scanline is `.stride` elements separated from - /// each other and contains `width` valid elements. - /// - /// There are `height` total scanlines available. - field base: [*]align(4) Color; - - /// Length of a scanline in elements. - field stride: usize; - - /// Number of valid elements in a scanline - field width: u16; - - /// Number of valid scanlines. - field height: u16; - } - - /// Returns a pointer to linear video memory, row-major. - /// - /// NOTE: The pointer inside `memory` is only valid until the next `Present` operation - /// for any front or back buffer mapping for the associated video output or until - /// the buffer mapping is destroyed. - /// - /// This requires careful management and it is not recommended to share different - /// `BufferMapping` resources with other actors. - syscall get_video_memory { - in buffer: BufferMapping; - - /// The descriptor of the memory mapped video buffer. - out memory: VideoMemory; - - /// `buffer` is not a valid buffer mapping resource. - error InvalidHandle; - } -} - -//? TODO: Review this namespace. -namespace audio { -//? TODO: Write this namespace. -//? -//? Your primary interface for audio streams is the ability to enqueue PCM/MIDI/ChipWrites at certain sample positions relative to your stream start. -//? -//? Later PCM schedules stop previous PCMs at exactly that sample, so only a single PCM data stream is active per logical audio stream. -//? MIDI should be obvious. -//? ChipWrites implements native support for audio chips like a MOS 6581 or AY-3-8910 where you can sample-precisely schedule register -//? writes to your audio chips to create multi-channel-multi-tier audio creations. -} - -/// This namespace contains items related to entropy management. -namespace random { - /// Fills `data` with random bytes. - /// - /// This call never waits. Bytes are generated from the kernel DRBG. - /// If the DRBG cannot be (re)seeded due to insufficient newly collected entropy, - /// output is still produced (possibly without reseeding). - /// - /// NOTE: Do not use this for key generation unless the system guarantees - /// the DRBG has been seeded at least once (see `GetStrictRandom`). - syscall get_soft_random { - /// The buffer that should be filled with random bytes. - in data: bytebuf; - } - - /// Fills `data` with random bytes, but only after the kernel DRBG is seeded. - /// - /// This async call completes once the entropy pool reached the minimum seeding - /// threshold, then generates bytes from the kernel DRBG. - /// - /// NOTE: May take a substantial amount of time on systems with weak entropy sources. - async_call GetStrictRandom { - /// The buffer that should be filled with random bytes. - in data: bytebuf; - } - - //? TODO: add "add entropy" syscall -} - -/// -/// Input devices, input groups, and input event delivery. -/// -/// The kernel exposes input in two ways: -/// - **Input groups**: loss-tolerant, bounded, strictly ordered queues. -/// - **Device waits**: edge-triggered fanout waits that do not buffer and may be lossy. -/// -/// LORE: Input groups exist so userland can define *which* devices it wants to consume, -/// while still retaining a strong ordering across multiple devices inside that group. -/// The previous "global merged queue" model made it hard to correctly split input -/// between unrelated consumers. -/// -/// -/// Event fusing rules: -/// -/// LORE: To reduce event pressure and avoid jitter, the kernel may fuse continuous events inside -/// group queues without changing the final reconstructed state. -/// -/// Rules: -/// - Only continuous events may be fused (e.g. relative motion, absolute motion, wheel, analog axis). -/// - Discrete events are never fused (e.g. key press/release, button press/release, text input). -/// - Fusing never combines different devices and never combines different event types. -/// - Fusing occurs only inside group queues and does not add a fixed input latency. -/// - A fused event uses the timestamp of the last fused constituent. -/// -//? TODO: Expose/standardize the exact fusing window (e.g. ~40ms) if userland ever needs it. -namespace input { - //? TODO: Add system call to upload a new potential keyboard layout. - - /// - /// Opaque identifier for an input device. - /// - /// Device ids are allocated by the kernel when devices are added (system start or hotplug) - /// and are dropped when the device is removed/unplugged. - /// - /// The ids are not assigned in a stable manner, this means the same device will receive a - /// different device id if removed and readded later. Also kernel enumeration at system - /// start has no specified order and devices will not have a stable id. - /// - /// NOTE: A `DeviceId` obtained from `enumerate_devices` is guaranteed to be valid only - /// until the calling thread yields to the scheduler. - /// After a yield, any syscall using that `DeviceId` may fail with `InvalidDevice` when - /// the device was removed. - /// - /// NOTE: Devices are not system resources. They do not have ownership semantics. - /// - /// NOTE: Device ids are allocated by the kernel in a monotonic manner, so it takes around - /// 4 billion plug/unplug operations until a device id is reused again. - /// This will take a while. - /// - /// LORE: It doesn't make sense to handle devices as system resources as they can be - /// potentially removed at runtime by external means and holding such a resource - /// afterwards would make the resource invalid anyways. Destroying a device resource - /// would also have no semantic meaning, as the physical device would still be plugged - /// into the system. - /// - enum DeviceId : u32 { - /// Special value used when an event has no originating device. - /// - /// NOTE: This value is only used as `InputEvent.device` for group-injected events. - /// It is not a valid target for `GetDeviceEvent` or `emit_device_event`. - item synthetic = 0; - - ... - } - - /// Enumerates all currently available input devices. - /// - /// If `ids` is `null`, the total number of available devices is returned; - /// otherwise, up to `ids.len` elements are written into the provided array - /// and the number of written elements is returned. - /// - /// NOTE: Returned ids are only guaranteed to be valid until the calling thread yields. - syscall enumerate_devices { - in ids: ?[]DeviceId; - out count: usize; - } - - - /// Describes the broad class of an input device. - enum DeviceClass : u8 { - item unknown = 0; - item keyboard = 1; - item mouse = 2; - item gamepad = 3; - item joystick = 4; - item @"3d_mouse" = 5; - ... - } - - /// Describes the transport/protocol of an input device. - enum DeviceProtocol : u8 { - item unknown = 0; - item usb = 1; - item bluetooth = 2; - item serial = 3; - item bitbang = 4; - item network = 5; - ... - } - - /// Capability flags of an input device. - bitstruct DeviceCapabilities : u16 { - /// Device can emit HID-style key usage codes. - field keys: bool; - - /// Device provides relative pointer motion events. - field rel_pointer: bool; - - /// Device provides absolute pointer position events. - field abs_pointer: bool; - - reserve u13 = 0; - } - - /// A structure describing an input device. - struct DeviceDescriptor { - field class: DeviceClass; - field protocol: DeviceProtocol; - field capabilities: DeviceCapabilities; - - /// Number of relative analog axes. - /// - /// NOTE: This includes axes like accelerometer axes, which have zero - /// output at rest, and only emit data when changed. - /// Same rate of change = Same value. - field rel_axes_cnt: u16; - - /// Number of absolute analog axes. - /// - /// NOTE: The value for these axes will be normalized by the kernel. - /// - /// NOTE: This includes axes like joysticks which have a zero position, - /// only change their reported value in a reproducible manner. - /// Same position = Same value. - field abs_axes_cnt: u16; - - /// Number of non-keyboard digital buttons the device provides. - /// - /// NOTE: This includes buttons like A/B/X/Y or Start/Select. - field digital_button_count: u16; - - /// The vendor id of the device. - /// NOTE: This value shall be interpreted depending on `protocol`. - /// NOTE: `vendor_id` may be set to `0xFFFF` if not applicable. - field vendor_id: u16; - - /// The product id of the device. - /// NOTE: This value shall be interpreted depending on `protocol`. - /// NOTE: `product_id` may be set to `0xFFFF` if not applicable. - field product_id: u16; - } - - struct DeviceMetadataLengths { - field name_len: usize; - field unique_id_len: usize; -} - - /// Queries metadata about an input device. - /// - /// If `name_buf` is `null`, no name is written but `name_len` is still returned. - /// If `unique_id_buf` is `null`, no unique id is written but `unique_id_len` is still returned. - /// - /// NOTE: `unique_id` is an optional, implementation-defined identifier that can be used by - /// applications to recognize devices again across hotplug. - /// - /// It may be empty if the kernel cannot provide one. - /// - syscall query_device_metadata { - in id: DeviceId; - in name_buf: ?[]u8; - in unique_id_buf: ?[]u8; - - /// If not `null`, the kernel will fill this structure with metadata for the device. - in descriptor: ?*DeviceDescriptor; - - out lengths: DeviceMetadataLengths; - - /// `id` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; - } - - - /// Waits for the next event emitted by a specific device. - /// - /// This operation is **edge-triggered**: - /// - it does not buffer events, - /// - it may be lossy under high pressure, - /// - if multiple events occur between yields, intermediate events may be missed. - /// - /// NOTE: Any number of concurrent `GetDeviceEvent` operations may be scheduled for the - /// same device; they will all complete with the same next event. - /// A subsequent device event requires re-scheduling a new `GetDeviceEvent`. - /// - /// NOTE: If the device is removed for a pending `GetDeviceEvent` operation, it is - /// aborted with `error.Cancelled`. - async_call GetDeviceEvent { - in device: DeviceId; - out event: InputEvent; - - /// `device` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; - - /// `device` is `DeviceId.synthetic`. - error BadDevice; - } - - /// Emits a synthetic event *as if a real device had emitted it*. - /// - /// This updates the internal device state immediately and completes pending `GetDeviceEvent` - /// operations for that device. - /// - /// The kernel sets: - /// - `InputEvent.device = device` - /// - `InputEvent.flags.synthetic = true` - /// - `InputEvent.timestamp = clock.now()` (implementation-defined moment during the syscall) - /// - /// NOTE: This operation does not depend on any input groups existing. - /// If the device is present in groups, the event is enqueued into those group queues. - syscall emit_device_event { - in device: DeviceId; - in type: InputEvent.Type; - in payload: InputEvent.Payload; - - /// `device` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; - - /// `device` is `DeviceId.synthetic`. - error BadDevice; - } - - /// Queries the current state of a device for a batch of `queries`. - /// - /// The kernel fills `queries[i].value` for each entry. - /// - /// NOTE: Unsupported `which` values produce a sane default (0 / centered). - syscall query_device_state { - in device: DeviceId; - in queries: []StateQuery; - - /// `device` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; - - /// `queries[i].what` contains an unknown value. - error InvalidValue; - } - - /// A userland-owned input event queue which merges events from 0..n devices. - /// - /// The queue length is fixed at creation time. - /// - /// NOTE: An input group with zero devices may still receive events through - /// synthetic event injection. - /// - /// NOTE: If a device is removed, it is implicitly removed from all groups. - resource InputGroup { } - - /// Creates a new input group with a fixed event queue size. - /// - /// NOTE: The queue is bounded. If it becomes full, the kernel will drop the - /// oldest queued events so the newest events are kept intact. - /// - /// NOTE: Dropped events are reported through `GetEvent.dropped_since_last`. - syscall create_group { - /// Maximum number of events buffered by this group. - /// - /// NOTE: If `queue_size` is zero, an error will be returned. - in queue_size: usize; - - out group: InputGroup; - - /// `queue_size` is zero. - error InvalidValue; - - error SystemResources; - } - - /// Adds a device to an input group. - /// - /// NOTE: A device can participate in 0..n groups at the same time. - /// Each emitted device event is enqueued once into each group that contains the device. - syscall add_device { - in group: InputGroup; - in device: DeviceId; - - /// `group` is not a valid input group resource. - error InvalidHandle; - - /// `device` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; - } - - /// Removes a device from an input group. - /// - /// NOTE: Removing a device does not purge already queued events originating from that device. - /// Those events remain ordered relative to all other queued events. - syscall remove_device { - in group: InputGroup; - in device: DeviceId; - - /// `group` is not a valid input group resource. - error InvalidHandle; - - /// `device` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; - } - - - /// Enumerates the devices that are currently part of an input group. - /// - /// If `devices` is `null`, the total number of devices in the group is returned; - /// otherwise, up to `devices.len` elements are written into the provided array. - syscall enumerate_group_devices { - in group: InputGroup; - in devices: ?[]DeviceId; - out count: usize; - - /// `group` is not a valid input group resource. - error InvalidHandle; - } - - /// Enumerates the input groups that currently contain the given device. - /// - /// If `groups` is `null`, the total number of groups containing the device is returned; - /// otherwise, up to `groups.len` elements are written into the provided array. - /// - /// NOTE: This returns only groups that are visible to the calling process. - /// - /// NOTE: The group resources returned in `groups` will be bound to the calling process with - /// the `at_least_weak` bind operation to ensure access. - syscall enumerate_device_groups { - in device: DeviceId; - in groups: ?[]InputGroup; - out count: usize; - - /// `device` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; - - error SystemResources; - } - - /// Waits for the next queued event from an input group. - /// - /// The operation completes when: - /// - the group queue is non-empty, or - /// - a new event arrives for the group. - /// - /// NOTE: If events are already available, this operation completes immediately. - /// - /// NOTE: Only a single `GetEvent` operation may be scheduled per group at a time. - /// This enforces strict, non-duplicating consumption and preserves ordering. - /// - /// NOTE: The group maintains a drop counter which increments whenever the queue is full - /// and an event must be dropped. - /// Each completion returns the number of dropped events since the last successful - /// dequeue/completion and resets that counter to zero. - /// - /// LORE: The queue is "newest-wins": on overflow, oldest events are discarded so the most - /// recent user input remains available. - /// - /// LORE: The group state is updated only when an event *leaves* the queue: - /// - a regular head-pop (returned by `GetEvent`), or - /// - an overflow head-pop (dropped due to overflow). - /// Events that are merely queued do not affect group state. - /// - async_call GetEvent { - in group: InputGroup; - - out event: InputEvent; - - /// Number of events dropped since the last successful dequeue from this group. - out dropped_since_last: u32; - - /// `group` is not a valid input group handle. - error InvalidHandle; - - /// A `GetEvent` operation for `group` is already scheduled. - error NonExclusiveAccess; - } - - /// Pushes a synthetic event into a group. - /// - /// The injected event is appended to the back of the group queue (same ordering rule as - /// device-emitted events). The kernel sets: - /// - `InputEvent.device = DeviceId.synthetic` - /// - `InputEvent.flags.synthetic = true` - /// - `InputEvent.timestamp = clock.now()` - /// - /// NOTE: This operation is atomic: it either enqueues the event or returns an error. - /// - /// If `force` is `false`, the syscall fails with `Overflow` if enqueueing would drop - /// an event due to a full queue. - /// - /// If `force` is `true`, the syscall behaves like a hardware event with newest-wins overflow, - /// except it is marked synthetic. - syscall queue_event { - in group: InputGroup; - in type: InputEvent.Type; - in payload: InputEvent.Payload; - in force: bool; - - /// `group` is not a valid input group resource. - error InvalidHandle; - - /// Returned when `force == false` and enqueueing would drop an existing queued event. - error Overflow; - } - - /// Queries the current fused/accumulated state of a group for a batch of `queries`. - /// - /// NOTE: The fused state includes only devices that can meaningfully contribute to the queried item. - /// Non-applicable devices are ignored. - /// - /// NOTE: Group state is updated only when events leave the queue (returned or dropped), - /// so userland state reconstruction from the event stream is equivalent unless events are dropped. - syscall query_group_state { - in group: InputGroup; - in queries: []StateQuery; - - /// `group` is not a valid input group resource. - error InvalidHandle; - - /// `queries[i].what` contains an unknown value. - error InvalidValue; - } - - /// A single state query item. - /// - /// The kernel reads `what` and `which`, and writes `value`. - /// - /// NOTE: All values are returned as i16: - /// - Digital inputs: 0 (inactive) or 1 (active) for a device; for a group, the sum of all - /// pressed contributors (so >1 is possible). - /// - Absolute axes: normalized [-32767..32767] for a device; for a group, summed and clamped. - /// - struct StateQuery { - /// Defines what kind of input should be queried. - field what: Item; - - /// Defines which instance of `what` should be queried. - field which: u16; - - /// Output value filled by the kernel. - field value: i16; - - /// Selects which state component is queried. - enum Item : u16 { - /// `which` is a `KeyUsageCode`. - item keyboard_key = 0; - - /// `which` is a `MouseButton` value. - item mouse_button = 1; - - /// `which` is a per-device button index (matches `InputEvent.Button.button`). - item control_button = 2; - - /// `which` is a per-device absolute axis index (matches `InputEvent.AbsAxis.axis`). - item abs_axis = 3; - - /// Absolute pointer X (normalized i16). `which` must be zero. - item pointer_x = 4; - - /// Absolute pointer Y (normalized i16). `which` must be zero. - item pointer_y = 5; - } - } - - /// Flags attached to an input event. - bitstruct EventFlags : u16 { - /// Set for events that did not originate from a device driver. - field synthetic: bool; - - reserve u15 = 0; - } - - /// An input event as delivered to userland. - struct InputEvent { - /// The type of event that was emitted. - field type: Type; - - /// Timestamp from the moment the kernel receives the event in its input subsystem. - /// - /// NOTE: Multiple events may share the same timestamp due to timer resolution and internal handling. - field timestamp: clock.Absolute; - - /// The originating device id or `DeviceId.synthetic` if none. - field device: DeviceId; - - /// Event flags. - field flags: EventFlags; - - /// The event payload. - field payload: Payload; - - enum Type : u16 { - item key_press = 0; - item key_release = 1; - - item mouse_rel_motion = 2; - item mouse_abs_motion = 3; - item mouse_button_press = 4; - item mouse_button_release = 5; - item mouse_wheel = 6; - - item digital_button_press = 7; - item digital_button_release = 8; - - item rel_axis_motion = 9; - item abs_axis_motion = 10; - - //? TODO: touch_down, touch_up, touch_move - - ... - } - - union Payload { - field keyboard: Keyboard; - - field mouse_rel_motion: MouseRelMotion; - field mouse_abs_motion: MouseAbsMotion; - field mouse_button: MouseButton; - field mouse_wheel: MouseWheel; - - field digital_button: Button; - - field rel_axis: RelAxis; - field abs_axis: AbsAxis; - } - - /// Relative motion delta in device units (implementation-defined). - /// - /// NOTE: Consecutive relative motion events may be fused inside group queues. - struct MouseRelMotion { - /// Relative position delta in the horizontal axis. - /// Positive values move to the right. - field dx: i16; - - /// Relative position delta in the vertical axis. - /// Positive values move downwards. - field dy: i16; - } - - /// Absolute pointer position on each axis in normalized i16: - /// - -32767 == -1.0 - /// - 0 == 0.0 - /// - 32767 == +1.0 - /// - /// NOTE: Consecutive absolute motion events may be fused inside group queues. - struct MouseAbsMotion { - /// Absolute position in the horizontal axis. - /// `-32767` is the left edge, `32767` is the right edge. - field x: i16; - - /// Absolute position in the vertical axis. - /// `-32767` is the top edge, `32767` is the bottom edge. - field y: i16; - } - - struct MouseButton { - /// Which mouse button was pressed/released. - field button: input.MouseButton; - } - - /// Wheel delta (implementation-defined units). - /// - /// NOTE: Consecutive wheel events in the same direction may be fused inside group queues. - struct MouseWheel { - field dx: i16; - field dy: i16; - } - - struct Keyboard { - /// The raw usage code for the key. Meaning depends on the layout; - /// kinda represents the physical position on the keyboard. - field usage: KeyUsageCode; - - /// If set, the pressed key combination has a mapping in the current - /// keyboard layout that produces text input. - /// - /// NOTE: This doesn't necessarily contain printable codes, but can also contain - /// combining characters like `U+0301` (Combining Acute Accent). - /// - /// NOTE: This isn't a true *composed* text input and cannot be directly used in a - /// text field or such. This is primarily meant to be passed into an input - /// method editor. - /// - /// NOTE: The lifetime of this pointer can be assumed valid until a keyboard layout - /// change is performed. - /// - /// LORE: This field isn't a perfect solution, but it's good enough for what we're trying to - /// achieve: International text input. - /// The idea of using combining characters for dead keys allows the IME to actually compose - /// a sequence of `U+0301` (Combining Acute Accent), `U+0041` (Latin Capital Letter A) to be composed - /// into `U+00C1` (Latin Capital Letter A With Acute) instead of emitting two codepoints. - /// - /// This method is flexible enough to be future-proof and extensible. - /// - field text: ?str; - - /// The modifier keys currently active - field modifiers: KeyboardModifiers; - } - - /// A non-keyboard digital button event (e.g. gamepad button). - /// - /// NOTE: `button` is an implementation-defined per-device index. - struct Button { - /// Defines which digital button was pressed/released. - field button: u16; - } - - /// An absolute analog axis event (e.g. joystick axis). - /// - /// NOTE: `axis` is an implementation-defined per-device index. - /// NOTE: `value` uses normalized i16: - /// -32767 == -1.0, 0 == 0.0, 32767 == +1.0 - struct AbsAxis { - /// Defines which axis has changed. - field axis: u16; - field value: i16; - } - - /// A relative analog axis event (e.g. accelerometer axis). - /// - /// NOTE: `axis` is an implementation-defined per-device index. - /// - /// NOTE: Consecutive relative axis events in the same direction may be fused inside group queues. - struct RelAxis { - /// Defines which axis has changed. - field axis: u16; - field delta: i16; - } - } - - enum MouseButton : u8 { - item none = 0; - item left = 1; - item right = 2; - item middle = 3; - item nav_previous = 4; - item nav_next = 5; - } - - /// Keyboard modifier state accompanying key events. - bitstruct KeyboardModifiers : u16 { - field shift: bool; - field alt: bool; - field ctrl: bool; - field gui: bool; - field shift_left: bool; - field shift_right: bool; - field ctrl_left: bool; - field ctrl_right: bool; - field alt_graph: bool; - field gui_left: bool; - field gui_right: bool; - reserve u5 = 0; - } - - /// - /// This is an enumeration of all well-known HID Keyboard/Keypad Page (0x07) usage codes for - /// keys. - /// - /// NOTE: These codes do not necessarily correlate with what's printed on the key, but what's - /// printed on the same location of a typical US layout keyboard. - /// Use key usage codes for when you're interested in the *location* of a key, not the - /// its semantic meaning. - /// For example, the typical `WASD` input scheme would be `ZQSD` on an AZERTY keyboard, but - /// the locations would be the same. - /// - /// LORE: This mapping was chosen as it's the most widespread standard key list. These codes - /// are directly produced by both USB and Bluetooth keyboards and don't require any translation - /// in these cases. Also HID is a widespread standard. - /// - /// NOTE: The notes in this enumeration are taken verbatim from - /// [HID Usage Tables, Version 1.6, Keyboard/Keypad Page (0x07)](https://usb.org/sites/default/files/hut1_6.pdf). - /// - enum KeyUsageCode : u16 { - //? 01 Keyboard ErrorRollOver - //? 02 Keyboard POSTFail - //? 03 Keyboard ErrorUndefined - - /// Keyboard `a` and `A` - /// NOTE: Typically remapped for other languages in the host system. - item a = 0x04; - - /// Keyboard `b` and `B` - item b = 0x05; - - /// Keyboard `c` and `C` - /// NOTE: Typically remapped for other languages in the host system. - item c = 0x06; - - /// Keyboard `d` and `D` - item d = 0x07; - - /// Keyboard `e` and `E` - item e = 0x08; - - /// Keyboard `f` and `F` - item f = 0x09; - - /// Keyboard `g` and `G` - item g = 0x0A; - - /// Keyboard `h` and `H` - item h = 0x0B; - - /// Keyboard `i` and `I` - item i = 0x0C; - - /// Keyboard `j` and `J` - item j = 0x0D; - - /// Keyboard `k` and `K` - item k = 0x0E; - - /// Keyboard `l` and `L` - item l = 0x0F; - - /// Keyboard `m` and `M` - /// NOTE: Typically remapped for other languages in the host system. - item m = 0x10; - - /// Keyboard `n` and `N` - item n = 0x11; - - /// Keyboard `o` and `O` - /// NOTE: Typically remapped for other languages in the host system. - item o = 0x12; - - /// Keyboard `p` and `P` - /// NOTE: Typically remapped for other languages in the host system. - item p = 0x13; - - /// Keyboard `q` and `Q` - /// NOTE: Typically remapped for other languages in the host system. - item q = 0x14; - - /// Keyboard `r` and `R` - item r = 0x15; - - /// Keyboard `s` and `S` - item s = 0x16; - - /// Keyboard `t` and `T` - item t = 0x17; - - /// Keyboard `u` and `U` - item u = 0x18; - - /// Keyboard `v` and `V` - item v = 0x19; - - /// Keyboard `w` and `W` - /// NOTE: Typically remapped for other languages in the host system. - item w = 0x1A; - - /// Keyboard `x` and `X` - /// NOTE: Typically remapped for other languages in the host system. - item x = 0x1B; - - /// Keyboard `y` and `Y` - /// NOTE: Typically remapped for other languages in the host system. - item y = 0x1C; - - /// Keyboard `z` and `Z` - /// NOTE: Typically remapped for other languages in the host system. - item z = 0x1D; - - - /// Keyboard `1` and `!` - /// NOTE: Typically remapped for other languages in the host system. - item @"1" = 0x1E; - - /// Keyboard `2` and `@` - /// NOTE: Typically remapped for other languages in the host system. - item @"2" = 0x1F; - - /// Keyboard `3` and `#` - /// NOTE: Typically remapped for other languages in the host system. - item @"3" = 0x20; - - /// Keyboard `4` and `$` - /// NOTE: Typically remapped for other languages in the host system. - item @"4" = 0x21; - - /// Keyboard `5` and `%` - /// NOTE: Typically remapped for other languages in the host system. - item @"5" = 0x22; - - /// Keyboard `6` and `^` - /// NOTE: Typically remapped for other languages in the host system. - item @"6" = 0x23; - - /// Keyboard `7` and `&` - /// NOTE: Typically remapped for other languages in the host system. - item @"7" = 0x24; - - /// Keyboard `8` and `*` - /// NOTE: Typically remapped for other languages in the host system. - item @"8" = 0x25; - - /// Keyboard `9` and `(` - /// NOTE: Typically remapped for other languages in the host system. - item @"9" = 0x26; - - /// Keyboard `0` and `)` - /// NOTE: Typically remapped for other languages in the host system. - item @"0" = 0x27; - - /// Keyboard Return (ENTER) - item enter = 0x28; - - /// Keyboard ESCAPE - item escape = 0x29; - - /// Keyboard DELETE (Backspace) - /// NOTE: Backs up the cursor one position, deleting a character as it goes. - item backspace = 0x2A; - - /// Keyboard Tab - item tab = 0x2B; - - /// Keyboard Spacebar - item space = 0x2C; - - /// Keyboard `-` and `_` - item minus = 0x2D; - - /// Keyboard `=` and `+` - /// NOTE: Typically remapped for other languages in the host system. - item equals = 0x2E; - - /// Keyboard `[` and `{` - /// NOTE: Typically remapped for other languages in the host system. - item square_bracket_open = 0x2F; - - /// Keyboard `]` and `}` - /// NOTE: Typically remapped for other languages in the host system. - item square_bracket_close = 0x30; - - /// Keyboard `\\` and `|` - /// NOTE: Typically remapped for other languages in the host system. - item backslash = 0x31; - - /// Keyboard Non-US `#` and `~` - /// NOTE: Typical language mappings: - /// US: `\` `|` - /// Belg: `µ` `\`` `£` - /// French Canadian: `<` `}` `>` - /// Danish: `'` `*` - /// Dutch: `<` `>` - /// French: `*` `µ` - /// German: `#` `'` - /// Italian: `ù` `§` - /// LatinAmerica: `}` `\`` `]` - /// Norwegian: `,` `*` - /// Spain: `}` `Ç` - /// Swedish: `,` `*` - /// Swiss: `$`, `£` - /// UK: `#` `~` - item non_us_hash = 0x32; - - /// Keyboard `;` and `:` - /// NOTE: Typically remapped for other languages in the host system. - item semicolon = 0x33; - - /// Keyboard `'` and `"` - /// NOTE: Typically remapped for other languages in the host system. - item apostrophe = 0x34; - - /// Keyboard Grave Accent (`\``) and Tilde (`~`) - /// NOTE: Typically remapped for other languages in the host system. - item grave_accent = 0x35; - - /// Keyboard `,` and `<` - /// NOTE: Typically remapped for other languages in the host system. - item comma = 0x36; - - /// Keyboard `.` and `>` - /// NOTE: Typically remapped for other languages in the host system. - item period = 0x37; - - /// Keyboard `/` and `?` - /// NOTE: Typically remapped for other languages in the host system. - item slash = 0x38; - - /// Keyboard Caps Lock - /// NOTE: Implemented as a non-locking key; sent as member of an array. - item caps_lock = 0x39; - - /// Keyboard F1 - item f1 = 0x3A; - - /// Keyboard F2 - item f2 = 0x3B; - - /// Keyboard F3 - item f3 = 0x3C; - - /// Keyboard F4 - item f4 = 0x3D; - - /// Keyboard F5 - item f5 = 0x3E; - - /// Keyboard F6 - item f6 = 0x3F; - - /// Keyboard F7 - item f7 = 0x40; - - /// Keyboard F8 - item f8 = 0x41; - - /// Keyboard F9 - item f9 = 0x42; - - /// Keyboard F10 - item f10 = 0x43; - - /// Keyboard F11 - item f11 = 0x44; - - /// Keyboard F12 - item f12 = 0x45; - - /// Keyboard PrintScreen - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item print_screen = 0x46; - - /// Keyboard Scroll Lock - /// NOTE: Implemented as a non-locking key; sent as member of an array. - item scroll_lock = 0x47; - - /// Keyboard Pause - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item pause = 0x48; - - /// Keyboard Insert - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item insert = 0x49; - - /// Keyboard Home - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item home = 0x4A; - - /// Keyboard PageUp - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item page_up = 0x4B; - - /// Keyboard Delete Forward - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - /// NOTE: Deletes one character without changing position. - item delete = 0x4C; - - /// Keyboard End - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item end = 0x4D; - - /// Keyboard PageDown - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item page_down = 0x4E; - - /// Keyboard RightArrow - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item right_arrow = 0x4F; - - /// Keyboard LeftArrow - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item left_arrow = 0x50; - - /// Keyboard DownArrow - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item down_arrow = 0x51; - - /// Keyboard UpArrow - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item up_arrow = 0x52; - - /// Keypad Num Lock and Clear - /// NOTE: Implemented as a non-locking key; sent as member of an array. - item num_lock = 0x53; - - /// Keypad `/` - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item kp_divide = 0x54; - - /// Keypad `*` - item kp_multiply = 0x55; - - /// Keypad `-` - item kp_subtract = 0x56; - - /// Keypad `+` - item kp_add = 0x57; - - /// Keypad ENTER - item kp_enter = 0x58; - - /// Keypad `1` and End - item kp_1 = 0x59; - - /// Keypad `2` and Down Arrow - item kp_2 = 0x5A; - - /// Keypad `3` and PageDn - item kp_3 = 0x5B; - - /// Keypad `4` and Left Arrow - item kp_4 = 0x5C; - - /// Keypad `5` - item kp_5 = 0x5D; - - /// Keypad `6` and Right Arrow - item kp_6 = 0x5E; - - /// Keypad `7` and Home - item kp_7 = 0x5F; - - /// Keypad `8` and Up Arrow - item kp_8 = 0x60; - - /// Keypad `9` and PageUp - item kp_9 = 0x61; - - /// Keypad `0` and Insert - item kp_0 = 0x62; - - /// Keypad `.` and Delete - item kp_period = 0x63; - - /// Keyboard Non-US `\\` and `|` - /// NOTE: Typical language mappings: - /// Belg: `<` `\` `>` - /// French Canadian: `<` `°` `>` - /// Danish: `<` `\` `>` - /// Dutch: `]` `|` `[` - /// French: `<` `>` - /// German: `<` `|` `>` - /// Italian: `<` `>` - /// Latin America: `<` `>` - /// Norwegian: `<` `>` - /// Spain: `<` `>` - /// Swedish: `<` `|` `>` - /// Swiss: `<` `>` - /// UK: `\` `|` - /// Brazil: `\` `|` - /// NOTE: Typically near the Left-Shift key in AT-102 implementations. - item non_us_backslash = 0x64; - - /// Keyboard Application - /// NOTE: Windows key for Windows 95, and Compose. - item application = 0x65; - - /// Keyboard Power - item power = 0x66; - - /// Keypad `=` - item kp_equals = 0x67; - - /// Keyboard F13 - item f13 = 0x68; - - /// Keyboard F14 - item f14 = 0x69; - - /// Keyboard F15 - item f15 = 0x6A; - - /// Keyboard F16 - item f16 = 0x6B; - - /// Keyboard F17 - item f17 = 0x6C; - - /// Keyboard F18 - item f18 = 0x6D; - - /// Keyboard F19 - item f19 = 0x6E; - - /// Keyboard F20 - item f20 = 0x6F; - - /// Keyboard F21 - item f21 = 0x70; - - /// Keyboard F22 - item f22 = 0x71; - - /// Keyboard F23 - item f23 = 0x72; - - /// Keyboard F24 - item f24 = 0x73; - - /// Keyboard Execute - item execute = 0x74; - - /// Keyboard Help - item help = 0x75; - - /// Keyboard Menu - item menu = 0x76; - - /// Keyboard Select - item select = 0x77; - - /// Keyboard Stop - item stop = 0x78; - - /// Keyboard Again - item again = 0x79; - - /// Keyboard Undo - item undo = 0x7A; - - /// Keyboard Cut - item cut = 0x7B; - - /// Keyboard Copy - item copy = 0x7C; - - /// Keyboard Paste - item paste = 0x7D; - - /// Keyboard Find - item find = 0x7E; - - /// Keyboard Mute - item mute = 0x7F; - - /// Keyboard Volume Up - item volume_up = 0x80; - - /// Keyboard Volume Down - item volume_down = 0x81; - - /// Keyboard Locking Caps Lock - /// NOTE: Implemented as a locking key; sent as a toggle button. - /// Available for legacy support; however, most systems should use the non-locking version of this key - item locking_caps_lock = 0x82; - - /// Keyboard Locking Num Lock - /// NOTE: Implemented as a locking key; sent as a toggle button. - /// Available for legacy support; however, most systems should use the non-locking version of this key - item locking_num_lock = 0x83; - - /// Keyboard Locking Scroll Lock - /// NOTE: Implemented as a locking key; sent as a toggle button. - /// Available for legacy support; however, most systems should use the non-locking version of this key - item locking_scroll_lock = 0x84; - - /// Keypad Comma - /// NOTE: Keypad Comma is the appropriate usage for the Brazilian keypad period (`.`) key. - /// This represents the closest possible match, and system software should do the correct - /// mapping based on the current locale setting. - item kp_comma = 0x85; - - /// Keypad Equal Sign - /// NOTE: Used on AS/400 keyboards. - item kp_equals_as400 = 0x86; - - - /// Keyboard International1 - /// NOTE: Keyboard International1 should be identified via footnote as the appropriate usage for the Brazilian - /// forward-slash (`/`) and question-mark (`?`) key. - /// This usage should also be renamed to either "Keyboard Non-US `/` and `?`" or to "Keyboard International1" - /// now that it's become clear that it does not only apply to Kanji keyboards anymore. - item international1 = 0x87; - - /// Keyboard International2 - item international2 = 0x88; - - /// Keyboard International3 - item international3 = 0x89; - - /// Keyboard International4 - item international4 = 0x8A; - - /// Keyboard International5 - item international5 = 0x8B; - - /// Keyboard International6 - item international6 = 0x8C; - - /// Keyboard International7 - /// NOTE: Toggle Double-Byte/Single-Byte mode - item international7 = 0x8D; - - /// Keyboard International8 - /// NOTE: Undefined, available for other Front End Language Processors. - item international8 = 0x8E; - - /// Keyboard International9 - /// NOTE: Undefined, available for other Front End Language Processors. - item international9 = 0x8F; - - /// Keyboard LANG1 - /// NOTE: Hangul/English toggle key. This usage is used as an input method editor control key on a Korean language keyboard. - item lang1 = 0x90; - - /// Keyboard LANG2 - /// NOTE: Hanja conversion key. This usage is used as an input method editor control key on a Korean language keyboard. - item lang2 = 0x91; - - /// Keyboard LANG3 - /// NOTE: Defines the Katakana key for Japanese USB word-processing keyboards. - item lang3 = 0x92; - - /// Keyboard LANG4 - /// NOTE: Defines the Hiragana key for Japanese USB word-processing keyboards. - item lang4 = 0x93; - - /// Keyboard LANG5 - /// NOTE: Defines the Zenkaku/Hankaku key for Japanese USB word-processing keyboards. - item lang5 = 0x94; - - /// Keyboard LANG6 - /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. - item lang6 = 0x95; - - /// Keyboard LANG7 - /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. - item lang7 = 0x96; - - /// Keyboard LANG8 - /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. - item lang8 = 0x97; - - /// Keyboard LANG9 - /// NOTE: Reserved for language-specific functions, such as Front End Processors and Input Method Editors. - item lang9 = 0x98; - - - - /// Keyboard Alternate Erase - /// NOTE: Example, Erase-Eaze™ key. - item alternate_erase = 0x99; - - /// Keyboard SysReq/Attention - /// NOTE: Usage of keys is not modified by the state of the Control, Alt, Shift or Num Lock keys. - /// That is, a key does not send extra codes to compensate for the state of any Control, - /// Alt, Shift or Num Lock keys. - item term_sysreq_attention = 0x9A; - - /// Keyboard Cancel - item term_cancel = 0x9B; - - /// Keyboard Clear - item term_clear = 0x9C; - - /// Keyboard Prior - item term_prior = 0x9D; - - /// Keyboard Return - item term_return = 0x9E; - - /// Keyboard Separator - item term_separator = 0x9F; - - /// Keyboard Out - item term_out = 0xA0; - - /// Keyboard Oper - item term_oper = 0xA1; - - /// Keyboard Clear/Again - item term_clear_again = 0xA2; - - /// Keyboard CrSel/Props - item term_crsel_props = 0xA3; - - /// Keyboard ExSel - item term_exsel = 0xA4; - - /// Keypad `00` - item kp_double_0 = 0xB0; - - /// Keypad `000` - item kp_triple_0 = 0xB1; - - /// Thousands Separator - /// NOTE: The symbol displayed will depend on the current locale settings - /// of the operating system. For example, the US thousands separator would - /// be a comma, and the decimal separator would be a period. - item kp_thousands_sep = 0xB2; - - /// Decimal Separator - /// NOTE: The symbol displayed will depend on the current locale settings - /// of the operating system. For example, the US thousands separator would - /// be a comma, and the decimal separator would be a period. - item kp_decimal_sep = 0xB3; - - /// Currency Unit - /// NOTE: The symbol displayed will depend on the current locale settings of the operating system. - /// For example the US currency unit would be $ and the sub-unit would be ¢. - item kp_currency_unit = 0xB4; - - /// Currency Sub-unit - /// NOTE: The symbol displayed will depend on the current locale settings of the operating system. - /// For example the US currency unit would be $ and the sub-unit would be ¢. - item kp_currency_subunit = 0xB5; - - /// Keypad `(` - item kp_round_bracket_open = 0xB6; - - /// Keypad `)` - item kp_round_bracket_close = 0xB7; - - /// Keypad `{` - item kp_curly_bracket_open = 0xB8; - - /// Keypad `}` - item kp_curly_bracket_close = 0xB9; - - /// Keypad Tab - item kp_tab = 0xBA; - - /// Keypad Backspace - item kp_backspace = 0xBB; - - /// Keypad `A` - item kp_a = 0xBC; - - /// Keypad `B` - item kp_b = 0xBD; - - /// Keypad `C` - item kp_c = 0xBE; - - /// Keypad `D` - item kp_d = 0xBF; - - /// Keypad `E` - item kp_e = 0xC0; - - /// Keypad `F` - item kp_f = 0xC1; - - /// Keypad XOR - item kp_logic_xor = 0xC2; - - /// Keypad `∧` - item kp_logic_and = 0xC3; - - /// Keypad % - item kp_percent = 0xC4; - - /// Keypad `<` - item kp_less_than = 0xC5; - - /// Keypad `>` - item kp_greater_than = 0xC6; - - /// Keypad `&` - item kp_ampersand = 0xC7; - - /// Keypad `&&` - item kp_double_ampersand = 0xC8; - - /// Keypad `|` - item kp_pipe = 0xC9; - - /// Keypad `||` - item kp_double_pipe = 0xCA; - - /// Keypad `:` - item kp_colon = 0xCB; - - /// Keypad `#` - item kp_hash = 0xCC; - - /// Keypad Space - item kp_space = 0xCD; - - /// Keypad `@` - item kp_at = 0xCE; - - /// Keypad `!` - item kp_exclamation = 0xCF; - - /// Keypad Memory Store - item kp_memory_store = 0xD0; - - /// Keypad Memory Recall - item kp_memory_recall = 0xD1; - - /// Keypad Memory Clear - item kp_memory_clear = 0xD2; - - /// Keypad Memory Add - item kp_memory_add = 0xD3; - - /// Keypad Memory Subtract - item kp_memory_subtract = 0xD4; - - /// Keypad Memory Multiply - item kp_memory_multiply = 0xD5; - - /// Keypad Memory Divide - item kp_memory_divide = 0xD6; - - /// Keypad `+/-` - item kp_plus_minus = 0xD7; - - /// Keypad Clear - item kp_clear = 0xD8; - - /// Keypad Clear Entry - item kp_clear_entry = 0xD9; - - /// Keypad Binary - item kp_binary = 0xDA; - - /// Keypad Octal - item kp_octal = 0xDB; - - /// Keypad Decimal - item kp_decimal = 0xDC; - - /// Keypad Hexadecimal - item kp_hexadecimal = 0xDD; - - /// Keyboard Left Control - item left_control = 0xE0; - - /// Keyboard Left Shift - item left_shift = 0xE1; - - /// Keyboard Left Alt - item left_alt = 0xE2; - - /// Keyboard Left GUI - /// NOTE: Windows key for Windows 95, and Compose. - /// NOTE: Windowing environment key, examples are Microsoft® LEFT WIN key, Macintosh® LEFT APPLE key, Sun® LEFT META key. - item left_gui = 0xE3; - - /// Keyboard Right Control - item right_control = 0xE4; - - /// Keyboard Right Shift - item right_shift = 0xE5; - - /// Keyboard Right Alt - item right_alt = 0xE6; - - /// Keyboard Right GUI - /// NOTE: Windows key for Windows 95, and Compose. - /// NOTE: Windowing environment key, examples are Microsoft® RIGHT WIN key, Macintosh® RIGHT APPLE key, Sun® RIGHT META key. - item right_gui = 0xE7; - - ... - } -} - -namespace network { - /// An address of the IPv4 internet protocol. - struct IPv4 { - /// The four bytes of the IP address. - /// - /// NOTE: This is a u32 that is always encoded in network byte order. - field addr: [4]u8 ; //? TODO: align(4) - } - - /// An address of the IPv6 internet protocol. - struct IPv6 { - /// The 16 bytes of the IP address. - /// - /// NOTE: This is always encoded in network byte order. - field addr: [16]u8; //? TODO: align(4) - - /// The interface for which this IP address is valid. - /// - /// NOTE: For non-link-local addresses, scope must be `link.InterfaceId.any`. - field scope: link.InterfaceId; - } - - /// A polymorphic IP address that can be both IPv4 or IPv6. - struct IP { - /// Defines which field of `addr` is active. - /// - /// NOTE: Must be `Type.ipv4` or `Type.ipv6`. - field type: Type; - - /// Union of the possible address types. - field addr: AnyAddr; - - enum Type : u8 { - item ipv4 = 0; - item ipv6 = 1; - - /// Not a concrete type of IP type, but a sentinel different - /// kernel APIs use for defining that they don't scope a syscall or - /// a operation to a specific IP type. - item any = 255; - } - - union AnyAddr { - /// Active when `IP.type == Type.ipv4`. - field v4: IPv4; - - /// Active when `IP.type == Type.ipv6`. - field v6: IPv6; - } - } - - /// An endpoint defines a connection target for TCP and UDP connections. - /// It is a tuple formed of an IP address and a port. - struct EndPoint { - /// IP address of the connection endpoint. - field ip: IP; - - /// The port number of the connection endpoint. - /// - /// NOTE: Uses host byte order. - field port: u16; - } - - /// - /// TODO: Write about the general idea of network interfaces. - /// - /// TODO: Talk about kernel subsystems being enabled/disabled and what each subsystem - /// is supposed to to. - /// - /// Kernel Subsystems - /// ================= - /// - /// IPv4: The kernel's built-in IPv4 stack. - /// This subsystem implements a regular IPv4 stack. If disabled, this interface - /// won't allow IPv4 operation through kernel interfaces anymore. - /// - /// NOTE: Disabling the IPv4 stack for an interface will remove all IPv4 addresses - /// and routes for this interface. - /// IPv6: - /// This subsystem implements a regular IPv6 stack. If disabled, this interface - /// won't allow IPv6 operation through kernel interfaces anymore. - /// - /// NOTE: Disabling the IPv6 stack for an interface will remove all IPv6 addresses - /// and routes for this interface. - /// - /// DHCPv4: - /// TODO: Write subsystem docs - /// This subsystem automatically performs DHCP management for the interface. - /// If enabled, the kernel automatically requests and refreshes DHCP leases, - /// and manages the routes. - /// - /// NOTE: This subsystem is disabled by default. - /// - /// DHCPv6: - /// TODO: Write subsystem docs - /// - /// NOTE: This subsystem is disabled by default. - /// - /// SLAAC: - /// TODO: Write subsystem docs - /// - /// NOTE: This subsystem is enabled by default. - /// - /// - /// TODO: Write how routing works in Ashet OS. - /// - namespace link { - /// Unique identifier of a network interface. - /// - /// NOTE: Interface ids are allocated in a monotonically increasing - /// way, and will be stable until a network interface is removed - /// from the kernel. - /// - /// NOTE: Id allocation will never allocate any of the named values inside this - /// enumeration. - /// - /// NOTE: Interface ids received from any kernel syscall or overlapped operation - /// are ephemeral and are only guaranteed to be valid until the next yielding - /// syscall. - /// - /// NOTE: Except `loopback`, the enumeration order of interfaces is unspecified - /// and the ids cannot be assumed stable between reboots. - enum InterfaceId : u32 { - /// The loopback interface is a virtual interface - /// that makes all sent packets be received by the same - /// interface again. - /// - /// This way, sockets can be bound to a local interface - /// and communicate without affecting any external systems. - /// - /// NOTE: The loopback interface has the IP addresses `127.0.0.1/8` - /// and `::1/128` by default. - /// - /// The kernel also adds a default connected route for these - /// two addresses. - item loopback = 0; - - /// A sentinel value that can be used to annotate the absence of a - /// specific interface. - /// - /// NOTE: This is not a true interface that can be used to query - /// information, but is a required "workaround" for IPv6 - /// scopes to be able to encode that an IP address isn't - /// scoped to a specific interface. - /// - /// NOTE: Using this on all APIs that don't explicitly mention it yields - /// the error `InvalidInterface`. - item any = 0xFFFFFFFF; - - ... - } - - /// Enumeration of all supported network interface types. - enum InterfaceType : u8 { - /// The interface is supported by the kernel, but the type - /// of network interface does not fit any of the other categories. - item unknown = 0; - - /// The interface is a loopback interface. - item loopback = 1; - - /// The interface is an Ethernet (IEEE 802.3) interface. - item ethernet = 2; - - /// The interface is virtual and is controlled by software. - item virtual = 3; - - /// The interface is a WLAN (IEEE 802.11) interface. - item wifi = 4; - - /// The interface is based on IEEE 802.15.4 (e.g. 6LoWPAN/Thread/Zigbee-style links). - item ieee_802_15_4 = 5; - - /// The interface is Bluetooth-based (e.g. PAN/BNEP or IPv6-over-BLE/IPSP). - item bluetooth = 6; - - /// The interface is point-to-point (e.g. PPP/SLIP). Usually has no meaningful link-layer address. - item point_to_point = 7; - - /// The interface is InfiniBand-based (IPoIB). - item infiniband = 8; - - /// The interface is a Wireless Body Area Network (WBAN), typically IEEE 802.15.6. - /// - /// NOTE: The underlying PHY may be narrowband, UWB, or body-coupled (HBC), - /// depending on the device. - item wban = 9; - } - - /// The physical address of a network interface. - /// - /// NOTE: This isn't just a MAC address, but it can hold several - /// different types of address. - struct PhysicalAddress { - /// Length of the physical address in bytes. - field len: u8; - - /// The type of the physical address. Specifies how `bytes` are interpreted. - field type: Type; - - /// Describes how the address value was assigned/generated (if known). - /// - /// NOTE: This is intentionally generic. For example, Bluetooth LE privacy - /// addresses (static/resolvable/non-resolvable) can be mapped onto - /// the `random_*` variants here. - field assignment: Assignment; - - /// Reserved. Must be zero. - field _reserved0: u8 = 0; - - /// Contains the bytes of the physical address. - /// - /// NOTE: The first `len` bytes are valid. All bytes beyond the - /// first `len` bytes must be zero. - /// - /// NOTE: These bytes must be interpreted according to `type`. - field bytes: [20]u8; - - /// Describes how a physical address was assigned/generated. - /// - /// NOTE: This is determined by the kernel in a best-effort fashion, and - /// may be `unknown` even if the address format is known. - enum Assignment: u8 { - /// The kernel does not know how this address was assigned. - item unknown = 0; - - /// Universally administered / externally assigned identifier. - /// - /// NOTE: For EUI-48/EUI-64 this typically means IEEE-assigned (U/L bit = 0). - item universal = 1; - - /// Locally administered identifier (not globally assigned). - /// - /// NOTE: For EUI-48/EUI-64 this typically means U/L bit = 1. - item local = 2; - - /// Randomly generated but expected to remain stable for long periods - /// (until reconfigured/reset). - /// - /// NOTE: Bluetooth LE "Static Random Address" maps here. - item random_stable = 3; - - /// Randomly generated and expected to rotate over time for privacy. - /// - /// NOTE: Wi-Fi MAC randomization and Bluetooth LE "Non-Resolvable Private Address" map here. - item random_rotating = 4; - - /// Randomly generated and expected to rotate, but can be mapped back to a stable - /// identity by peers that possess a shared secret / resolver. - /// - /// NOTE: Bluetooth LE "Resolvable Private Address" maps here. - item random_rotating_resolvable = 5; - } - - /// Enumeration of possible types for physical link addresses. - enum Type: u8 { - /// Special marker that encodes `?PhysicalAddress == null`. - /// - /// NOTE: `absent` means the `PhysicalAddress` exists in a logical sense, but is empty. - /// `null` means the `PhysicalAddress` value itself is absent/missing. - /// - /// NOTE: All addresses of this type are empty (zero bytes long). - /// - /// NOTE: A `PhysicalAddress` `null` value must be fully zeroed out: - /// - `len = 0` - /// - `assignment = Assignment.unknown` - /// - `_reserved0 = 0` - /// - `bytes = {0} ** 20`. - item null = 0; - - /// This type marks an absent physical address. - /// - /// This typically means the associated interface does not support physical addresses at all. - /// - /// NOTE: All addresses of this type are empty (zero bytes long). - /// - /// NOTE: This is a special case to handle links that have no address, - /// but without introducing "out of band" communication for absent - /// physical addresses. - /// - /// NOTE: This is distinct from `null` in that a `PhysicalAddress` exists but is empty, - /// while `null` means the `PhysicalAddress` value does not exist. - /// - /// NOTE: For `absent`, the struct must satisfy: - /// - `len = 0` - /// - `assignment = Assignment.unknown` - /// - `_reserved0 = 0` - /// - `bytes = {0} ** 20`. - item absent = 1; - - /// A physical address is available, but the kernel cannot represent the type of address. - /// - /// NOTE: Any `len` may be valid for this kind of physical address. - item unknown = 2; - - /// An address from the EUI-48 namespace. This is typically known as a MAC address. - /// - /// NOTE: These addresses are typically used with Ethernet or WLAN interfaces. - /// - /// NOTE: `PhysicalAddress.len` must be `6`. - /// - /// NOTE: See RFC 9542 for more information on this address type. - item eui_48 = 3; - - /// An address from the EUI-64 namespace. This is typically known as a MAC address. - /// - /// NOTE: These addresses are typically used with 802.15.4 based protocols (e.g. ZigBee). - /// - /// NOTE: `PhysicalAddress.len` must be `8`. - /// - /// NOTE: See RFC 9542 for more information on this address type. - item eui_64 = 4; - - /// An IPoIB (IP over InfiniBand) link-layer address. - /// - /// NOTE: This is the 20-byte "link-layer address" used by IPoIB for IPv4/ARP - /// and for IPv6 Neighbor Discovery source/target link-layer address options. - /// - /// Layout (network byte order): - /// - byte 0: Reserved flags (must be zero on send; ignore on receive) - /// - bytes 1-3: Queue Pair Number (QPN, 24-bit) - /// - bytes 4-19: Port GID (16 bytes) - /// - /// NOTE: This address is not guaranteed to be stable across reboots or even - /// network interface resets because the QPN may change. - /// - /// NOTE: In IPv6 Neighbor Discovery, the on-wire option is padded to 24 bytes - /// total; the extra padding is not part of this 20-byte address. - /// - /// See RFC 4391, Section 9.1.1 and Section 9.3. - /// - /// NOTE: `PhysicalAddress.len` must be `20`. - item infiniband = 5; - - /// A local WBAN link-layer identifier. - /// - /// NOTE: `PhysicalAddress.len` must be `2`. - /// - /// Interpretation: - /// - byte 0: WBAN / BAN identifier (local scope) - /// - byte 1: Node identifier within that WBAN - /// - /// NOTE: This is not a standalone IEEE-defined 16-bit address format. - /// It is a packing of the IEEE 802.15.6 WBAN_ID + Node_ID fields. - /// - /// NOTE: This address is *not* globally unique. It is only meaningful within - /// the given interface's WBAN. - /// - /// NOTE: This is intended to cover IEEE 802.15.6 style WBAN links, including - /// Human Body Communication (HBC) PHY variants. - item wban_local = 6; - - /// IEEE 802.15.4 short address (16-bit). - /// - /// NOTE: `PhysicalAddress.len` must be `2`. - /// - /// NOTE: The value logically encodes a `u16` in big endian format. - item ieee_802_15_4_short = 7; - } - } - - /// A bit set of several subsystems that the kernel - /// can provide per interface. - bitstruct SubsystemSet : u32 { - field ipv4: bool; - field ipv6: bool; - field dhcp4: bool; - field dhcp6: bool; - field slaac: bool; - - reserve u27 = 0; - } - - struct InterfaceDescription { - /// The type of this network interface. - field type: InterfaceType; - - /// The physical address this interface has. - field address: PhysicalAddress; - - /// The display name of the network interface. - /// - /// NOTE: The lifetime of this string is bound to the lifetime - /// of the network interface. Assume it is only valid between - /// obtaining the interface description from the kernel and the - /// next thread yield. - field name: str; - - /// Name of the NIC vendor or an empty string if unknown. - /// - /// NOTE: The lifetime of this string is bound to the lifetime - /// of the network interface. Assume it is only valid between - /// obtaining the interface description from the kernel and the - /// next thread yield. - field vendor: str; - - /// The maximum bandwidth this interface can theoretically achieve - /// in bits per second. - /// - /// NOTE: For the loopback interface, this value is always zero. - /// - /// NOTE: For virtual interfaces, the driver shall set this value - /// to either a real value or zero, if no upper bound is known. - field max_bandwidth: u64; - - /// The current bandwidth this interface has negotiated with the - /// connected network in bits per second. - /// - /// NOTE: If zero, the value cannot be determined for one of several reasons: - /// - The link is down. - /// - The driver has no way to query the current bandwidth. - /// - There is no physically realistic value (e.g. for virtual or loopback interfaces). - field current_bandwidth: u64; - - /// The set of currently enabled kernel subsystems. - field enabled_subsystems: SubsystemSet; - } - - /// Enumerates the currently available network interfaces. - syscall enumerate_interfaces { - /// Buffer that shall receive the list of interfaces or `null` if the - /// total amount of interfaces should be queried. - in list: ?[]InterfaceId; - - /// If `list` is not `null`, returns the number of elements written to `list`, - /// otherwise it returns the total number of currently available interfaces. - out count: usize; - } - - /// Queries the description of an interface. - syscall get_description { - in interface: InterfaceId; - - out description: InterfaceDescription; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - /// Queries the physical address for the interface. - syscall get_physical_address { - in interface: InterfaceId; - - out address: PhysicalAddress; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - /// Attempts to change the physical address for an interface. - async_call SetPhysicalAddress { - in interface: InterfaceId; - - in address: PhysicalAddress; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// The interface does not allow changing its physical address. - error Unsupported; - - error SystemResources; - } - - /// Enables or disables kernel subsystems for an interface. - /// - /// NOTE: The two sets `enable` and `disable` must be disjoint and - /// must not contain overlapping subsystems. - syscall control_subsystems { - /// The interface for which kernel subsystems should be enabled - /// or disabled. - in interface: InterfaceId; - - /// Every subsystem in this set will be started. - in enable: SubsystemSet; - - /// Every subsystem in this set will be stopped. - in disable: SubsystemSet; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// `enable` and `disable` are overlapping sets and conflict - /// in their semantics. - error InvalidValue; - } - - /// Queries the currently enabled subsystems for the given - /// interface. - syscall get_subsystems { - in interface: InterfaceId; - - /// The set of all enabled network subsystems for `interface`. - out enabled: SubsystemSet; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - //? IP Addressing - - /// Enumeration of potential sources for IP addresses. - enum AddressOrigin : u8 { - /// The IP address was manually added with `AddAddress`. - item manual = 0; - - /// The IP address was assigned by the DHCPv4 network subsystem. - item dhcp4 = 1; - - /// The IP address was assigned by the DHCPv6 network subsystem. - item dhcp6 = 2; - - /// The IP address was assigned by the SLAAC network subsystem. - item slaac = 3; - - /// The IP address was assigned automatically by the kernel through - /// configuration files. - item autoconfig = 4; - } - - /// A binding of an IP address to a network interface. - struct AddressBinding { - /// The IP address that is bound. - field address: IP; - - /// The prefix of the address. Defines which prefix is - /// reachable through the interface directly without routing. - /// - /// NOTE: Must be ≤ 32 for IPv4. - /// - /// NOTE: must be ≤ 128 for IPv6. - field prefix_len: u8; - - /// The origin of the IP address. - /// - /// NOTE: `AddAddress` will ignore this field and always - /// use `AddressOrigin.manual`. - field origin: AddressOrigin; - - /// The lifetime of the IP address. - /// - /// After this timestamp is reached by `clock.monotonic`, the IP address - /// will be automatically removed by the kernel. - /// - /// NOTE: The kernel will also automatically remove the associated connected route. - /// - /// NOTE: If an IP address should not expire, pass `clock.Absolute.infinity`. - field valid_until: clock.Absolute; - } - - /// Enumerates the currently available IP addresses for an interface. - syscall enumerate_addresses { - /// The interface to query. - in interface: InterfaceId; - - /// Buffer that shall receive the list of address bindings or `null` if the - /// total amount of bindings should be queried. - in bindings: ?[]AddressBinding; - - /// If `bindings` is not `null`, returns the number of elements written to `bindings`, - /// otherwise it returns the total number of currently available bindings. - out count: usize; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - /// Adds a new IP address to an interface. - /// - /// NOTE: This operation will implicitly add a new route to the routing - /// table based off (`interface`, `binding.address`, `binding.prefix_len`). - /// - /// The route will use the prefix derived from `binding.address` and will - /// set the `IPv6.scope` for the `Route.network` to `InterfaceId.any`. - /// - /// This route will have `RouteOrigin.connected`. - /// - /// NOTE: This operation is upserting on `binding.address` and will - /// replace the properties of the current address with those from - /// `binding`. - /// This will also update the connected route in the routing table. - /// - /// NOTE: This is an asynchronous call as adding IP addresses may require - /// communication with the NIC to set up filters. This could take - /// some time and thus, the operation was made overlapped. - - async_call AddAddress { - //? TODO: Add a "force: bool" parameter and a new "error NetworkConflict" (ARP/DAD conflict) - //? Detail: IPv6 requires DAD (Neighbor Solicitation for the derived IP) before the address - //? is fully usable. If DAD fails (someone else has the IP), the address must be marked - //? as duplicated and not used. - - - /// The interface which should receive a new IP binding. - in interface: InterfaceId; - - /// The binding specification for the IP address. - in binding: AddressBinding; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// Binding is not a valid value. - /// - /// This could be due to the following reasons: - /// - `binding.address` is not valid. - /// - `binding.prefix_len` cannot be applied to `binding.address`. - /// - `binding.valid_until` is in the past. - /// - `binding.address.addr.ipv6.scope` is not `interface` for link-local IPv6 addresses. - /// - `binding.address.addr.ipv6.scope` is not `InterfaceId.any` for non-link-local IPv6 addresses. - error InvalidValue; - - /// There was an i/o error that lead to the failure of this operation. - error IoError; - - /// The IPv4 or IPv6 network subsystem is disabled and the address type can't be used on this interface. - error SubsystemDisabled; - - error SystemResources; - } - - /// Removes an address from the interface. - /// - /// NOTE: This operation will implicitly remove the connected route from the routing - /// table with the key (`interface`, prefix address derived from `address`, `RouteOrigin.connected`). - /// - /// NOTE: This is an asynchronous call as adding IP addresses may require - /// communication with the NIC to set up filters. This could take - /// some time and thus, the operation was made overlapped. - /// - /// NOTE: If `address` does not exist on `interface`, the operation does nothing. - /// - /// LORE: This operation has no subsystem failure possible as removing an address - /// is idempotent for non-existing addresses, thus there's no reason to - /// care for this error. The final outcome is the same: The address doesn't - /// exist on the interface. - async_call RemoveAddress { - /// The interface which should have an address removed. - in interface: InterfaceId; - - /// The address that shall be removed. - /// - /// NOTE: As for each address, only a single binding can exist, we just need - /// the address to remove the IP binding. - /// - /// NOTE: If this is an IPv6 address, `scope` must match the rules for valid IPv6 - /// addressed and `scope` must be either `interface` or `InterfaceId.any`. - in address: IP; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// `address` is not valid. - error InvalidValue; - - /// There was an i/o error that lead to the failure of this operation. - error IoError; - - error SystemResources; - } - - //? Routing - - /// Enumeration of potential origins for routes. - enum RouteOrigin : u8 { - /// The route was manually created with `add_route`. - item manual = 0; - - /// The route was created by the DHCPv4 network subsystem. - item dhcp4 = 1; - - /// The route was created by the DHCPv6 network subsystem. - item dhcp6 = 2; - - /// The route was created by the SLAAC network subsystem. - item slaac = 3; - - /// The route was created by the kernel through configuration files. - item autoconfig = 4; - - /// The route was created by the kernel when adding an IP address - /// to a network interface. - item connected = 5; - } - - /// A route describes rules on where to send IP packets. - /// - /// A route is valid if: - /// - `network.type` is `gateway.type`. - /// - `prefix_len` is valid for `network.type`. - /// - `valid_until` is in the future. - /// - If the route is an IPv6 route: - /// - `network.scope` is `InterfaceId.any`. - /// - `gateway.scope` is `interface` for all link-local addresses. - /// - `gateway.scope` is `InterfaceId.any` for all other addresses. - struct Route { - /// Defines the target network for this route. - /// - /// NOTE: This value is only semantically valuable together with `prefix_len`. - /// - /// NOTE: For IPv6, `scope` must be set to `InterfaceId.any` to prevent conflicting - /// definitions with `interface`. - field network: IP; - - /// Defines how many bits of the IP in `network` identify the target network. - /// - /// NOTE: Must be ≤ 32 for IPv4. - /// - /// NOTE: must be ≤ 128 for IPv6. - field prefix_len: u8; - - /// Defines where the next-hop for the target network is and - /// sends the packets this way. - /// - /// NOTE: If set to the unspecified IP (`0.0.0.0` or `::`), the - /// route defines an on-link route and target addresses should - /// be discovered via neighbor discovery (ARP for IPv4, NDP for IPv6). - /// - /// NOTE: If not set to an unspecified IP, the address must be a valid - /// unicast address. - /// - /// NOTE: If gateway is a link-local IPv6 address, the `scope` must be the - /// `interface` of the route. - /// - /// NOTE: `gateway.type` must match `network.type`. - field gateway: IP; - - /// The interface that will be used for sending the packets to the target network. - field interface: InterfaceId; - - /// The priority is a tie-breaker for when multiple rules would match - /// with the same prefix, but different interfaces. - /// - /// Higher priorities win. - /// - /// NOTE: When two routes with the same prefix and priority match, - /// the first inserted route is taken. - field priority: u16; - - /// The source system that added this route to the routing table. - /// - /// NOTE: `add_route` will ignore the value and will always add a route - /// with `RouteOrigin.manual`. - field origin: RouteOrigin; - - /// The lifetime of the route. - /// - /// After this timestamp is reached by `clock.monotonic`, the route - /// will be automatically removed by the kernel. - /// - /// NOTE: If a route should not expire, pass `clock.Absolute.infinity`. - field valid_until: clock.Absolute; - } - - /// Enumerates the routing table. - /// - /// NOTE: The table is returned ordered longest to shortest prefix, - /// highest-to-lowest priority, then retains insertion order. - /// - /// LORE: Enumerating routes ordered is cheap for the kernel as it - /// has to keep the table ordered in-memory anyways for efficient - /// evaluation, and thus we can expose this property into userland. - syscall enumerate_routes { - /// Buffer that shall receive the list of routes or `null` if the - /// total amount of routes should be queried. - in routes: ?[]Route; - - /// If `routes` is not `null`, returns the number of elements written to `routes`, - /// otherwise it returns the total number of routes. - out count: usize; - } - - /// Adds a new route to the routing table. - syscall add_route { - /// The route to be added. - in route: Route; - - /// The route is not valid. - /// - /// See `Route` documentation for validation rules. - error InvalidValue; - - /// Another route for the same target network exists on the same interface. - /// - /// This means that another route with (`route.interface`, `route.network`, `route.prefix_len`, `route.gateway`) exists. - error Conflict; - - /// `route.interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The IPv4 or IPv6 network subsystem is disabled and the `route.network` type can't be used on `route.interface`. - error SubsystemDisabled; - - error SystemResources; - } - - /// Removes an existing route from the routing table. - /// - /// NOTE: Removing a non-existent route is idempotent and does nothing. - /// - /// LORE: This syscall has no subsystem failure possible as removing a route - /// is idempotent for non-existing routes, thus there's no reason to - /// care for this error. The final outcome is the same: The route doesn't - /// exist on the interface anymore. - syscall remove_route { - /// The route to delete. - /// - /// NOTE: When selecting which rules should be deleted, `route.priority`, - /// `route.origin`, `route.valid_until` are ignored. - in route: Route; - - /// The route is not valid. - /// - /// See `Route` documentation for validation rules. - error InvalidValue; - - /// `route.interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - //? Link state - - enum LinkState : u8 { - /// The network interface has not recognized any connection. - item down = 0; - - /// The network interface is connected to the network. - item up = 1; - } - - /// Queries the current link state of a network interface. - syscall get_link_state { - in interface: InterfaceId; - - /// Current state of the link. - out state: LinkState; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - //? /// Completes when a link changes its state. - //? async_call WaitForLinkState { - //? /// The interface for which a link state shall be awaited. - //? in interface: InterfaceId; - - //? /// If not `null`, the operation completes when the link becomes `desired`. - //? /// Otherwise, the operation completes on the next interface state change. - //? in desired: ?LinkState; - - //? /// The new link state. - //? out current: LinkState; - - //? /// `interface` is not a valid interface id (anymore). - //? error InvalidInterface; - - //? /// The underlying `interface` was removed during this operation. - //? error Gone; - //? } - - /// Sends an ICMP or ICMPv6 echo request. - /// - /// NOTE: This can be used to test if a host is reachable. - async_call Ping { - /// The interface that shall send the ping message. - /// - /// NOTE: May be `InterfaceId.any` to use the routing table - /// to determine which interface and gateway shall be used. - in interface: InterfaceId; - - /// The IP that should be tested. - /// - /// NOTE: `target.type` decides which underlying protocol shall be used. - in target: IP; - - /// Maximum number of hops before the operation times out. - in ttl: u8; - - /// The deadline for the operation. - /// - /// The operation returns the `Timeout` error when `timeout` is smaller than `clock.monotonic()`. - /// - /// LORE: In contrast to many other overlapped operations, a `Ping` would potentially - /// never complete and it's an expected outcome that no response is received. - /// Thus, the general rule of "no timeouts in overlapped operations" is broken - /// here on purpose. - in timeout: clock.Absolute; - - /// The payload of the ICMP/ICMPv6 echo request that is sent with the message. - /// - /// NOTE: This buffer must stay valid until the end of the operation. - in payload_request: bytestr; - - /// The payload of the ICMP/ICMPv6 echo response that may be received. - /// - /// NOTE: This buffer must stay valid until the end of the operation. - /// - /// NOTE: `payload_response.len` must not be smaller than `payload_request.len`. - /// - /// NOTE: This buffer will only include the echoed payload. - in payload_response: bytebuf; - - /// The IP address which finally answered our echo request. - /// - /// NOTE: For responses from link-local IPv6 addresses, `IPv6.scope` is set - /// to the interface that received the echo response. - out responder: IP; - - /// The timestamp when the kernel received the echo reply. - out received_at: clock.Absolute; - - /// Actual number of bytes sent from `payload_request`. - /// - /// NOTE: This may be smaller than `payload_request.len` when the message is truncated. - out request_len: usize; - - /// Number of bytes received from `responder`. - out response_len: usize; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// No response was received until `timeout`. - error Timeout; - - /// The ping operation yielded an ICMP error instead of an ICMP echo. - error IcmpError; - - /// The kernel does not know a route to `target`. - error MissingRoute; - - /// A parameter was badly specified. - /// - /// This may be due to: - /// - `payload_request.len` > `payload_response.len`. - /// - `target` is an link-local IPv6 address with a `scope` that is not `interface`. - error InvalidValue; - - /// The IPv4 or IPv6 network subsystem is disabled and the `target` type can't be used on the effective interface. - error SubsystemDisabled; - - /// `interface` is down and cannot send any data. - error LinkDown; - - /// There was an i/o error that lead to the failure of this operation. - error IoError; - - error SystemResources; - } - } - - /// Neighbor discovery / neighbor cache (ARP for IPv4, NDP for IPv6). - namespace neighborhood { - /// Enumerates potential origins for a network neighbor. - enum Origin : u8 { - /// The neighbor was manually created with `add_neighbor`. - item manual = 0; - - /// The neighbor was discovered by the kernel through an ARP/NDP request. - item learned = 1; - - /// The IP address was created automatically by the kernel through - /// configuration files. - item autoconfig = 2; - } - - /// A neighbor is the mapping of an IP address to a physical address. - /// - /// The following invariants apply to the timestamp values: - /// - `last_updated` >= `known_since` - /// - `expires_after` >= `known_since` - /// - `fresh_until` >= `known_since` - /// - `fresh_until` <= `expires_after` - /// - `fresh_until` >= `last_updated` if `state == State.reachable` - struct Neighbor { - /// The interface on which the neighbor was discovered. - field interface: link.InterfaceId; - - /// The IP address that identifies the neighbor. - /// - /// NOTE: Depending on `ip.type`, the following protocols were used for discovery: - /// - IPv4: ARP - /// - IPv6: NDP - /// - /// NOTE: If this IP is a link-local IPv6 ip, its scope must be equal to `interface`. - field ip: IP; - - /// The physical address that was discovered. - /// - /// NOTE: This value is an optional `link.PhysicalAddress` with the following rules: - /// - If `state` is `State.reachable`, `physical.type` is never `link.PhysicalAddress.Type.null`. - /// - Else, the `physical.type` always is `link.PhysicalAddress.Type.null`. - field physical: link.PhysicalAddress; - - /// The state defines if a neighbor is usable or not. - /// - /// NOTE: Can never have a higher numeric value than `enumerate_neighbors.max_state` - /// when received through enumeration. - field state: State; - - /// The source which discovered the neighbor. - /// - /// NOTE: When the neighbor is added with `add_neighbor`, this value is ignored - /// and `Origin.manual` is used. - field origin: Origin; - - /// Flags storing additional information about this neighbor. - field flags: Flags; - - /// The timestamp when this neighbor entry was added to the list. - /// - /// NOTE: This field allows computing the age of the neighbor. - /// - /// NOTE: The other timestamps will always be at least `known_since`. - field known_since: clock.Absolute; - - /// The timestamp when this neighbor entry was refreshed last. - /// - /// NOTE: This allows deriving a liveness for the neighbor. - field last_updated: clock.Absolute; - - /// The timestamp until which this neighbor is assumed to be valid. - /// - /// NOTE: For IPv4/ARP, the lifetime is computed from a kernel configuration. - /// - /// NOTE: For IPv6/NDP, this is derived from the ReachableTime. - /// - /// NOTE: As long as `state == State.reachable`, this value is never less - /// than `last_updated`. - /// - /// NOTE: When `clock.monotonic` returns a value bigger than this, the neighbor has - /// become stale. - /// This means the neighbor isn't necessarily valid anymore, but the kernel assumes - /// it's still usable. - /// The effect of this is that the kernel will still attempt to directly communicate - /// with the neighbor without awaiting a neighbor discovery, but it will trigger - /// an asynchronous re-discovery to ensure the neighbor still exists. - field fresh_until: clock.Absolute; - - /// The drop-dead time after which this neighbor is killed. - /// - /// NOTE: The kernel will automatically remove the entry as soon - /// as `clock.monotonic` reaches this value. - field expires_after: clock.Absolute; - - /// Flags that further specify the neighbors state. - bitstruct Flags : u8 { - /// This neighbor is believed to be an IPv6 capable router on the link. - /// - /// NOTE: This value may change over time based on observed communications on the network. - /// - /// NOTE: For IPv4 neighbors, this value is always `false`. - field is_router: bool; - - /// This neighbor is proxied through network segments. - /// - /// Set only when the kernel knows ND proxying is in effect for this mapping (e.g., learned through ND-proxy mechanisms / configuration). Otherwise the value is false. - /// - /// NOTE: For IPv4 neighbors, this value is always `false`. - field is_proxy: bool; - - reserve u6 = 0; - } - - /// Enumeration of potential states a neighbor has. - /// - /// NOTE: The numeric value of a state is a "usefulness" order and - /// is used by `enumerate_neighbors` to filter states. - enum State : u8 { - /// The neighbor is actively reachable and valid. - /// - /// This means the kernel will immediately use the physical address without performing - /// a request first. - /// - /// NOTE: When `clock.monotonic` returns a value bigger than `fresh_until`, the kernel - /// will perform an automatic background discovery of the neighbor to check if - /// neighbor is still valid. - /// - /// NOTE: When this state is active, `Neighbor.physical.type` is never `link.PhysicalAddress.Type.null`. - item reachable = 0; - - /// A neighbor discovery was executed and the neighbor could not be found. - /// - /// NOTE: This means the kernel will return an error for operations using this - /// neighbor address until `clock.monotonic` reaches `expires_after`. - /// - /// The next request after the `expires_after` is reached will trigger a new discovery process. - /// - /// NOTE: When this state is active, `Neighbor.physical.type` is `link.PhysicalAddress.Type.null`. - item failed = 1; - - /// The neighbor was requested, but isn't `reachable` nor `failed` yet. - /// - /// NOTE: This state is only set until the kernel has performed the first discovery. - /// It does not represent the background discovery. - /// - /// NOTE: When this state is active, `Neighbor.physical.type` is `link.PhysicalAddress.Type.null`. - item resolving = 2; - } - } - - /// Enumerates the currently known neighborhood for an interface. - syscall enumerate_neighbors { - /// The interface for which we want to receive the neighbors - /// or `link.InterfaceId.any` to enumerate the neighbors of all interfaces. - in interface: link.InterfaceId; - - /// Defines for which IP protocols the neighbors should be returned. - /// - /// NOTE: If `IP.Type.any` is passed, both IPv4 and IPv6 neighbors are returned, - /// assuming the corresponding subsystem is enabled. - in protocol: IP.Type; - - /// A buffer that receives the neighbors of `interface` or `null` to query - /// the total amount of neighbors. - in neighbors: ?[]Neighbor; - - /// Defines the maximum integer state value this enumeration returns. - /// This effectively allows filtering the returned list for: - /// - `Neighbor.State.reachable`: Only reachable entries are returned. This is the "true" neighborhood as currently known. - /// - `Neighbor.State.failed`: Only entries with a well-defined state are returned. This also yields knowledge about who is currently not our neighbor. - /// - `Neighbor.State.resolving`: All entries are returned. This allows querying if we're currently searching for a specific neighbor. - in max_state: Neighbor.State; - - /// The number of items written to `neighbors` if not `null`, otherwise - /// the total number of neighbors returned by the query. - out count: usize; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// Returned when `interface` is not `link.InterfaceId.any` and: - /// - `protocol` is `IP.Type.ipv4` and the ipv4 subsystem is disabled for `interface`. - /// - `protocol` is `IP.Type.ipv6` and the ipv6 subsystem is disabled for `interface`. - /// - `protocol` is `IP.Type.any` and both the ipv4 and ipv6 subsystem are disabled for `interface`. - error SubsystemDisabled; - } - - /// Queries a single neighbor. - /// - /// NOTE: `neighbor.state` must be queried to check reachability for a given - /// IP, as the neighbor might have any possible state. - syscall get_neighbor { - /// The interface for which the neighbor should be queried, or - /// or `link.InterfaceId.any` to query the neighbor on all interfaces. - in interface: link.InterfaceId; - - /// The IP address to query. - /// - /// NOTE: If `interface` is not `link.InterfaceId.any` and this IP is a link-local IPv6 ip, - /// its scope must be equal to `interface`. - /// - /// NOTE: If `interface` is `link.InterfaceId.any` and this IP is a link-local IPv6 ip, - /// its scope is used for `interface`. - in ip: IP; - - /// The neighbor entry for `ip`. - out neighbor: Neighbor; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// `ip` is an IPv6 address, but the scope is not suitable for `interface`. - error InvalidValue; - - /// The requested neighbor is not available in the neighborhood. - error NotAvailable; - - /// The `ip` is available on more than one `interface`. - /// - /// NOTE: This error can only happen when `interface` is `link.InterfaceId.any`. - error Conflict; - - /// Returned when the associated subsystem of `ip.type` is disabled for `interface`. - /// NOTE: Can never be returned if `interface == link.InterfaceId.any`. If no interface - /// with an enabled subsystem for `ip.type` has `ip`, `NotAvailable` is returned.” - error SubsystemDisabled; - } - - /// Adds a new static neighbor to the neighborhood. - /// - /// NOTE: The kernel will do the following transformations on `neighbor`: - /// - `neighbor.state = Neighbor.State.reachable` if `neighbor.physical.type != link.PhysicalAddress.Type.null` - /// - `neighbor.state = Neighbor.State.failed` if `neighbor.physical.type == link.PhysicalAddress.Type.null` - /// - `neighbor.origin = Origin.manual` - /// - `neighbor.known_since = clock.monotonic()` - /// - `neighbor.last_updated = clock.monotonic()` - /// - `neighbor.fresh_until = neighbor.expires_after` - /// - /// NOTE: `neighbor` will be validated *after* the transformations are applied. - syscall add_neighbor { - /// The neighbor to add. - - in neighbor: Neighbor; - - /// Defines that if a neighbor with the same IP already exists, - /// it is implicitly replaced with `neighbor`. - in upsert: bool; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The neighbor does not encode a correct value. - /// - /// In addition to the general validity rules of `Neighbor`, the following rules apply: - /// - `neighbor.interface` must not be `link.InterfaceId.any`. - /// - `neighbor.ip` must be a valid IP address. - /// - `neighbor.ip.scope` must be `neighbor.interface` if `neighbor.ip` is a link-local IPv6 address. - /// - `neighbor.expires_after` must not be in the past. - error InvalidValue; - - /// `upsert` is `false` and a neighbor with `neighbor.ip` already exists. - error Conflict; - - /// Returned when the associated subsystem of `neighbor.ip.type` is disabled for `neighbor.interface`. - error SubsystemDisabled; - - error SystemResources; - } - - /// Removes a single neighbor from the neighborhood. - /// - /// NOTE: If the neighbor with `ip` was created through - /// neighbor discovery, `include_learned` must be - /// set, otherwise `Forbidden` is returned as an error. - /// - /// NOTE: If no neighbor with `ip` exists, this syscall is idempotent. - syscall remove_neighbor { - /// The interface for which the neighbor should be removed. - in interface: link.InterfaceId; - - /// The neighbor to be removed. - /// - /// NOTE: If this IP is a link-local IPv6 ip, its scope must be equal to `interface`. - in ip: IP; - - /// Guardrail to prevent accidental removal of neighbors created - /// through neighbor discovery. - in include_learned: bool; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// `ip` is an IPv6 address, but the scope is not suitable for `interface`. - error InvalidValue; - - /// If `include_learned` is `false` and the neighbor to remove has - /// `Neighbor.origin == Origin.learned`. - /// - /// NOTE: Will never happen if no neighbor with `ip` exists. - error Forbidden; - - /// Returned when: - /// - `ip.type` is `IP.Type.ipv4` and the ipv4 subsystem is disabled for `interface`. - /// - `ip.type` is `IP.Type.ipv6` and the ipv6 subsystem is disabled for `interface`. - error SubsystemDisabled; - } - - /// Invalidates the neighbor table for `interface` and removes all items for `protocol`. - syscall flush_neighbors { - /// The interface for which the neighborhood should be flushed. - /// If `link.InterfaceId.any` is passed, all neighborhoods for all interfaces are flushed. - in interface: link.InterfaceId; - - /// Defines for which IP protocols the neighbors should be flushed. - /// - /// NOTE: If `IP.Type.any` is passed, both IPv4 and IPv6 neighbors are flushed, - /// assuming the corresponding subsystem is enabled. - in protocol: IP.Type; - - /// If `true`, will only remove the neighbors with `Neighbor.origin == Origin.learned`. - in keep_manual: bool; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// Returned when the associated subsystem of `protocol` is disabled for `interface` - /// and `interface` is not `link.InterfaceId.any`. - error SubsystemDisabled; - } - - /// Resolves an IP address the the associated physical address. - /// - /// NOTE: The kernel will potentially delay sending ARP/NDP requests - /// until an internal timeout has elapsed. - /// - /// This is to prevent flooding the network with requests. - /// - /// `flood` overwrites this behavior. - /// - /// NOTE: Not every schedule of a resolve triggers a new discovery. - /// The kernel is free to fuse several scheduled `Resolve` - /// operations for the same (`interface`, `target`) groups. - /// - /// `deadline` is still respected for each individual operation. - /// - /// This does not apply to operations that have `flood` set. - async_call Resolve { - /// The interface which should query its network for the physical address. - in interface: link.InterfaceId; - - /// The IP for which the physical address should be resolved. - /// - /// NOTE: The following protocols will be used depending on `target.type`: - /// - IPv4: ARP - /// - IPv6: NDP - /// - /// NOTE: If this IP is a link-local IPv6 ip, its scope must be equal to `interface`. - in target: IP; - - /// The deadline for the operation. - /// - /// The operation returns the `Timeout` error when `deadline` is smaller than `clock.monotonic()`. - /// - /// LORE: In contrast to many other overlapped operations, a `Resolve` would potentially - /// never complete and it's an expected outcome that no response is received. - /// Thus, the general rule of "no timeouts in overlapped operations" is broken - /// here on purpose. - in deadline: clock.Absolute; - - /// Overwrites the kernels internal flood protection and immediately starts - /// sending a request. - /// - /// NOTE: Setting `flood` disables the internal fusing and the kernel will trigger - /// many ARP/NDP requests. - in flood: bool; - - /// The information received by the resolver. - out neighbor: Neighbor; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// No response was received until `deadline`. - error Timeout; - - /// `target` was an invalid IP or - /// `target` is a link-local IPv6 address with `target.scope != interface`. - error InvalidValue; - - /// The associated subsystem for `target.type` is disabled on `interface`. - error SubsystemDisabled; - - /// `interface` is down and cannot send any data. - error LinkDown; - - /// There was an i/o error that lead to the failure of this operation. - error IoError; - - error SystemResources; - } - - //? TODO: Add async_call WaitForUpdate when a proper model for the wait operation was found. - } - - /// IPv6 Stateless Address Autoconfiguration (SLAAC). - namespace slaac { - - //? TODO: Add support for RFC 4191 ( Default Router Preferences and More-Specific Routes) - - /// Represents a single Prefix Information Option (PIO) learned from an IPv6 Router Advertisement. - /// - /// NOTE: The following invariants apply to the timestamp types: - /// - `preferred_until` >= `received_at` - /// - `updated_at` >= `received_at` - /// - `valid_until` >= `received_at` - /// - `valid_until` >= `preferred_until` - /// - `valid_until` >= `updated_at` - struct Prefix { - /// The router that announced the prefix. - /// - /// NOTE: This address is typically a link-local address with the associated interface for `IPv6.scope`. - /// - /// NOTE: If router is link-local, `router.scope` must be the associated interface. - field router: IPv6; - - /// The prefix announced by the router. - /// - /// NOTE: `prefix.scope` must be `link.InterfaceId.any`. - /// - /// NOTE: All bits beyond `prefix_len` are zeroed out to enable determinism. - field prefix: IPv6; - - /// The number of bits inside `prefix` that are part of the prefix. - /// NOTE: Can be between 0 and 128 inclusive. - field prefix_len: u8; - - /// Defines additional information for this prefix. - field flags: Flags; - - /// The timestamp when the kernel initially received this prefix. - field received_at: clock.Absolute; - - /// The timestamp when the kernel last received this prefix. - field updated_at: clock.Absolute; - - /// Timestamp until which the kernel will prefer this prefix and derived IP addresses. - /// - /// NOTE: The prefix is still valid until `valid_until`, but the network stack should choose - /// another prefix if possible. - field preferred_until: clock.Absolute; - - /// Timestamp at which the kernel will drop the prefix. - field valid_until: clock.Absolute; - - bitstruct Flags : u8 { - /// The prefix is reachable directly through this interface without - /// the need of a gateway. - /// - /// NOTE: If the slaac subsystem is enabled on the associated interface, the kernel will - /// automatically add (or upsert) a on-link prefix route for this prefix with: - /// - `link.Route.network` set to `Prefix.prefix`. - /// - `link.Route.prefix_len` set to `Prefix.prefix_len`. - /// - `link.Route.gateway` set to the unspecified address (`::`). - /// - `link.Route.interface` set to the associated interface of the `Prefix`. - /// - `link.Route.priority` set to `0`. - /// - `link.Route.origin` set to `link.RouteOrigin.slaac`. - /// - `link.Route.valid_until` set to a value depending on the context. - /// - /// If multiple `Prefix` entries exist for the same - /// (`interface`, `prefix`, `prefix_len`) (i.e. advertised by different routers), - /// the derived on-link prefix route remains present as long as at least one matching - /// `Prefix` entry with `on_link = true` is still valid. - /// - /// The kernel sets `link.Route.valid_until` to the maximum `valid_until` - /// across all currently valid matching `Prefix` entries with `on_link = true`. - /// - /// If the set of matching prefixes changes (update/expiry/flush), - /// the kernel recomputes `link.Route.valid_until` accordingly and removes the - /// derived route once no matching prefixes remain. - /// - /// NOTE: The kernel will only create/update routes with `link.Route.origin = link.RouteOrigin.slaac` - /// and will not modify routes of other origins. - field on_link: bool; - - /// The interface shall automatically derive IP addresses from the prefix. - /// - /// NOTE: See `Config` on how this bit will be used. - /// - /// NOTE: The kernel will automatically derive addresses from the prefix when the slaac subsystem - /// is enabled and `prefix_len` is 64. - /// - /// The derived addresses will be added with `link.AddressBinding.origin` - /// set to `link.AddressOrigin.slaac`. - /// - /// If multiple matching `Prefix` entries exist for the same - /// (`interface`, `prefix`, `prefix_len`) with `autonomous = true`, - /// the kernel sets `link.AddressBinding.valid_until` to the maximum `valid_until` - /// across all currently valid matching entries, and recomputes it on - /// update/expiry/flush. The address is removed once no matching prefixes remain. - /// - /// NOTE: The kernel will only create/update address bindings with `link.AddressBinding.origin = link.AddressOrigin.slaac` - /// and will not modify address bindings of other origins. - field autonomous: bool; - - reserve u6 = 0; - } - } - - /// Represents the router identity from a single Router Advertisement. - /// Also contains neighborhood discovery parameters sent with the Router Advertisement. - /// - /// NOTE: The following invariants apply to the timestamp types: - /// - `valid_until` >= `received_at` - /// - `valid_until` >= `updated_at` - /// - `updated_at` >= `received_at` - struct Router { - /// The address of the discovered router. - /// - /// NOTE: If the slaac subsystem is enabled for the interface, a default route - /// will be upserted that uses `address` as the next-hop: - /// - /// - `link.Route.network` set to the unspecified address (`::`). - /// - `link.Route.prefix_len` set to `0`. - /// - `link.Route.gateway` set to `Router.address`. - /// - `link.Route.interface` set to the associated interface for this router. - /// - `link.Route.priority` set to `0`. - /// - `link.Route.valid_until` set to `Router.valid_until`. - /// - `link.Route.origin` set to `link.RouteOrigin.slaac`. - /// - /// NOTE: If this IP is a link-local IPv6 IP, its scope must be equal to associated interface. - /// - /// NOTE: The kernel will only create/update routes with `link.Route.origin = link.RouteOrigin.slaac` - /// and will not modify routes of other origins. - field address: IPv6; - - /// The max. number of hops for outbound packets via this router. - /// - /// NOTE: 0 means the router did not advertise a hop limit and the - /// actual value is unknown. - field hop_limit: u8; - - /// The MTU for the link. - /// - /// NOTE: 0 means the router did not advertise an MTU and the - /// actual value is unknown. - field mtu: u32; - - /// The time in milliseconds a neighbor should be considered reachable. - /// - /// NOTE: If zero, a default value shall be used (typically 30 seconds). - /// - /// NOTE: This value affects how `neighborhood.Neighbor.fresh_until` is derived. - field reachable_time_ms: u32; - - /// Time between retransmitted Neighbor Solicitation messages in milliseconds. - /// - /// This affects neighbor discovery retries (including DAD). - /// - /// NOTE: If zero, the router did not specify a value and the kernel uses its default. - field retrans_time_ms: u32; - - /// Defines additional information for this router. - field flags: Flags; - - /// The timestamp at which the kernel received this Router Advertisement. - field received_at: clock.Absolute; - - /// The timestamp when the kernel last received this Router Advertisement. - field updated_at: clock.Absolute; - - /// The timestamp at which the kernel will automatically remove the router. - field valid_until: clock.Absolute; - - bitstruct Flags : u8 { - /// If set, defines that DHCPv6 shall be used to obtain addresses. - /// - /// NOTE: If set, and the dhcp6 subsystem is enabled for the interface, the kernel - /// will automatically perform the DHCPv6 requests when the Router Advertisement - /// is received. - field managed: bool; - - /// If set, defines that additional configuration like DNS servers shall be obtained through - /// DHCPv6. - /// - /// NOTE: If set, and the dhcp6 subsystem is enabled for the interface, the kernel - /// will automatically perform the DHCPv6 requests when the Router Advertisement - /// is received. - field other_config: bool; - - reserve u6 = 0; - } - } - - //? Configuration: - - /// Enumeration of potential methods for stable address generation. - /// - /// NOTE: Addresses generated with these methods are reboot-safe - /// and will be stable for the system. - enum StableAddressGeneration : u8 { - /// Don't generate an address at all. - item none = 0; - - /// Derives stable addresses from the physical link address. - /// - /// NOTE: This method is best-effort and may use other sources to - /// generate reboot-stable IDs for the interface. - item eui64 = 1; - - /// Derives stable addresses in a privacy-preserving manner using a - /// stable secret on the system. - item rfc7217 = 2; - } - - /// Enumeration of potential methods for temporary address generation. - /// - /// NOTE: Addresses generated with these methods are ephemeral and may - /// be rotated after a certain time. - enum TemporaryAddressGeneration : u8{ - /// Don't generate an address at all. - item none = 0; - - /// Temporary addresses are generated in a (pseudo) random pattern. - item rfc8981 = 1; - } - - /// SLAAC configuration for an interface. - struct Config { - /// The currently used stable address generation method to automatically derive - /// addresses when requested. - /// - /// NOTE: The default values is `StableAddressGeneration.eui64` unless - /// changed by a kernel configuration. - field stable_method: StableAddressGeneration; - - /// The currently used temporary address generation method to automatically derive - /// addresses when requested. - /// - /// NOTE: The default values is `TemporaryAddressGeneration.none` unless - /// changed by a kernel configuration. - field temp_method: TemporaryAddressGeneration; - } - - /// Queries the currently enabled configuration for the given interface. - /// - /// NOTE: This syscall will also work when the slaac subsystem is disabled, - /// to enable setting a policy before activating the subsystem. - syscall get_config { - /// The interface for which the configuration shall be returned. - in interface: link.InterfaceId; - - /// The currently active configuration. - out config: Config; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - /// Changes the currently enabled configuration for the given interface. - /// - /// NOTE: This will not re-generate addresses derived with the previous configuration. - /// - /// NOTE: This syscall will also work when the slaac subsystem is disabled, - /// to enable setting a policy before activating the subsystem. - syscall set_config { - /// The interface for which the SLAAC configuration shall be updated. - in interface: link.InterfaceId; - - /// The new configuration, replacing the previous one. - in config: Config; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - } - - //? Status Query: - - /// A summary of the SLAAC status for an interface. - struct Status { - /// The timestamp when the kernel last received a Router Advertisement - /// or a `Router`/`Prefix` expired. - field last_update: clock.Absolute; - - /// Additional boolean properties - field flags: Flags; - - bitstruct Flags : u16 { - /// `true` if at least a single `Prefix.flags.on_link` is set. - field on_link: bool; - - /// `true` if at least a single `Prefix.flags.autonomous` is set. - field autonomous: bool; - - /// `true` if at least a single `Router.flags.managed` is set. - field managed: bool; - - /// `true` if at least a single `Router.flags.other_config` is set. - field other_config: bool; - - reserve u12 = 0; - } - } - - /// A quick status query to get the current SLAAC status. - /// - /// LORE: This syscall primarily exists to prevent userland - /// to enumerate all properties just to learn some trivial - /// information. - syscall get_status { - /// The interface for which the status shall be queried. - in interface: link.InterfaceId; - - /// The current status of `interface`. - out status: Status; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// Returned when the SLAAC subsystem of `interface` is disabled. - error SubsystemDisabled; - } - - //? Queries: - - /// Enumerates the routers learned through SLAAC for `interface`. - syscall enumerate_routers { - /// The interface for which the routers shall be queried. - in interface: link.InterfaceId; - - /// A buffer that will receive the enumerated routers. - /// - /// NOTE: If `null`, no routers will be enumerated, but only the total - /// count is returned. - in routers: ?[]Router; - - /// Total number of available routers. - /// - /// NOTE: If smaller than `routers.len`, the elements for `routers[count..]` will - /// be left unchanged. - out count: usize; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// Returned when the SLAAC subsystem of `interface` is disabled. - error SubsystemDisabled; - } - - /// Enumerates the prefixes learned through SLAAC for `interface`. - syscall enumerate_prefixes { - /// The interface for which the prefixes shall be queried. - in interface: link.InterfaceId; - - /// A buffer that will receive the enumerated prefixes. - /// - /// NOTE: If `null`, no prefixes will be enumerated, but only the total - /// count is returned. - in prefixes: ?[]Prefix; - - /// Total number of available prefixes. - /// - /// NOTE: If smaller than `prefixes.len`, the elements for `prefixes[count..]` will - /// be left unchanged. - out count: usize; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// Returned when the SLAAC subsystem of `interface` is disabled. - error SubsystemDisabled; - } - - /// Defines what parts to flush. - bitstruct FlushMode : u8 { - /// If `true`, removes all routers from the selected interface. - /// - /// NOTE: This implies that all "default" routes added for this - /// interface through the SLAAC subsystem will also be removed. - field routers: bool; - - /// If `true`, removes all prefixes from the selected interface. - /// - /// NOTE: This implies that all on-link prefix routes added for this - /// interface through the SLAAC subsystem will also be removed. - field prefixes: bool; - - reserve u6 = 0; - } - - /// Flushes the SLAAC state for a given interface. - /// - /// NOTE: A flush counts as an "update event" and will change the - /// `Status.last_update` field to `clock.monotonic()`. - syscall flush { - /// The interface for which the SLAAC state shall be flushed. - in interface: link.InterfaceId; - - /// Defines how to flush the state. - /// - /// NOTE: If all bits inside mode are zero, `InvalidValue` is returned. - in mode: FlushMode; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// Returned when the SLAAC subsystem of `interface` is disabled. - error SubsystemDisabled; - - /// All bits inside `mode` were zero. - error InvalidValue; - } - - /// Forces the refresh of the router advertisements by sending a - /// Router Solicitation. - /// - /// NOTE: The operation will complete when at least a single Router Advertisement - /// was received. - /// - /// NOTE: Not every schedule of a refresh triggers a new Router Solicitation. - /// The kernel is free to fuse several scheduled `Refresh` - /// operations for the same `interface`. - /// - /// `deadline` is still respected for each individual operation. - /// - /// NOTE: If `interface` has no suitable link-local address available, the kernel - /// will send the Router Solicitation from the unspecified address (`::`). - /// If multiple suitable addresses are available, the kernel will choose - /// one in a stable manner: The source address will be the same until the - /// address bindings of `interface` change. - async_call Refresh { - /// The interface for which the SLAAC state shall be refreshed. - in interface: link.InterfaceId; - - /// The deadline for the operation. - /// - /// The operation returns the `Timeout` error when `deadline` is smaller than `clock.monotonic()`. - /// - /// LORE: In contrast to many other overlapped operations, a `Refresh` would potentially - /// never complete and it's an expected outcome that no response is received. - /// Thus, the general rule of "no timeouts in overlapped operations" is broken - /// here on purpose. - in deadline: clock.Absolute; - - /// The new status of `interface`. - out status: Status; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// No response was received until `deadline`. - error Timeout; - - /// `deadline` is not in the future. - error InvalidValue; - - /// Returned when the SLAAC subsystem of `interface` is disabled. - error SubsystemDisabled; - - /// `interface` is down and cannot send any data. - error LinkDown; - - /// There was an i/o error that lead to the failure of this operation. - error IoError; - - error SystemResources; - } - - /// This operation completes when `Status.last_update` changes. - async_call WaitForUpdate { - /// The interface for which an update shall be awaited. - in interface: link.InterfaceId; - - /// The new SLAAC status of `interface`. - out new_status: Status; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// Returned when the SLAAC subsystem of `interface` is disabled. - error SubsystemDisabled; - - error SystemResources; - } - } - - /// Everything related to DHCP management for IPv4. - namespace dhcp4 { - - /// NOTE: These options are defined in RFC 2132. - enum OptionCode : u8 { - //? TODO: Add documentation for all options including their contents and size constraints based on the information from RFC 2132. - - item pad = 0; - item subnet_mask = 1; - item time_offset = 2; - item router = 3; - - item domain_name_server = 6; - - item ip_address_lease_time = 51; - - item server_identifier = 54; - - item renewal_time_value = 58; - - item rebinding_time_value = 59; - - item end = 255; - //? TODO: fill out the rest of the options and document their meaning and use - ... - } - - /// A DHCP option. - struct Option { - /// The type of option - field code: OptionCode; - - /// The value of the option. - /// - /// NOTE: Maximum length of `value` is 255. - /// - /// NOTE: The length of the value must match the legal values for `code`. - /// - /// NOTE: The lifetime of this value for fields in a lease is documented on `Lease` itself. - /// - /// NOTE: Options passed to `RequestLease` will be copied by the kernel in the schedule operation. - field value: []const u8; - } - - /// A DHCP lease. - /// - /// NOTE: Any pointer to an array inside the lease is valid until one of the invalidation events occur: - /// - The interface for the lease is removed from the kernel. - /// - The lease is automatically refreshed by the kernel (may only happen if the dhcp4 subsystem is enabled). - /// - A `ReleaseLease` is scheduled for the owning interface. - /// - A `RequestLease` completes for the owning interface. - /// - /// These events can only happen during a scheduler yield, so the pointers may be dead - /// when the thread yields. - /// - /// In general, an application should not hold onto `Lease` information for longer - /// than technically necessary. - struct Lease { - /// The server that issued the DHCP lease. - /// - /// NOTE: This is either the value sent with `OptionCode.server_identifier` - /// or if the option does not exist, the IP from which the DHCPACK was sent. - field server: IPv4; - - /// The address issued by the DHCP server. - /// - /// NOTE: If the lease is acquired with DHCPINFORM this value will be the - /// unspecified address (`0.0.0.0`). - field address: IPv4; - - /// The prefix length of the network for `address`. - /// - /// NOTE: This value is derived from the value sent with `OptionCode.subnet_mask` - /// or 32 if the option is not present. - /// - /// NOTE: If the lease is acquired with DHCPINFORM this value will be zero. - field prefix_len: u8; - - /// The list of announced routers by the DHCP server. - /// - /// NOTE: This list will be derived from the value sent with `OptionCode.router`. - /// - /// NOTE: The lifetime of this array is documented on `Lease` itself. - field routers: []const IPv4; - - /// The list of DNS servers provided by the DHCP server. - /// - /// NOTE: This list will be derived from the value sent with `OptionCode.domain_name_server`. - /// - /// NOTE: The lifetime of this array is documented on `Lease` itself. - field dns_servers: []const IPv4; - - /// The complete list of options sent by the DHCP server. - /// - /// NOTE: The order inside this array is the same as it was sent by the server. - /// - /// NOTE: The lifetime of this array is documented on `Lease` itself. - /// - /// NOTE: The kernel will remove all options with `OptionCode.pad` and `OptionCode.end`. - field options: []const Option; - - /// The timestamp when the kernel applied the DHCPACK message that issued - /// this lease. - field issued_at: clock.Absolute; - - /// The timestamp when the lease should be renewed (T1). - /// - /// NOTE: If the lease was created by `DHCPINFORM`, the value will be set to - /// `clock.Absolute.infinity`. - /// - /// NOTE: If present, will be set from `OptionCode.renewal_time_value`, otherwise - /// will use the default value of 50% of the total lease time. - /// - /// NOTE: If no lease time can be derived, `clock.Absolute.infinity` is used. - field renew_at: clock.Absolute; - - /// The timestamp when the lease should be rebound (T2). - /// - /// NOTE: If the lease was created by `DHCPINFORM`, the value will be set to - /// `clock.Absolute.infinity`. - /// - /// NOTE: If present, will be set from `OptionCode.rebinding_time_value`, otherwise - /// will use the default value of 87.5% of the total lease time. - /// - /// NOTE: If no lease time can be derived, `clock.Absolute.infinity` is used. - field rebind_at: clock.Absolute; - - /// The timestamp after which the lease is not valid anymore. - /// - /// NOTE: If the lease was created by `DHCPINFORM`, the value will be set to - /// `clock.Absolute.infinity`. - /// - /// NOTE: If present, will be set from `OptionCode.ip_address_lease_time`, otherwise - /// will be set to the requested address lease time if present in `RequestLease.options`, - /// or otherwise will be set to `clock.Absolute.infinity` to mark that no lease time was - /// issued. - field valid_until: clock.Absolute; //? when does the lease expire - } - - /// Enumerations of how the kernel will handle the received lease of `RequestLease`. - enum AutoUpdateMode : u8 { - /// The kernel will not do anything with the received lease. - /// - /// NOTE: This can be used to perform a DHCP request without directly changing network - /// configuration. - item disabled = 0; - - /// The kernel will only use the `Lease.address` and `Lease.prefix_len` to - /// perform an automatic `link.AddAddress` operation on `RequestLease.interface`. - /// - /// This includes: - /// - Automatic adding/updating of the received IP address to `interface`. - /// - Automatic creation/update of the connected route for `interface`. - item address_only = 1; - - /// In addition to the effects of `address_only`, the kernel will also upsert - /// a route to the network `0.0.0.0/0` through `RequestLease.interface` for - /// all received routers. - /// - /// If multiple routers are advertised, the `Route.priority` will be staged - /// such that the last router has priority 1 and each previous router will - /// have a priority 1 higher. - /// - /// This means that the first advertised router has the highest priority (which is - /// set to the number of routers). - item address_and_route = 2; - - /// In addition to the effects of `address_only`, the kernel will also upsert - /// the received DNS servers associated with `RequestLease.interface`. - item full = 3; - } - - /// Requests new DHCP lease. Completes when at least a single server has successfully - /// provided a DHCP lease or the operation timed out. - /// - /// NOTE: Only a single `RequestLease` or `ReleaseLease` operation can be active per `interface` at the - /// same time. - /// - /// NOTE: The kernel will always use the first successful DHCPACK when multiple options are available. - /// - /// NOTE: When multiple DHCPOFFER messages are received, the kernel will perform the DHCPREQUEST process - /// in parallel for each received option unless a DHCPACK was already received. - async_call RequestLease { - /// The network interface that shall perform the DHCP request. - in interface: link.InterfaceId; - - /// The deadline until which the whole operation has to be completed. - /// - /// LORE: In contrast to many other overlapped operations, a `RequestLease` would potentially - /// never complete and it's an expected outcome that no response is received. - /// Thus, the general rule of "no timeouts in overlapped operations" is broken - /// here on purpose. - in deadline: clock.Absolute; - - /// The unicast address of the DHCP server that shall be used to obtain the DHCP lease. - /// - /// NOTE: When the unspecified address (0.0.0.0) is passed, - /// the kernel will perform a broadcast to discover potential DHCP servers. - in server: IPv4; - - /// Additional options to send with the request. - /// - /// NOTE: The kernel omits any of its own options if `options` contains at least - /// one option with the same `Option.code`. - /// - /// This allows userland to overwrite all potentially kernel-provided options. - /// - /// NOTE: The kernel will copy the options array in `overlapped.schedule`. - /// This means the user can use the memory after successful scheduling. - /// - /// NOTE: The options in this array will be sent in exactly this order *after* - /// the kernel-sent options. - /// - /// This allows userland to emit multiple instances of the same option - /// code (some encodings support this); the kernel transmits as-is. - /// - /// NOTE: The kernel will never merge any values of this array. - in options: []const Option; - - /// If `true`, the kernel will perform a DHCPINFORM process instead of a the regular - /// DHCPDISCOVER/DHCPREQUEST process. - /// - /// This allows querying local network configuration even with a non-DHCP configured IP address. - /// - /// NOTE: This requires that we already have a statically configured IP address. - /// - /// NOTE: If `server` is the unspecified address (`0.0.0.0`), the kernel will send a - /// broadcast message for each configured IPv4 addresses for `interface`. - in inform_only: bool; - - /// Defines how the kernel will update `interface` when the request is successful. - /// - /// NOTE: If the dhcp4 subsystem is enabled, this option is ignored and the kernel will - /// handle the results like it would've performed an automatic/timed DHCP request/renew. - /// - /// This means the operation can be used as a force-refresh the DHCP lease. - /// - /// NOTE: If the dhcp4 subsystem is disabled, setting this option does not imply the - /// kernel will perform automatic renewal or rebinding of the DHCP lease. - in update_mode: AutoUpdateMode; - - /// The lease provided by the server. - /// - /// NOTE: If `inform_only` is `true`, `lease.address` is `0.0.0.0` - /// and `lease.prefix_len` is zero. - out lease: Lease; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// No response was received until `deadline`. - error Timeout; - - /// An invalid value was passed as a parameter. - /// - /// This may be due to: - /// - `server` is neither the unspecified address nor a valid unicast address. - /// - `options` contains an invalid DHCP option. - /// - `deadline` is not in the future. - error InvalidValue; - - /// Another `RequestLease` or `ReleaseLease` operation is in progress for `interface`. - /// - /// NOTE: This error may "spuriously" happen when the dhcp4 subsystem is enabled - /// and currently performs an internal DHCP operation. - error InProgress; - - /// The DHCP server offered an address that is not ours, but reachable - /// through `interface`. - /// - /// NOTE: This error implies the kernel has responeded with DHCPDECLINE. - error AddressConflict; - - /// All available DHCP servers have rejected our request with `DHCPNAK`. - error Rejected; - - /// The kernel does not know a route to `server`. - /// - /// NOTE: This error can only happen when `server` is not unspecified. - error MissingRoute; - - /// The kernel cannot send the DHCPINFORM message as `interface` has no - /// assigned IPv4 address. - error MissingSourceAddress; - - /// The IPv4 subsystem is disabled on `interface`. - error SubsystemDisabled; - - /// `interface` is down and cannot send any data. - error LinkDown; - - /// There was an i/o error that lead to the failure of this operation. - error IoError; - - error SystemResources; - } - - /// Releases current lease, idempotent if none exists. - /// - /// NOTE: Only a single `RequestLease` or `ReleaseLease` operation can be active per `interface` at the - /// same time. - /// - /// NOTE: This operation will implicitly disable the dhcp4 subsystem as otherwise - /// the subsystem would immediatly try to request a new DHCP lease and `ReleaseLease` - /// would be just an implicit `RequestLease` operation. - async_call ReleaseLease { - /// The interface for which a lease should be released. - in interface: link.InterfaceId; - - /// If `true` will remove the kernel-managed lease object even in - /// the case of a communication error. - /// - /// NOTE: Each error code that will still remove the release - /// documents this. - /// - /// NOTE: If `force` is set, the scheduling implicitly cancels an - /// active `RequestLease` operation. - in force: bool; - - /// The deadline until which the whole operation has to be completed. - /// - /// LORE: In contrast to many other overlapped operations, a `RequestLease` would potentially - /// never complete and it's an expected outcome that no response is received. - /// Thus, the general rule of "no timeouts in overlapped operations" is broken - /// here on purpose. - in deadline: clock.Absolute; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The underlying `interface` was removed during this operation. - error Gone; - - /// No response was received until `deadline`. - /// - /// NOTE: The leases was still removed when `force` was set. - error Timeout; - - /// Another `RequestLease` or `ReleaseLease` operation is in progress for `interface`. - /// - /// NOTE: This error cannot happen when `force` is set. - error InProgress; - - /// An invalid value was passed as a parameter. - /// - /// This may be due to: - /// - `deadline` is not in the future. - error InvalidValue; - - /// The IPv4 subsystem is disabled on `interface`. - /// - /// NOTE: This error implies that the interface won't have any lease anyways. - error SubsystemDisabled; - - /// `interface` is down and cannot send any data. - /// - /// NOTE: This error informs the caller that the kernel cannot - /// send the DHCPRELEASE message. - /// - /// NOTE: The leases was still removed when `force` was set. - error LinkDown; - - /// There was an i/o error that lead to the failure of this operation. - /// - /// NOTE: The leases was still removed when `force` was set. - error IoError; - - error SystemResources; - } - - /// Returns current lease or NotAvailable if none exists - syscall get_info { - /// Interface for which the lease shall be queried. - in interface: link.InterfaceId; - - /// The current lease the kernel stores for `interface`. - out info: Lease; - - /// `interface` is not a valid interface id (anymore). - error InvalidInterface; - - /// The kernel currently has no lease associated with `interface`. - error NotAvailable; - - /// The IPv4 subsystem is disabled on `interface`. - error SubsystemDisabled; - } - - //? /// Completes when new lease was set, old lease was released or expired. - //? /// - //? /// NOTE: Multiple `WaitForUpdate` can be issued which will all complete at the - //? /// same time when the status changes. - //? async_call WaitForUpdate { - //? /// The interface for which a new DHCP state shall be awaited. - //? in interface: link.InterfaceId; - - //? /// The new lease or `null` if the lease was removed. - //? out info: ?Lease; - - //? /// `interface` is not a valid interface id (anymore). - //? error InvalidInterface; - - //? /// The underlying `interface` was removed during this operation. - //? error Gone; - - //? error SystemResources; - //? } - } - - namespace dhcp6 { - //? TODO: Design the DHCPv6 kernel API - } - - namespace dns { - //? TODO: Design the DNS kernel API - - //? TODO: Keep in mind that DNS servers can be globally set or associated with an - //? interface. Also DNS servers have a lifetime as well. - } -} - -/// A file or directory on Ashet OS can be named with any legal UTF-8 sequence -/// that does not contain `/` and `:`. It is recommended to only create file names -/// that are actually typeable on the operating system though. -/// -/// File names are measured in bytes (not Unicode codepoints). A single path segment -/// must not exceed `fs.max_file_name_len` bytes. -/// -/// There are some reserved file names: -/// -/// - `.` is the "current directory" selector and does not add to the path. -/// - `..` is the "parent directory" selector and navigates up in the directory hierarchy if possible. -/// -/// Paths: -/// -/// The filesystem kernel API only accepts *relative* paths. -/// -/// A kernel path is a UTF-8 string composed of a sequence of names separated by one or more `/`. -/// -/// Syntax rules: -/// - The empty string `""` is invalid. -/// - Consecutive slashes (regex `/+`) will be compacted into a single `/`. -/// - A leading `/` is invalid. -/// - A trailing `/` is invalid. -/// - `.` is allowed and means "this directory". -/// - `..` is allowed and navigates to the parent directory. At filesystem root, `..` saturates. -/// -/// Here are some examples for valid paths: -/// - `example.txt` -/// - `docs/wiki.txt` -/// - `system/fonts/../config.ini` -/// -/// NOTE: Absolute paths and filesystem designators like `SYS:/foo` are userland concepts. -/// -/// The canonical format for absolute paths in userland is a filesystem name, followed by a `:/`, -/// then a regular path relative to the root directory of the filesystem. -/// -/// Examples: -/// - `SYS:/apps/editor/code` -/// - `USB0:/foo/../bar` (which is equivalent to `USB0:/bar`) -/// -/// -/// NOTE: The filesystem that is used to boot the OS from has an alias `SYS:` that -/// is always a legal way to address this file system. -/// -/// NOTE: Reserved names can never exist as actual directory entries. -/// -/// NOTE: There is a limit on how long a file/directory name can be, but there's no limit -/// on how long a path can be. -/// This means there is no implicit directory nesting limit. -/// -/// NOTE: A file system identifier uses the following rules for names: -/// - Allowed characters: `[A-Z0-9\.]` -/// - Must start with a letter. -/// - Must not end with `.`. -/// -/// Examples: -/// - `SYS` -/// - `USB0.2` -/// - `NFS1` -/// -/// NOTE: Overlapped operations scheduled against the same underlying filesystem object -/// are ordered deterministically (FIFO-equivalent semantics), even across multiple -/// system resources. -/// The kernel may perform safe internal optimizations as long as the observable -/// semantics match the scheduling order. -/// -/// NOTE: Filesystem operations are write-through. On successful completion of an -/// operation, the change is committed to the underlying storage. -namespace fs { - /// The maximum number of bytes in a file system identifier name. - /// - /// This is chosen to be a power of two, and long enough to accommodate - /// typical file system names: - /// - `SYS` - /// - `USB0` - /// - `USB10` - /// - `USB10.3` - /// - `PF0` - /// - `CF7` - const max_fs_name_len = 8; - - /// The maximum number of bytes in a file system type name. - /// - /// Chosen to be a power of two, and long enough to accomodate typical names: - /// - `FAT16` - /// - `FAT32` - /// - `exFAT` - /// - `NTFS` - /// - `ReiserFS` - /// - `ISO 9660` - /// - `btrfs` - /// - `AFFS` - const max_fs_type_len = 32; - - /// The maximum number of bytes in a file name. - /// - /// LORE: This was chosen based off a survey of my local file system - /// and checking what kind of files exists. - /// As some programs use sha256 checksums for file names and 64 bytes - /// are enough to store a hex-encoded 256 bit sequence (`114ac2caf8fefad1116dbfb1bd68429f68e9e088b577c9b3f5a3ff0fe77ec886`), - /// the initial choice was 64 byte. - /// - /// With the invention of Ashet FS, a 64 byte string was possible, but would've wasted - /// 56 bytes of padding space due to two 32 bit pointers inside the same file system node, - /// the limit was raised to 120 characters. - /// - /// With 120 characters, we're settled well for basically all realistic file names - /// encountered in the wild. - const max_file_name_len = 120; - - /// Identifies a mounted filesystem instance known to the kernel. - /// - /// NOTE: File system ids are allocated in a monotonically increasing - /// way, and will be stable until a file system is unmounted/removed - /// from the kernel. - /// - /// NOTE: Except `system`, the enumeration order of file systems is unspecified - /// and the ids cannot be assumed stable between reboots. - enum FileSystemId : u32 { - /// The filesystem the OS booted from. - item system = 0; - - /// All other ids are unique file systems. - ... - } - - /// Enumerates all currently available filesystems. - syscall enumerate_filesystems { - /// If not `null`, will receive filesystem ids. - in list: ?[]FileSystemId; - - /// The number of ids written to `list`, or the total count if `list` is `null`. - out count: usize; - } - - /// Finds a filesystem by name. - /// - /// NOTE: Name matching is case-sensitive. - /// - /// NOTE: The passed `name` may include a trailing `:`. - syscall find_filesystem { - in name: str; - - out id: FileSystemId; - - /// No filesystem exists with the given name. - error NotFound; - - /// `name` does not conform to the name rules for filesystems. - error InvalidName; - } - - struct FileSystemInfo { - /// System-unique id of this file system - field id: FileSystemId; - - /// Compressed infos about the file system - field flags: Flags; - - /// User-addressable file system identifier (e.g. `SYS`, `USB0`, ...). - /// - /// Encoding: - /// - UTF-8 - /// - NUL-padded (first NUL determines length, otherwise full array). - field name: [max_fs_name_len]u8; - - /// String identifier of a file system driver (e.g. `FAT32`, `NFS`, ...) - /// - /// Encoding: - /// - UTF-8 - /// - NUL-padded (first NUL determines length, otherwise full array). - field filesystem: [max_fs_type_len]u8; - - bitstruct Flags : u16 { - /// This is the system boot filesystem. - field system: bool; - - /// The file system can be removed by the user. - field removable: bool; - - /// The filesystem is immutable and cannot be modified. - field immutable: bool; - - reserve u13 = 0; - } - } - - /// Queries information about a filesystem. - syscall get_filesystem_info { - in fs_id: FileSystemId; - out info: FileSystemInfo; - - /// The given filesystem id does not exist. - error InvalidFileSystem; - - error SystemResources; - } - - //? TODO: a way to query free space / capacity (if you want userland UIs to show disk usage without filesystem-specific driver calls) - - /// A directory is a group of files and other directories in a file system. - resource Directory { } - - /// Opens a directory relative to the root of a filesystem. - async_call Mount { - in fs_id: FileSystemId; - - /// The directory path relative to the root of the filesystem. - /// - /// NOTE: Passing `"."` yields the root directory handle. - in path: str; - - out dir: Directory; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The requested entry does not exist. - error FileNotFound; - - /// The given filesystem id did not exist when scheduling the operation. - error InvalidFileSystem; - - /// The given `path` is syntactically invalid. - error InvalidPath; - - /// The requested entry exists but is not a directory. - error NotADir; - - /// The underlying filesystem of `fs_id` was removed - /// during the creation of the directory. - error Gone; - - error SystemResources; - } - - /// Opens a directory relative to `start_dir`. - async_call OpenDir { - in start_dir: Directory; - - /// The directory path relative to `start_dir`. - in path: str; - - out dir: Directory; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The requested entry does not exist. - error FileNotFound; - - /// `start_dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `start_dir` was removed. - error Gone; - - /// The given `path` is syntactically invalid. - error InvalidPath; - - /// The requested entry exists but is not a directory. - error NotADir; - - error SystemResources; - } - - /// Writes the name of a directory into `*name_out`. - /// - /// NOTE: For the filesystem root directory, the returned name is `"."`. - /// - /// NOTE: This syscall does not require filesystem I/O. - syscall get_dir_name { - in dir: Directory; - in name_out: *FileName; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - } - - /// Returns the filesystem id a directory resides on. - /// - /// NOTE: This syscall does not require filesystem I/O. - syscall get_dir_filesystem { - in dir: Directory; - out fs_id: FileSystemId; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - } - - /// Returns true if `dir` is the root directory of its filesystem. - /// - /// NOTE: This syscall does not require filesystem I/O. - syscall is_root_dir { - in dir: Directory; - out is_root: bool; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - } - - /// A directory enumerator allows enumerating directory entries. - /// - /// Enumeration properties: - /// - The enumeration order is unspecified. - /// - The enumerator is not rewindable/resettable. - /// - The enumerator is not a snapshot. - /// - /// Determinism rule: - /// - Enumeration is deterministic under scheduling. - /// - The kernel tracks mutations and enumerators to ensure an entry is never returned twice. - /// - An enumerator may over-enumerate (return entries that are deleted later), - /// but must never under-enumerate (skip entries that exist). - resource DirEnumerator { } - - /// Information about a filesystem entry (file or directory). - /// - /// NOTE: This structure intentionally does not include the entry name. - /// APIs that enumerate directory items accept an optional `FileName*` - /// output for the name. - struct FileInfo { - /// The size in bytes. - /// - /// NOTE: For directories, this value is always zero. - field size: u64; - - /// Timestamp of the creation time of the file. - /// - /// NOTE: Only valid when `flags.creation_date_valid` is true. - field creation_date: datetime.DateTime; - - /// Timestamp of the last modification time of the file. - /// - /// NOTE: Only valid when `flags.modified_date_valid` is true. - field modified_date: datetime.DateTime; - - /// Additional packed information. - field flags: Flags; - - enum FileType : u2 { - item file = 0; - item directory = 1; - } - - bitstruct Flags : u16 { - /// Entry type. - field type: FileType; - - /// `creation_date` is valid. - field creation_date_valid: bool; - - /// `modified_date` is valid. - field modified_date_valid: bool; - - reserve u12 = 0; - } - } - - /// A fixed-size file name buffer. - /// - /// The file name bytes are stored in `bytes[0..len]`. - /// Bytes beyond `len` are unspecified. - /// - /// NOTE: `len` is always `<= max_file_name_len`. - struct FileName { - field len: u8; - field bytes: [max_file_name_len]u8; - } - - /// Creates a directory enumerator for `dir`. - syscall create_enumerator { - in dir: Directory; - - out enumerator: DirEnumerator; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - - error SystemResources; - } - - /// Returns information about the next entry in a directory enumeration. - /// - /// If `name_out` is not `null`, the kernel writes the entry name into `*name_out`. - /// The `name_out` pointer must remain valid until the operation completes. - async_call GetNextDirItem { - in enumerator: DirEnumerator; - - /// Optional output buffer for the entry name. - in name_out: ?*FileName; - - /// The information about the directory entry. - out info: FileInfo; - - /// Returned when the enumerator reached the end of the directory. - /// - /// NOTE: The `enumerator` resource should be destroyed after this error is - /// returned as there is no way to unwind an enumerator resource. - error EndOfDirectory; - - /// `enumerator` is not a valid enumerator resource. - error InvalidHandle; - - /// The underlying filesystem of `enumerator` was removed. - error Gone; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - error SystemResources; - } - - /// Deletes a filesystem entry by path. - /// - /// Active handle rule: - /// - If the target (or any descendant when `recurse=true`) has an active handle - /// (`File`, `Directory`, `Location`, or `DirEnumerator`), the operation fails with `ActiveHandle`. - async_call Delete { - in dir: Directory; - - /// The path relative to `dir`, points to the file system entry that shall - /// be deleted. - in path: str; - - /// Defines if the operation should recursively delete a directory if - /// `path` points to a directory resource. - /// - /// - `false`: Deleting a non-empty directory fails with `DirectoryNotEmpty`. - /// - `true`: Directories are deleted recursively. - in recurse: bool; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The requested entry does not exist. - error FileNotFound; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - - /// The given `path` is syntactically invalid. - error InvalidPath; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - /// The target is a non-empty directory and `recurse` is false. - error DirectoryNotEmpty; - - /// The operation conflicts with existing active handles. - error ActiveHandle; - - /// The filesystem is immutable and cannot be modified. - error ImmutableFileSystem; - - error SystemResources; - } - - /// Creates a new directory relative to `dir`. - async_call MkDir { - in dir: Directory; - - /// A path relative to `dir` which points to the directory that shall be created. - /// - /// NOTE: If `path` contains subdirectories, missing intermediate directories are created. - in path: str; - - /// If `true`, the operation will return a directory handle. - in mkopen: bool; - - /// Optional handle to the created directory. - /// - /// If `mkopen` is true, `new_dir` receives an opened handle to the created directory. - /// If `mkopen` is false, `new_dir` is returned as `null`. - out new_dir: ?Directory; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The target path was expected to be non-existent, but an entry exists. - error Exists; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - - /// The given `path` is syntactically invalid. - error InvalidPath; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - /// There is not enough free space on the filesystem. - error NoSpaceLeft; - - /// The filesystem is immutable and cannot be modified. - error ImmutableFileSystem; - - error SystemResources; - } - - /// Queries a filesystem entry by path. - async_call StatEntry { - in dir: Directory; - in path: str; - out info: FileInfo; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The requested entry does not exist. - error FileNotFound; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - - /// The given `path` is syntactically invalid. - error InvalidPath; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - error SystemResources; - } - - /// Renames or moves an entry within the same filesystem. - /// - /// NOTE: This operation is logically atomic under scheduling: - /// operations scheduled after it observe the new name/location; - /// operations scheduled before it observe the old name/location. - /// - /// NOTE: This is a cheap operation and does not require the copying of data. - /// - /// NOTE: This operation does not support replacing an existing destination. - async_call NearMove { - /// The directory defining the base for both the source - /// and destination of the rename operation. - in dir: Directory; - - /// Path relative to `dir` that points to the current file or directory name. - in src_path: str; - - /// Path relative to `dir` that points to the new file or directory name. - in dst_path: str; - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The destination path was expected to be non-existent, but an entry exists. - error Exists; - - /// The source entry does not exist. - error FileNotFound; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - - /// A passed path is syntactically invalid. - error InvalidPath; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - /// There is not enough free space on the filesystem (e.g. directory entry allocation). - error NoSpaceLeft; - - /// The filesystem is immutable and cannot be modified. - error ImmutableFileSystem; - - error SystemResources; - } - - - /// Moves an entry between unrelated directories. - /// - /// Atomicity: - /// - The move is all-or-nothing unless an `IoError` prevents full atomicity. - /// - On failures like `NoSpaceLeft`, the kernel attempts to roll back changes. - /// - /// Active handle rule: - /// - The kernel checks for `ActiveHandle` recursively before starting the move. - /// - While the operation is active, moved entries are treated as not visible at the source. - /// - The destination becomes visible only on successful completion. - /// - /// NOTE: This operation can move between different filesystems and may copy data. - /// - /// NOTE: If `src_dir` and `dst_dir` are in the same file system, the kernel may perform - /// the move operation with the efficiency of `NearMove`, but the `ActiveHandle` checks - /// are still performed. - async_call FarMove { - /// The directory that defines the source file system. - in src_dir: Directory; - - /// Path relative to `src_dir` that defines which entry should be moved. - in src_path: str; - - /// The directory that defines the destination file system. - in dst_dir: Directory; - - /// Path relative to `dst_dir` that defines where the entry should be moved. - in dst_path: str; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The destination path was expected to be non-existent, but an entry exists. - error Exists; - - /// The source entry does not exist. - error FileNotFound; - - /// One of the directory handles is invalid. - error InvalidHandle; - - /// The underlying filesystem of a passed handle was removed. - error Gone; - - /// A passed path is syntactically invalid. - error InvalidPath; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - /// The operation conflicts with existing active handles. - error ActiveHandle; - - /// There is not enough free space on the destination filesystem. - error NoSpaceLeft; - - /// A involved filesystem is immutable and cannot be modified. - error ImmutableFileSystem; - - error SystemResources; - } - - /// Copies an entry between unrelated directories. - /// - /// NOTE: This operation can copy between different filesystems. - /// - /// Atomicity: - /// - The copy is all-or-nothing unless an `IoError` prevents full atomicity. - /// - On failures like `NoSpaceLeft`, the kernel attempts to roll back changes. - async_call Copy { - /// The directory that defines the base of `src_path`. - in src_dir: Directory; - - /// A path relative to `src_dir` that defines the entry which shall be copied. - in src_path: str; - - /// The directory that defines the base of `dst_path`. - in dst_dir: Directory; - - /// A path relative to `dst_dir` that defines the target of the copy operation. - in dst_path: str; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The destination path was expected to be non-existent, but an entry exists. - error Exists; - - /// The source entry does not exist. - error FileNotFound; - - /// One of the directory handles is invalid. - error InvalidHandle; - - /// The underlying filesystem of a passed handle was removed. - error Gone; - - /// A passed path is syntactically invalid. - error InvalidPath; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - /// There is not enough free space on the destination filesystem. - error NoSpaceLeft; - - /// A involved filesystem is immutable and cannot be modified. - error ImmutableFileSystem; - - error SystemResources; - } - - /// A file is a handle to a binary data storage that is stored - /// in a file system. - /// - /// NOTE: Files are byte-addressed and accessed by explicit offsets. - resource File { } - - enum FileAccess : u8 { - item read_only = 0; - item write_only = 1; - item read_write = 2; - } - - enum FileMode : u8 { - /// Opens file when it exists on disk - item open_existing = 0; - - /// Creates file when it does not exist, or opens the file without truncation. - item open_always = 1; - - /// Creates file when there is no file with that name - item create_new = 2; - - /// Creates file when it does not exist, or opens the file and truncates it to zero length - item create_always = 3; - } - - /// Opens a file relative to `dir`. - /// - /// NOTE: Opening a directory path as a file fails with `NotAFile`. - async_call OpenFile { - in dir: Directory; - in path: str; - - /// Defines what access to the file is desired. - in access: FileAccess; - - /// Defines how the open operation should handle non-existing/existing files. - in mode: FileMode; - - out handle: File; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The requested entry does not exist. - error FileNotFound; - - /// The target path was expected to be non-existent, but an entry exists. - error Exists; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - - /// The given `path` is syntactically invalid. - error InvalidPath; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - /// The requested entry exists but is not a file. - error NotAFile; - - /// There is not enough free space on the filesystem. - error NoSpaceLeft; - - /// The filesystem is immutable and cannot be modified. - /// - /// NOTE: This error is only returned when `access` requests write access. - error ImmutableFileSystem; - - error SystemResources; - } - - /// Writes the basename of a file into `*name_out`. - /// - /// NOTE: This syscall does not require filesystem I/O. - syscall get_file_name { - in file: File; - - in name_out: *FileName; - - /// `file` is not a valid file resource. - error InvalidHandle; - - /// The underlying filesystem of `file` was removed. - error Gone; - } - - /// Reads data from a file at `offset` into `buffer`. - /// - /// NOTE: Multiple `Read` and `Write` operations can be scheduled at the same time - /// and may complete concurrently. - async_call Read { - in file: File; - - /// The offset of the read operation in bytes from the beginning of the file. - in offset: u64; - - /// The buffer which shall receive the read data. - /// NOTE: `buffer` must stay valid until the operation completes. - in buffer: bytebuf; - - /// The number of bytes written to `buffer`. - /// - /// NOTE: This is only ever less than `buffer.len` if the - /// read operation would read over the end of the file. - /// - /// If that is the case, `count` is computed as: - /// `count = file_size -| offset`. - out count: usize; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// `file` is not a valid file resource. - error InvalidHandle; - - /// The underlying filesystem of `file` was removed. - error Gone; - - error SystemResources; - } - - /// Writes data to a file at `offset` from `buffer`. - /// - /// NOTE: Writes never extend a file. File growth is only possible via `Resize`. - /// - /// NOTE: Multiple `Read` and `Write` operations can be scheduled at the same time - /// and may complete concurrently. - async_call Write { - in file: File; - - /// The offset of the write operation in bytes from the beginning of the file. - in offset: u64; - - /// The data that shall be written to the file. - in buffer: bytestr; - - /// The number of bytes written to file. - /// - /// NOTE: This is only ever less than `buffer.len` if the - /// write operation would write over the end of the file. - /// - /// If that is the case, `count` is computed as: - /// `count = file_size -| offset`. - out count: usize; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// `file` is not a valid file resource. - error InvalidHandle; - - /// The underlying filesystem of `file` was removed. - error Gone; - - /// The file handle does not permit writing. - error WriteProtected; - - /// The filesystem is immutable and cannot be modified. - error ImmutableFileSystem; - - error SystemResources; - } - - /// Queries information about an opened file. - async_call StatFile { - in file: File; - out info: FileInfo; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// `file` is not a valid file resource. - error InvalidHandle; - - /// The underlying filesystem of `file` was removed. - error Gone; - - error SystemResources; - } - - /// Resizes a file to `length` bytes. - /// - /// Growth properties: - /// - The filesystem must physically allocate storage (no sparse/overcommitted growth). - /// - Newly allocated bytes are zero-filled. - /// - /// Shrink properties: - /// - Shrinking is immediate and permanent. - /// - If the file is grown again later, the new bytes are zero. - /// - /// NOTE: Can be also used to truncate a file to zero length. - async_call Resize { - in file: File; - in length: u64; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// `file` is not a valid file resource. - error InvalidHandle; - - /// The underlying filesystem of `file` was removed. - error Gone; - - /// There is not enough free space on the filesystem. - error NoSpaceLeft; - - /// The file handle does not permit writing. - error WriteProtected; - - /// The filesystem is immutable and cannot be modified. - error ImmutableFileSystem; - - error SystemResources; - } - - /// A filesystem location is a `Directory` plus an associated relative path. - /// - /// This resource is used to transport a filesystem location across process boundaries, - /// including potentially non-existent targets (e.g. the output parameter inside Process arguments). - /// - /// A location can be opened/used similar to how a `(dir, filename)` pair - /// can be used. - /// - /// LORE: This type was introduced as a solution on how to pass file names - /// over a command line interface into an application. - /// As Ashet OS prefers relative paths to known directory handles over - /// absolute paths, a shell still needs the ability to pass non-existing - /// locations for parameters like `--output=…`. - /// Thus, the `Location` type was introduced which fuses a directory together - /// with a relative path. - resource Location { } - - /// Specifies how a `Location` should be interpreted by consumers. - enum LocationIntent : u8 { - /// The location may refer to a file or a directory. - item any = 0; - - /// The location should be treated as a file. - item file = 1; - - /// The location should be treated as a directory. - item directory = 2; - } - - /// Creates a new `Location` from `dir` and `path`. - /// - /// The stored path is normalized and syntactically resolved. - /// - /// NOTE: The path stored in a `Location` is normalized and syntactically resolved: - /// - Repeated separators are collapsed. - /// - `.` components are removed. - /// - `..` cancels a preceding non-`..` component (`x/../y` becomes `y`). - /// - If the resolved path would be empty, it is stored as `"."`. - /// - /// This is purely syntactical processing and does not touch the filesystem. - syscall create_location { - /// The directory that is the base of our location. - in dir: Directory; - - /// The path relative to `dir` which the location describes. - in path: str; - - /// The intent for the file system location. - /// - /// NOTE: This can be queried with `get_location_intent`. - in intent: LocationIntent; - - out loc: Location; - - /// `dir` is not a valid directory resource. - error InvalidHandle; - - /// The underlying filesystem of `dir` was removed. - error Gone; - - /// The given `path` is syntactically invalid. - error InvalidPath; - - error SystemResources; - } - - /// Returns the intent stored inside a `Location`. - syscall get_location_intent { - in loc: Location; - out intent: LocationIntent; - - /// `loc` is not a valid location resource. - error InvalidHandle; - - /// The underlying filesystem of `loc` was removed. - error Gone; - } - - /// Returns a clone of the base directory stored in a `Location`. - syscall get_location_dir { - in loc: Location; - out dir: Directory; - - /// `loc` is not a valid location resource. - error InvalidHandle; - - /// The underlying filesystem of `loc` was removed. - error Gone; - - error SystemResources; - } - - /// Returns the normalized, resolved path stored in a `Location`. - /// - /// NOTE: The returned string remains valid as long as `loc` is not destroyed. - syscall get_location_path { - in loc: Location; - out path: str; - - /// `loc` is not a valid location resource. - error InvalidHandle; - - /// The underlying filesystem of `loc` was removed. - error Gone; - } - - /// Opens the `Location` as a directory. - /// - /// NOTE: This is the `Location` variant of `OpenDir`. - async_call OpenAsDirectory { - in loc: Location; - out dir: Directory; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The requested entry does not exist. - error FileNotFound; - - /// `loc` is not a valid location resource. - error InvalidHandle; - - /// The underlying filesystem of `loc` was removed. - error Gone; - - /// The requested entry exists but is not a directory. - error NotADir; - - error SystemResources; - } - - /// Opens the `Location` as a file. - /// - /// NOTE: This is the `Location` variant of `OpenFile`. - async_call OpenAsFile { - in loc: Location; - in access: FileAccess; - in mode: FileMode; - out file: File; - - /// The underlying storage subsystem had an I/O failure. - error IoError; - - /// The requested entry does not exist. - error FileNotFound; - - /// The target path was expected to be non-existent, but an entry exists. - error Exists; - - /// `loc` is not a valid location resource. - error InvalidHandle; - - /// The underlying filesystem of `loc` was removed. - error Gone; - - /// The requested entry exists but is not a file. - error NotAFile; - - /// A path traversal expected a directory, but found a non-directory entry. - error NotADir; - - /// There is not enough free space on the filesystem. - error NoSpaceLeft; - - /// The filesystem is immutable and cannot be modified. - /// - /// NOTE: This error is only returned when `access` requests write access. - error ImmutableFileSystem; - - error SystemResources; - } -} - -/// This namespace contains items related to shared memory objects. -namespace shm { - /// A shared memory object which, for its livetime, provides - /// a memory region which can be read and modified. - /// - /// NOTE: The memory region is valid until the resource is destroyed. - resource SharedMemory { } - - /// Constructs a new shared memory object with `size` bytes of memory. - /// Shared memory 1can be written without any memory protection. - /// - /// NOTE: The shared memory region will not be initialized by the kernel - /// so the content after creation is unspecified. - /// It should be set to the desired contents by the initial creator. - syscall create { - /// Number of bytes for the shared memory region. - /// The operation will fail when `0` is passed. - in size: usize; - - /// The created shared memory object. - out handle: SharedMemory; - - /// Returned when `size` is 0. - error InvalidSize; - - error SystemResources; - } - - /// Returns the memory region for the shared memory object. - /// - /// NOTE: The memory returned by this function is valid until the `handle` object is destroyed. - syscall get_memory { - in handle: SharedMemory; - - /// The memory region of the shared memory object. - out memory: []align(16) u8; - - /// The `handle` is not a valid shared memory object. - error InvalidHandle; - } -} - -/// This namespace contains items related to data pipes. -namespace pipe { - - /// A pipe is a two-ended, one-directional communication - /// channel which can either transport data streams or packets. - /// - /// Pipes can be synchronous or buffered: - /// - A synchronous pipe can only transfer data if a `Read` and a `Write` are active - /// at the same time. - /// - A buffered pipe has an internal memory which can store some elements - /// and makes `Read` and `Write` independent of each other. - /// - /// NOTE: Pipes will never transfer partial elements. - /// - /// NOTE: If multiple `Read` and `Write` operations are scheduled, the kernel - /// will process them in a FIFO manner. - /// This means that no interleaving between multiple `Write` operations will happen. - /// - /// NOTE: If a pipe is synchronous, a `Read` or `Write` operation can only complete - /// when a concurrent opposite operation is active. - /// The kernel will transfer the data directly from a `Write` operation into the - /// buffer of a `Read` operation without storing elements in kernel memory. - /// - /// NOTE: If a pipe is synchronous, and a `Read` uses `PipeMode.at_least_one`, it will - /// consume the maximum possible amount of elements from a single `Write`, but will - /// not merge data from multiple `Write` operations. - /// - /// NOTE: If a pipe is buffered, and a `Read` operation uses `PipeMode.at_least_one`, the - /// operation will consume a maximum of `fifo_length` elements, even if `Read.buffer` - /// could store more elements. - /// - /// NOTE: The `PipeMode` of a `Read` or `Write` operation only affects the operation itself - /// and will never affect other concurrently scheduled operations. - resource Pipe { } - - /// Creates a new pipe with `fifo_length` elements of `element_size` bytes. - /// If `fifo_length` is 0, the pipe is synchronous and can only send data - /// if a `Read` call is active. Otherwise, up to `fifo_length` elements can be - /// stored in a FIFO. - syscall create { - /// The size of the primitives in bytes the pipe operates on. Each element - /// transferred by the pipe has this size. - /// - /// NOTE: An elements size of 1 is making the pipe byte-oriented. - /// This can be mentally seen as data streaming instead of - /// packet oriented transmission. - /// - /// NOTE: An elements size of 0 is illegal and returns an error. - in element_size: usize; - - /// The number of elements that can be buffered inside the pipe before - /// making `Write` blocking. - /// - /// Passing 0 here makes the pipe a synchronous pipe, - /// any other value makes the pipe buffered. - in fifo_length: usize; - - /// The newly created pipe resource. - out handle: Pipe; - - error SystemResources; - - /// Returned when `element_size == 0`. - error InvalidSize; - } - - /// Returns the length of the pipe-internal FIFO in elements. - syscall get_fifo_length { - /// The pipe which should be queried. - in handle: Pipe; - - /// The length of the FIFO in elements. - out length: usize; - - /// `handle` is not a valid pipe resource. - error InvalidHandle; - } - - /// Returns the size of the elements stored in the pipe. - syscall get_element_size { - /// The pipe which should be queried. - in handle: Pipe; - - /// The size of the elements in bytes. - out size: usize; - - /// `handle` is not a valid pipe resource. - error InvalidHandle; - } - - enum PipeMode : u8 { - /// Completes immediately even if no elements could be processed. - /// NOTE: This means that `Read.count` or `Write.count` can be zero after completion. - item nonblocking = 0; - - /// Returns when at least one element could be processed. - /// NOTE: This means that `Read.count` or `Write.count` is at least one after completion - /// unless `data` or `buffer` do not hold a single element. - item at_least_one = 1; - - /// Returns only when all elements are processed. - /// NOTE: This means that `Read.count` or `Write.count` are at the maximum possible value - /// derived from `stride` and `data.len`/`buffer.len`. - item all = 2; - } - - /// Writes elements from `data` into the given pipe. - /// - /// NOTE: The number of elements inside `data` is computed by `(data.len - element_size + 1) / stride`. - async_call Write { - in handle: Pipe; - - /// Pointer to the first element. Length defines how many elements are to be transferred. - /// - /// NOTE: If `data.len < element_size`, the operation transfers 0 elements and completes - /// immediately. - in data: bytestr; - - /// Distance in bytes between each element in `data`. Can be different from the pipe's element - /// size to allow sparse data to be transferred. - /// - /// NOTE: If `0` is passed, `stride` will be set to the `element_size` property of the pipe. - /// - /// NOTE: It is legal to pass a `stride` smaller than `element_size`. This will copy elements - /// which are overlapping. - in stride: usize; - - /// Defines how the write should operate. - in mode: PipeMode; - - /// Number of elements written into the pipe. - out count: usize; - - /// `handle` is not a valid pipe resource. - error InvalidHandle; - } - - /// Reads elements from a pipe into `buffer`. - /// - /// NOTE: The max. number of elements written to `buffer` is computed by `(buffer.len - element_size + 1) / stride`. - async_call Read { - in handle: Pipe; - - /// Points to the first element to be received. - /// - /// NOTE: The kernel will only write chunks of `element_size` in steps of `stride` bytes. - /// It will not write any other part of the buffer. - /// - /// NOTE: `BufferSize` is returned if `buffer.len < element_size`. - in buffer: bytebuf; - - /// Distance between each element in `buffer`. Can be different from the pipe's element size - /// to allow sparse data to be transferred. - /// - /// NOTE: If `0` is passed, `stride` will be set to the `element_size` property of the pipe. - /// - /// NOTE: It is legal to pass a `stride` smaller than `element_size`. This will write elements - /// which are overlapping inside `buffer`. If this is the case, only the last element - /// written is complete. - in stride: usize; - - /// Defines how the read should operate. - in mode: PipeMode; - - /// Number of elements written to `buffer`. - out count: usize; - - /// `handle` is not a valid pipe resource. - error InvalidHandle; - - /// `buffer.len` is smaller than `element_size`. - error BufferSize; - } -} - -/// This namespace contains items related to synchronization between multiple threads. -namespace sync { - /// A mutex implements an object which can be locked and unlocked. - /// - /// NOTE: As Ashet OS is cooperatively scheduled, it is not necessary to guard an access/operation - /// with a mutex without a scheduler yield. - /// This means using a mutex is only sensible when access to a certain resource should be guarded - /// over scheduler yield points. - /// - /// LORE: In contrast to most other operating systems, a mutex in Ashet OS isn't tied - /// to a thread or a process, but is a regular system resource that can be passed - /// around and can be shared between several processes and threads. - /// - /// This means that the concept of a "recursive mutex" doesn't make sense, as a mutex - /// has no knowledge of the locking thread. - resource Mutex { } - - /// Creates a new mutex. - syscall create_mutex { - out mutex: Mutex; - - error SystemResources; - } - - /// Tries to lock a mutex and returns if it was successful. - syscall try_lock { - /// The mutex that shall be locked. - in mutex: Mutex; - - /// `true` if the lock was successful, `false` otherwise. - out is_locked: bool; - - /// `mutex` is not a valid mutex resource. - error InvalidHandle; - } - - /// Unlocks a mutex. - /// - /// Completes the oldest pending `Lock` operation if one exists. - syscall unlock { - in mutex: Mutex; - - /// `mutex` is not a valid mutex resource. - error InvalidHandle; - - //? TODO: Consider "NotLocked" error to make it possible to detect - //? programming errors - } - - /// Locks a mutex. Will complete once the mutex is locked. - async_call Lock { - /// The mutex that shall be locked. - in mutex: Mutex; - - /// `mutex` is not a valid mutex resource. - error InvalidHandle; - } - - /// A sync-event is an edge-triggered notification mechanism that - /// can synchronize multiple actors. - resource SyncEvent { } - - /// Creates a new `SyncEvent` object that can be used to synchronize - /// different processes. - syscall create_event { - /// The created SyncEvent resource. - out event: SyncEvent; - error SystemResources; - } - - /// Completes the oldest pending `WaitForEvent` operation waiting for the given event. - /// - /// NOTE: If currently no `WaitForEvent` operation is pending on `event`, - /// the notification is lost. - syscall notify_one { - /// The event that shall be notified. - in event: SyncEvent; - - /// `true` if the notification completed a pending `WaitForEvent` operation, otherwise `false`. - out received: bool; - - /// `event` is not a valid sync event. - error InvalidHandle; - } - - /// Completes all `WaitForEvent` operations waiting for the given event. - /// - /// NOTE: If currently no `WaitForEvent` operation is pending on `event`, - /// the notification is lost. - syscall notify_all { - /// The event that shall be notified. - in event: SyncEvent; - - /// The number of completed `WaitForEvent` operations. - /// - /// NOTE: If `received == 0`, the notification was lost. - out received: usize; - - /// `event` is not a valid sync event. - error InvalidHandle; - } - - /// Waits for the given `SyncEvent` to be notified. - async_call WaitForEvent { - /// The event which shall be awaited. - in event: SyncEvent; - - /// `event` is not a valid sync event. - error InvalidHandle; - } -} - -/// This namespace contains items related to graphics rendering. -namespace draw { - /// A font is required to render text and defines how - /// glyphs are drawn. - resource Font { } - - /// A framebuffer is something that can be drawn on. - resource Framebuffer { } - - enum FramebufferType : u8 { - /// A pure in-memory frame buffer used for off-screen rendering. - item memory = 0; - - /// A video device backed frame buffer. Can be used to paint on a screen - /// directly. - item video = 1; - - /// A frame buffer provided by a window. These frame buffers - /// may hold additional semantic information. - item window = 2; - - /// A frame buffer provided by a user interface element. These frame buffers - /// may hold additional semantic information. - item widget = 3; - } - - /// Returns the font for the given font name, if any. - /// - /// NOTE: System fonts are fonts that are either embedded in the kernel or - /// automatically loaded from the `SYS:/system/fonts` folder on - /// boot. - /// - /// NOTE: The returned resource can be unbound, but cannot be destroyed. - /// A `resources.destroy` operation will unbind the font resource from all - /// processes, effectively invalidating this userland handle. - /// - /// The underlying kernel resource won't be destroyed. - syscall get_system_font { - /// The name of the system font. - in font_name: str; - - //? TODO: Add a way to hint font sizes for vector fonts. - - /// The resource handle of the system font. - out handle: Font; - - /// No system font with the given name exists. - error FileNotFound; - - error SystemResources; - } - - /// Creates a new custom font from the given data. - syscall create_font { - /// The encoded font data for a bitmap or vector format. - /// - /// TODO: Specify which font formats are allowed. - in data: bytestr; - - //? TODO: Add a way to hint font sizes for vector fonts. - - /// A font resource that represents the font inside `data`. - out handle: Font; - - /// `data` does not encode a valid font. - error InvalidData; - - error SystemResources; - } - - /// Returns true if the given font is a system-owned font. - syscall is_system_font { - in font: Font; - - /// `true` if `font` is a system font resource, otherwise `false`. - out system_font: bool; - - /// `font` is not a valid font resource. - error InvalidHandle; - } - - /// Measures the size of a text string. - /// - /// NOTE: This function accepts strings using the LF line separator - /// and will return the height of all lines and the width of - /// the longest line. - syscall measure_text_size { - in font: Font; - in text: str; - out size: Size; - - /// `font` is not a valid font resource. - error InvalidHandle; - } - - /// Creates a new in-memory framebuffer that can be used for off-screen painting. - /// - /// NOTE: The contents of the newly created framebuffer are unspecified. - syscall create_memory_framebuffer { - /// The size of the created framebuffer in pixels. - in size: Size; - - out handle: Framebuffer; - - /// Returned when `size.width` or `size.height` are zero. - error InvalidSize; - - error SystemResources; - } - - /// Creates a new framebuffer based off a video output. Can be used to output pixels - /// to the screen. - /// - /// NOTE: The returned `handle` is destroyed automatically when `output` - /// is destroyed. - syscall create_video_framebuffer { - in output: video.VideoOutput; - out handle: Framebuffer; - - /// `output` is not a valid video output resource. - error InvalidHandle; - - error SystemResources; - } - - /// Creates a new framebuffer that allows painting into a GUI window. - /// - /// NOTE: The returned `handle` is destroyed automatically when `window` - /// is destroyed. - syscall create_window_framebuffer { - in window: gui.Window; - out handle: Framebuffer; - - /// `window` is not a valid window resource. - error InvalidHandle; - - error SystemResources; - } - - /// Creates a new framebuffer that allows painting into a widget. - /// - /// NOTE: The returned `handle` is destroyed automatically when `widget` - /// is destroyed. - syscall create_widget_framebuffer { - in widget: gui.Widget; - out handle: Framebuffer; - - /// `widget` is not a valid widget resource. - error InvalidHandle; - - error SystemResources; - } - - /// Returns the type of a framebuffer object. - syscall get_framebuffer_type { - in fb: Framebuffer; - - /// The type of framebuffer `fb` is. - out type: FramebufferType; - - /// `fb` is not a valid framebuffer resource. - error InvalidHandle; - } - - /// Returns the size of a framebuffer object. - syscall get_framebuffer_size { - in fb: Framebuffer; - - /// The size of the framebuffer in pixels. - out size: Size; - - /// `fb` is not a valid framebuffer resource. - error InvalidHandle; - } - - /// Returns the video memory for a memory framebuffer. - /// - /// NOTE: The returned memory is stable and valid until the `fb` is destroyed. - /// - /// NOTE: Any framebuffer except memory framebuffers cannot have - /// memory mappings. - syscall get_framebuffer_memory { - in fb: Framebuffer; - - /// The descriptor of the pixel memory that forms the contents of `fb`. - out memory: video.VideoMemory; - - /// `fb` is not a valid framebuffer resource. - error InvalidHandle; - - /// `fb` is not a framebuffer created with `create_memory_framebuffer`. - error Unsupported; - } - - /// Marks a portion of the framebuffer as changed and forces the OS to - /// perform an update action if necessary. - syscall invalidate_framebuffer { - in fb: Framebuffer; - - /// The area of the framebuffer that has changed. - /// - /// NOTE: `area` is limited to the actual bounds of the framebuffer. - /// - /// NOTE: If `area.width` or `area.height` are zero, nothing will be invalidated. - in area: Rectangle; - - /// `fb` is not a valid framebuffer resource. - error InvalidHandle; - } - - /// Renders the provided Ashet Graphics Protocol `sequence` into `target` framebuffer. - /// - /// The operation will complete when rendering is done. - /// - /// NOTE: On machines without hardware acceleration, this operation might be - /// completed synchronously. - async_call Render { - /// The framebuffer which should be drawn to. - in target: Framebuffer; - - /// The AGP code that defines the drawing. - /// - /// NOTE: The kernel will validate the code inside `overlapped.schedule` and - /// immediately complete the operation with `BadCode` if `sequence` - /// is not a valid AGP command sequence. - /// - /// NOTE: The kernel will create an ephemeral copy of the code inside `overlapped.schedule` - /// if the operation will not be completed immediately. - in sequence: bytestr; - - /// If the target framebuffer is invalidatable, it is automatically invalidated after the completion - /// of the command sequence, ensuring presentation of the contents. - /// - /// This is useful when painting into widgets or windows to ensure the window manager - /// actually sees the changes as soon as they are done, reducing graphics pipeline latency. - in auto_invalidate: bool; - - /// `sequence` is not a valid AGP command sequence. - error BadCode; - - /// `target` is not a valid framebuffer resource. - error InvalidHandle; - } -} - -//? TODO: Review this namespace. -namespace gui { - resource Window { } - - resource Widget { } - - resource Desktop { } - - resource WidgetType { } - - enum NotificationSeverity : u8 { - /// Important information that require immediate action - /// by the user. - /// - /// This should be handled with care and only for reall - /// urgent situations like low battery power or - /// unsufficient disk memory. - item attention = 0; - - /// This is a regular user notification, which should be used - /// sparingly. - /// - /// Typical notifications of this kind are in the category of - /// "download completed", "video fully rendered" or similar. - item information = 128; - - /// Silent notifications that might be informational, but do not - /// require attention by the user at all. - item whisper = 255; - - ... - } - - enum MessageBoxIcon : u8 { - item information = 0; - item question = 1; - item warning = 2; - item @"error" = 3; - } - - bitstruct WindowFlags : u32 { - field popup: bool; - field resizable: bool; - reserve u30 = 0; - } - - bitstruct CreateWindowFlags : u32 { - field popup: bool = false; - reserve u31 = 0; - } - - typedef WidgetEventHandler = fnptr (WidgetType, Widget, *const WidgetEvent) void; - - struct WidgetDescriptor { - field uuid: UUID; - - /// Number of bytes allocated in a Widget for this widget type. - /// See @`gui.get_widget_data` function for further information. - field data_size: usize; - - field flags: Flags; - - //? TODO: Fill this out - - //? Event Handlers: - - field handle_event: WidgetEventHandler; - - bitstruct Flags : u32 { - /// If `true`, the user can focus this widget with the mouse or keyboard. - field focusable: bool; - - /// If `true`, the user is able to open a context menu on this. - field context_menu: bool; - - /// If `true`, this widget is able to receive events with the mouse. - /// If `false`, the widget is ignored in the position-to-widget resolution. - field hit_test_visible: bool; - - /// If `true`, the user is able to potentially drop data via Drag&Drop - /// on this widget. - field allow_drop: bool; - - /// If `true`, the user can copy/cut/paste data from/into this widget. - field clipboard_sensitive: bool; - - reserve u27 = 0; - } - } - - struct WidgetControlMessage { - field event_type: WidgetEvent.Type; - - /// The widget-specific type of the control message. - /// Could be something like `get_property`, `set_property`, `set_text`, ... - field type: gui.WidgetControlID; - - /// Generic parameters that can be passed to the widget. - field params: [4]usize; - } - - struct WidgetNotifyEvent { - field event_type: WindowEvent.Type; - - field widget: Widget; - - /// The widget-specific type of event. - /// Could be something like `text_changed`, `clicked`, `checked_changed`, ... - field type: gui.WidgetNotifyID; - - /// Generic data associated with the event. - field data: [4]usize; - } - - - enum MessageBoxResult : u8 { - item ok = 0; - item cancel = 1; - item yes = 2; - item no = 3; - item abort = 4; - item retry = 5; - item continue = 6; - item ignore = 7; - } - - bitstruct MessageBoxButtons : u8 { - const ok: MessageBoxButtons = .{ .has_ok = true }; - const ok_cancel: MessageBoxButtons = .{ .has_ok = true, .has_cancel = true }; - const yes_no: MessageBoxButtons = .{ .has_yes = true, .has_no = true }; - const yes_no_cancel: MessageBoxButtons = .{ .has_yes = true, .has_no = true, .has_cancel = true }; - const retry_cancel: MessageBoxButtons = .{ .has_retry = true, .has_cancel = true }; - const abort_retry_ignore: MessageBoxButtons = .{ .has_abort = true, .has_retry = true, .has_ignore = true }; - - field has_ok: bool = false; - field has_cancel: bool = false; - field has_yes: bool = false; - field has_no: bool = false; - field has_abort: bool = false; - field has_retry: bool = false; - field has_continue: bool = false; - field has_ignore: bool = false; - } - - typedef DesktopEventHandler = fnptr (Desktop, *const DesktopEvent) void; - - struct DesktopDescriptor { - /// Number of bytes allocated in a Window for this desktop. - /// See @`gui.get_desktop_data` function for further information. - field window_data_size: usize; - - /// A function pointer to the event handler of a desktop. - /// The desktop will receive events via this function. - field handle_event: DesktopEventHandler; - } - - union DesktopEvent { - field event_type: Type; - - field create_window: DesktopWindowEvent; - field destroy_window: DesktopWindowEvent; - field invalidate_window: DesktopWindowInvalidateEvent; - - field show_notification: DesktopNotificationEvent; - field show_message_box: MessageBoxEvent; - - enum Type : u16 { - //? lifecycle management: - - /// A window was created on this desktop. - item create_window = 0; - - /// A window was destroyed on this desktop. - item destroy_window = 1; - - /// A window has been invalidated and must be drawn again. - item invalidate_window = 2; - - //? user interaction: - - /// `send_notification` was called and the desktop user should display - /// a notification. - item show_notification = 3; - - /// `send_notification` was called and the desktop user should display - /// a notification. - item show_message_box = 4; - - ... - } - } - - struct DesktopWindowEvent { - field event_type: DesktopEvent.Type; - field window: Window; - } - - struct DesktopWindowInvalidateEvent { - field event_type: DesktopEvent.Type; - field window: Window; - field area: Rectangle; - } - - struct DesktopNotificationEvent { - field event_type: DesktopEvent.Type; - - /// The text of the notification. - field message: str; - - /// The severity/importance of the notification. - field severity: NotificationSeverity; - } - - struct MessageBoxEvent { - field event_type: DesktopEvent.Type; - - /// The desktop-specific request id that must be passed into - /// `notify_message_box` to finish the message box request. - field request_id: RequestID; - - /// Content of the message box. - field message: str; - - /// Caption of the message box. - field caption: str; - - /// Which buttons are presented to the user? - field buttons: MessageBoxButtons; - - /// Which icon is shown? - field icon: MessageBoxIcon; - - enum RequestID : u16 { ... } - } - - /// Dummy struct to satisfy the parser. - struct MouseEvent { - field event_type: input.InputEvent.Type; - } - - /// Dummy struct to satisfy the parser. - struct KeyboardEvent { - field event_type: input.InputEvent.Type; - } - - union WidgetEvent { - field event_type: Type; - - field mouse: MouseEvent; - field keyboard: KeyboardEvent; - field control: WidgetControlMessage; - - //? TODO: Add event data - - enum Type : u16 { - //? lifecycle: - - /// The widget was created and attached to a window. - item create = 0; - - /// The widget is in the process of being destroyed. - /// After this event, the handle will be invalid. - item destroy = 1; - - /// The creator of the widget wants to do something widget-specific. - item control = 2; - - //? basic input: - - /// The user clicked on the widget with the primary mouse button - /// or pressed the return or space bar button on the keyboard. - /// - /// NOTE: A click with the mouse is valid when, and only when: - /// `mouse_button_down` and `mouse_button_up` with the left mouse button happen on the - /// same widget. The hovered widget *may* change in between the mouse down and mouse up, - /// but the click will still be recognized. - /// NOTE: A click with the keyboard is valid when, and only when: - /// `key_press` and `key_release` happen without changing the focused widget, and only when - /// the focus giving key (space, return, ...) was pressed without any other key interrupting. - item click = 3; - - //? keyboard input: - - /// A key was pressed on the keyboard. - item key_press = 4; - - /// A key was released on the keyboard. - item key_release = 5; - - //? mouse specific extras: - - /// The mouse was moved inside the rectangle of the widget. - /// - /// NOTE: This event can only happen when `hit_test_visible` was set - /// in the widget creation flags. - item mouse_enter = 6; - - /// The mouse was moved outside the rectangle of the widget. - /// - /// NOTE: This event can only happen when `hit_test_visible` was set - /// in the widget creation flags. - item mouse_leave = 7; - - /// The mouse stopped for some time over the widget. - /// - /// NOTE: This event can only happen when `hit_test_visible` was set - /// in the widget creation flags. - item mouse_hover = 8; - - /// A mouse button was pressed over the widget. - /// - /// NOTE: This event can only happen when `hit_test_visible` was set - /// in the widget creation flags. - item mouse_button_press = 9; - - /// A mouse button was released over the widget. - /// - /// NOTE: This event can only happen when `hit_test_visible` was set - /// in the widget creation flags. - item mouse_button_release = 10; - - /// The mouse was moved over the widget. - /// - /// NOTE: This event can only happen when `hit_test_visible` was set - /// in the widget creation flags. - item mouse_motion = 11; - - /// A vertical or horizontal scroll wheel was scrolled over the widget. - /// - /// NOTE: This event can only happen when `hit_test_visible` was set - /// in the widget creation flags. - item scroll = 12; - - //? drag&drop operations: - - /// The user dragged a payload into the rectangle of this widget. - /// - /// NOTE: This event can only happen when `allow_drop` was set in the - /// widget creation flags. - item drag_enter = 13; - - /// The user dragged a payload out of the rectangle of this widget. - /// - /// NOTE: This event can only happen when `allow_drop` was set in the - /// widget type creation flags. - item drag_leave = 14; - - /// The user dragged a payload over the rectangle of this widget. - /// - /// NOTE: This event can only happen when `allow_drop` was set in the - /// widget type creation flags. - item drag_over = 15; - - /// The user dropped a payload into this widget. - /// - /// NOTE: This event can only happen when `allow_drop` was set in the - /// widget type creation flags. - item drag_drop = 16; - - //? clipboard operations: - - /// The user requested a clipboard copy operation, usually by pressing 'Ctrl-C'. - /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in - /// the widget type creation flags. - item clipboard_copy = 17; - - /// The user requested a clipboard paste operation, usually by pressing 'Ctrl-V'. - /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in - /// the widget type creation flags. - item clipboard_paste = 18; - - /// The user requested a clipboard cut operation, usually by pressing 'Ctrl-X'. - /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in - /// the widget type creation flags. - item clipboard_cut = 19; - - //? widget specific: - - //? TODO: Implement ResizedArgs with "desired size, actual size" - /// The widget was resized with a call to `place_widget`. - /// - /// NOTE: This event will not fire if the widget was only moved. - item resized = 21; - - /// The widget should draw itself. - item paint = 20; - - /// User pressed the "context menu" button or did a - /// secondary mouse button click on the widget. - item context_menu_request = 22; - - /// The widget received focus via mouse or keyboard. - item focus_enter = 23; - - /// The widget lost focus after receiving it. - item focus_leave = 24; - - ... - } - } - - union WindowEvent { - field event_type: Type; - - field mouse: MouseEvent; - field keyboard: KeyboardEvent; - field widget_notify: WidgetNotifyEvent; - - enum Type : u16 { - item widget_notify = 0; - - item key_press = 1; - item key_release = 2; - - item mouse_enter = 3; - item mouse_leave = 4; - item mouse_motion = 7; - item mouse_button_press = 6; - item mouse_button_release = 5; - - /// The user requested the window to be closed. - item window_close = 8; - - /// The window was minimized and is not visible anymore. - item window_minimize = 9; - - /// The window was restored from minimized state. - item window_restore = 10; - - /// The window is currently moving on the screen. Query `window.bounds` to get the new position. - item window_moving = 11; - - /// The window was moved on the screen. Query `window.bounds` to get the new position. - item window_moved = 12; - - /// The window size is currently changing. Query `window.bounds` to get the new size. - item window_resizing = 13; - - /// The window size changed. Query `window.bounds` to get the new size. - item window_resized = 14; - } - } - - syscall register_widget_type { - in descriptor: *const WidgetDescriptor; - out handle: WidgetType; - error AlreadyRegistered; - error SystemResources; - } - - - - - /// Opens a message box popup window and prompts the user for response. - async_call ShowMessageBox { - in desktop: Desktop; - in message: str; - in caption: str; - in buttons: MessageBoxButtons; - in icon: MessageBoxIcon; - out result: MessageBoxResult; - } - - /// Spawns a new window. - syscall create_window { - in desktop: Desktop; - in title: str; - in min: Size; - in max: Size; - in startup: Size; - in flags: CreateWindowFlags; - out handle: Window; - error InvalidDimensions; - error InvalidHandle; - error SystemResources; - } - - syscall get_window_title { - in window: Window; - in title_buf: ?[]u8; - out title_len: usize; - error InvalidHandle; - } - - syscall get_window_size { - in window: Window; - out size: Size; - error InvalidHandle; - } - - syscall get_window_min_size { - in window: Window; - out min_size: Size; - error InvalidHandle; - } - - syscall get_window_max_size { - in window: Window; - out max_size: Size; - error InvalidHandle; - } - - syscall get_window_flags { - in window: Window; - out flags: WindowFlags; - error InvalidHandle; - } - - /// Sets the `size` of `window` and returns the new actual size. - /// NOTE: This event is meant to be used from desktop APIs and will not automatically - /// notify the window of the resize event. - syscall set_window_size { - in window: Window; - in size: Size; - out actual_size: Size; - error InvalidHandle; - } - - /// Resizes a window to the new size. - syscall resize_window { - in window: Window; - in size: Size; - error InvalidHandle; - } - - /// Changes a window title. - syscall set_window_title { - in handle: Window; - in title: str; - error InvalidHandle; - } - - /// Notifies the desktop that a window wants attention from the user. - /// This could just pop the window to the front, make it blink, show a small notification, ... - syscall mark_window_urgent { - in handle: Window; - error InvalidHandle; - } - - /// Waits for an event on the given `Window`, completing as soon as - /// an event arrived. - async_call GetWindowEvent { - in window: Window; - out event: WindowEvent; - error Cancelled; - error InProgress; - error InvalidHandle; - } - - /// Create a new widget identified by `uuid` on the given `window`. - /// Position and size of the widget are undetermined at start and a call to `place_widget` should be performed on success. - syscall create_widget { - in window: Window; - in uuid: *const UUID; - out widget: Widget; - error SystemResources; - error WidgetNotFound; - error InvalidHandle; - } - - /// Moves and resizes a widget in one. - /// - /// NOTE: The position of a widget is unrestricted, but it's size - /// may be restricted by the selected widget type. - syscall place_widget { - in widget: Widget; - - /// The desired position and size of the widget. - in desired: Rectangle; - - /// The actual position and size of the widget after the operation. - out actual: Rectangle; - - error InvalidHandle; - } - - enum WidgetControlID : u32 { ... } - - /// Triggers the `control` event of the widget with the given `message` as a payload. - syscall control_widget { - in widget: Widget; - in message: WidgetControlMessage; - error SystemResources; - error InvalidHandle; - } - - enum WidgetNotifyID : u32 { ... } - - /// Puts a `widget_notify` event into the event queue of the `Window` that owns `widget`. - /// The parameters are passed as a `WidgetNotifyEvent` to the event queue. - syscall notify_owner { - in widget: Widget; - in type: WidgetNotifyID; - in params: *const [4]usize; - error SystemResources; - error InvalidHandle; - } - - /// Returns WidgetType-associated "opaque" data for this widget. - /// - /// This is meant as a convenience tool to store additional information per widget - /// like internal state and such. - /// - /// The size of this must be known and cannot be queried. - syscall get_widget_data { - in widget: Widget; - out data: [*]align(16) u8; - error InvalidHandle; - } - - /// Returns the current location and size of the provided widget. - syscall get_widget_bounds { - in widget: Widget; - out bounds: Rectangle; - error InvalidHandle; - } - - /// Creates a new desktop with the given name. - syscall create_desktop { - /// User-visible name of the desktop. - in name: str; - in descriptor: *const DesktopDescriptor; - out desktop: Desktop; - error SystemResources; - } - - /// Returns the name of the provided desktop. - syscall get_desktop_name { - in desktop: Desktop; - in name_buf: ?[]u8; - out name_len: usize; - error InvalidHandle; - } - - /// Enumerates all available desktops. - syscall enumerate_desktops { - in serverlist: ?[]Desktop; - out count: usize; - } - - /// Returns all windows for a desktop handle. - syscall enumerate_desktop_windows { - in desktop: Desktop; - in window: ?[]Window; - out count: usize; - error InvalidHandle; - } - - /// Returns desktop-associated "opaque" data for this window. - /// - /// This is meant as a convenience tool to store additional information per window - /// like position on the screen, orientation, alignment, ... - /// - /// The size of this must be known and cannot be queried. - syscall get_desktop_data { - in window: Window; - out data: [*]align(16) u8; - error InvalidHandle; - } - - /// Notifies the system that a message box was confirmed by the user. - /// - /// NOTE: This function is meant to be implemented by a desktop server. - /// Regular GUI applications should not use this function as they have no - /// access to a `MessageBoxEvent.RequestID`. - syscall notify_message_box { - /// The desktop that completed the message box. - in source: Desktop; - /// The request id that was passed in `MessageBoxEvent`. - in request_id: MessageBoxEvent.RequestID; - /// The resulting button which the user clicked. - in result: MessageBoxResult; - error BadRequestId; - error InvalidHandle; - } - - /// Posts an event into the window event queue so the window owner - /// can handle the event. - syscall post_window_event { - in window: Window; - in event: WindowEvent; - error SystemResources; - error InvalidHandle; - } - - /// Sends a notification to the provided `desktop`. - syscall send_notification { - /// Where to show the notification? - in desktop: Desktop; - /// What text is displayed in the notification? - in message: str; - /// How urgent is the notification to the user? - in severity: NotificationSeverity; - error SystemResources; - error InvalidHandle; - } - - namespace clipboard { - /// Sets the contents of the clip board. - /// Takes a mime type as well as the value in the provided format. - syscall set { - in desktop: Desktop; - in mime: str; - in value: str; - error SystemResources; - } - - /// Returns the current type present in the clipboard, if any. - syscall get_type { - in desktop: Desktop; - in type_buf: ?[]u8; - out type_len: usize; - error InvalidHandle; - } - - /// Returns the current clipboard value as the provided mime type. - /// The os provides a conversion *if possible*, otherwise returns an error. - /// The returned memory for `value` is owned by the process and must be freed with `ashet.process.memory.release`. - syscall get_value { - in desktop: Desktop; - in mime: str; - out value: []const u8; - error InvalidHandle; - error SystemResources; - error ConversionFailed; - error ClipboardEmpty; - } - } -} - -/// The service namespace implements a kernel-mediated Object Request Broker (ORB). -/// -/// It allows processes to register "Interfaces" consisting of functions that can be -/// called by other processes. -/// -/// KEY FEATURES: -/// - **Hybrid Invocation**: Interfaces can support synchronous (blocking) and/or -/// asynchronous (overlapped) invocation models. -/// - **Type Safety**: The kernel validates that the caller passes the correct number and types -/// of arguments (integers vs resources). -/// - **Resource Marshalling**: Resources passed to/from interfaces are automatically -/// bound to the receiver's process. -/// - **Context Switching**: The kernel switches the "Resource Context" of the executing thread -/// to the Interface's owning process during the execution of the handler. -namespace service { - /// An interface is a collection of synchronous functions and overlapped operations. - /// - /// Calling the functions will directly invoke an associated function in the creating - /// process. - /// - /// Calling an overlapped operation will trigger the creating process which eventually - /// completes the operation. - /// - /// Each interface is uniquely identified by a UUID which allows identification of the - /// interface and asserts the contract for the semantics of the functions and overlapped ops. - /// - /// In addition to the UUID, each interface has a signature that asserts the compatibility - /// between the producer and the consumer of the interface. - /// - /// Interfaces can have up to 256 functions and overlapped operations each, with each - /// function having up to 8 input values and a single return value, and overlapped operations - /// having up to 8 input and 8 output values. - /// - /// NOTE: As interface resources are hold both by the service and the consumer, an interface is - /// always tied to the lifetime of to the creating process. This ensures that when the process - /// is terminated, the interface resource will be destroyed. - /// - /// This is not done through the tethering interface, but through a dedicated mechanism that - /// ensures correctness. - resource Interface { } - - /// Defines the type of argument and the potential transformations the - /// kernel performs when passing the argument between caller and callee. - enum MarshalType : u2 { - /// The argument/result is unused. - /// Unused values must be set to zero or otherwise the call/return is invalid. - item unused = 0; - - /// This value is reserved and should not be used. - item reserved = 1; - - /// The value is passed unmodified by the kernel. - /// NOTE: This value should be used for integers, enumerations, - /// raw pointers and so on. - item raw = 2; - - /// The value passed is a system resource. - /// - /// MARSHALLING rules: - /// - A zero value is never marshalled and passed verbatim. This allows passing optional resources. - /// - /// - For `invoke` / `Invoke` inputs: - /// The kernel interprets resource handles in the *calling thread's current resource context*, - /// validates them, then creates an `at_least_weak` binding for the process that owns `interface`. - /// The `Function` / `AsyncHandler` receives resource handles valid in its own resource context. - /// - /// - For `invoke` output: - /// If the signature declares a resource output, the kernel interprets the returned handle in the - /// resource context active during the `Function` call (the interface handler context). - /// If it is non-zero and valid, the kernel creates a strong binding for the `invoke` caller's - /// resource context and returns the translated handle. - /// If it is non-zero and invalid, `invoke` returns `error.BadReturnValue`. - /// - /// - For `complete_request` outputs: - /// The kernel interprets resource handles in `results` in the *calling thread's current resource context*. - /// For each non-zero valid handle, it creates a strong binding for the resource context that scheduled - /// the original `Invoke` request and returns the translated handle in `Invoke.results`. - item @"resource" = 3; - } - - /// Defines the signature of a function or overlapped operation. - /// - /// NOTE: For function signatures, `outputs[1..]` must be set to - /// `MarshalType.unused`. - /// - /// NOTE: For both `inputs` and `outputs`: As soon as an index in the - /// array is `MarshalType.unused`, all following items must also be - /// `MarshalType.unused`. - bitstruct FunctionSignature : u32 { - /// Defines the number and type of the input arguments. - /// `inputs[0]` is the first argument, `inputs[7]` is the eighth argument. - field inputs: [8]MarshalType; - - /// Defines the number and type of the result values. - /// `outputs[0]` is the first result, `outputs[7]` is the eighth result. - /// - /// NOTE: Only `outputs[0]` may be set for functions. Overlapped operations - /// can use all eight values. - field outputs: [8]MarshalType; - } - - /// A token used by the interface to identify a pending asynchronous request. - /// - /// NOTE: Request tokens are valid globally and may be passed between processes. - enum RequestToken : u32 { ... } - - /// The signature of the asynchronous request handler function registered by an interface. - /// - /// **Parameters:** - /// 1. `context`: The opaque pointer associated with the interface. - /// 2. `request`: The token to the incoming request. - /// 3. `operation`: The index of the operation being called. - /// 4. `arguments`: Pointer to the 8 input arguments provided by the caller. - /// - /// **Behavior:** - /// The handler should store the `req` and the values inside `args` (if needed) and return immediately. - /// The operation is completed later via `complete_request`. - /// - /// NOTE: A handler function should not yield the executing thread, as this will generate - /// hard to debug scenarios. - /// - /// NOTE: `arguments` must be assumed invalid after the return of the callback. - /// - /// NOTE: The handler should perform a strong binding of resources inside `arguments` if it must - /// retain independent access even if the caller later unbinds/releases its handles. - /// This still does not prevent explicit destruction of the resource. - typedef AsyncHandler = fnptr(context: ?anyptr, request: RequestToken, operation: u8, arguments: *const [8]usize) void; - - /// The signature of the asynchronous cancellation handler function registered by an interface. - /// - /// **Parameters:** - /// 1. `context`: The opaque pointer associated with the interface. - /// 2. `request`: The token to the incoming request. - /// - /// **Behavior:** - /// This handler is invoked inside `overlapped.cancel` when the operation wasn't completed yet. - /// The implementor shall perform potential cancellation of the request and must not call `complete_request` - /// or `fail_request` anymore, as `request` will be invalidated after the cancel handler returns. - /// - /// NOTE: A handler function should not yield the executing thread, as this will generate - /// hard to debug scenarios. - typedef CancelHandler = fnptr(context: ?anyptr, request: RequestToken) void; - - /// The signature of a synchronous function call registered by an interface. - /// - /// **Parameters:** - /// 1. `context`: The opaque pointer associated with the interface. - /// 2. `arguments`: Pointer to the 8 input arguments provided by the caller. - /// - /// The function may or may not return a value depending on it's signature. - /// - /// If the signature does not define a return value, the function must return zero. - /// - /// NOTE: A synchronous function should not yield the executing thread, as this will generate - /// hard to debug scenarios. - /// - /// NOTE: `arguments` must be assumed invalid after the return of the callback. - typedef Function = fnptr(context: ?anyptr, arguments: *const [8]usize) usize; - - /// Creates a new interface that can be invoked by other processes. - /// - /// NOTE: After creation, the interface is not yet discoverable with `enumerate`. - /// This way, private interfaces can be passed between processes. - /// - /// NOTE: If an interface should be available as a system service, it must be - /// published with `register`. - syscall create { - /// The unique identifier of the interface. - /// - /// This UUID defines the contract this interface implements. - /// - /// NOTE: The kernel copies the UUID object internally. - in uuid: *const UUID; - - /// A human-readable name for enumeration and debugging. - in name: str; - - /// Defines the signatures and count of synchronous function calls in the interface. - /// - /// NOTE: The kernel will create an internal copy of this array, so userland - /// can reuse the memory freely after this call. - in sync_signatures: []const FunctionSignature; - - /// Defines the signatures and count of overlapped operations in the interface. - /// - /// NOTE: The kernel will create an internal copy of this array, so userland - /// can reuse the memory freely after this call. - in async_signatures: []const FunctionSignature; - - /// The opaque context pointer ("this") passed to the functions in `vtable` and the `async_handler`. - /// - /// NOTE: This can be used to implement a stateful interface that allows a process to create - /// the same interface more than once and still have context which of the interfaces were - /// called. - in context: ?anyptr; - - /// The synchronous implementation functions (vtable). - /// - /// NOTE: Must have the same number of elements as `sync_signatures`. - /// - /// NOTE: The kernel will create an internal copy of this array, so userland - /// can reuse the memory freely after this call. - in vtable: []const Function; - - /// The asynchronous request handler. - /// - /// NOTE: All asynchronous requests go through the same function handler, and - /// dispatch must happen in userland. - /// - /// The kernel ensures that argument marshalling will be properly performed, - /// and function will never be out of range for the interface. - /// - /// NOTE: May be `null` if, and only if, `async_signatures.len == 0`. - in async_handler: ?AsyncHandler; - - /// The asynchronous cancellation handler. - /// - /// NOTE: All cancellation requests go through the same function handler, and - /// dispatch must happen in userland. - /// - /// NOTE: May be `null` even if `async_signatures.len > 0`. - /// - /// NOTE: If `null`, the kernel just invalidates the `RequestToken` on cancellation - /// and will not allow completion of the request. - /// - /// The userland process may still perform unnecessary work. - in cancel_handler: ?CancelHandler; - - /// The created interface resource. - out interface: Interface; - - /// A signature inside `sync_signatures` or `async_signatures` is invalid. - /// - /// This means either: - /// - A function has more than a single output - /// - A signature has `MarshalType.unused` between used inputs or outputs. - /// - `MarshalType.reserved` is used. - error InvalidSignature; - - /// Returned if a parameter is malformed. - /// - /// Reasons for this may be: - /// - `sync_signatures.len != vtable.len`. - /// - `uuid` is nil (all bits zero) or omni (all bits one). - /// - `async_handler` is null, but `async_signatures.len > 0`. - error InvalidValue; - - error SystemResources; - } - - /// Returns the UUID of the interface. - syscall get_interface_uuid { - in interface: Interface; - out uuid: UUID; - - /// `interface` is not a valid interface resource. - error InvalidHandle; - } - - /// Returns the name of the interface. - syscall get_interface_name { - in interface: Interface; - in name_buf: ?[]u8; - - /// If `name_buf` is null, the total length of the name. - /// If `name_buf` is not null, the number of bytes written to `name_buf`. - out name_len: usize; - - /// `interface` is not a valid interface resource. - error InvalidHandle; - } - - /// Registers an interface as a systemwide service. - /// - /// All registered interfaces can be discovered through `enumerate`. - /// - /// NOTE: Revoking the registration is not possible by design. To unpublish - /// a service, the interface resource must be destroyed. - /// - /// NOTE: Registering an `interface` twice is idempotent and does nothing. - syscall register { - /// The interface that shall be published. - in interface: Interface; - - /// `interface` is not a valid interface resource. - error InvalidHandle; - - error SystemResources; - } - - /// Enumerates all registered services. - syscall enumerate { - /// If not `null`, the enumeration returns only interfaces - /// with the given unique identifier. - /// If `null` will enumerate all interfaces. - in uuid: ?*const UUID; - - /// If not `null`, the kernel will write the registered interfaces to this array. - /// - /// NOTE: The interface handles will be bound to the calling process with `BindOperation.at_least_weak` - /// to ensure resource access. - in services: ?[]Interface; - - /// Number of elements written to `services` or total number of registered - /// interfaces. - out count: usize; - - error SystemResources; - } - - /// Invokes an interface function synchronously. - /// - /// **Execution Flow:** - /// 1. Kernel validates arguments against signature. - /// 2. Kernel marshals input resources. - /// 3. Kernel context-switches to interface process. - /// 4. Kernel calls `vtable[func_index]`. - /// 5. Kernel context-switches back. - /// 6. Kernel optionally marshals the output resource. - syscall invoke { - in interface: Interface; - - /// Index of the function which shall be invoked. - in function: u8; - - /// The arguments passed to `function`. - /// - /// NOTE: The kernel will perform marshalling as defined in the function signature. - /// - /// NOTE: The kernel will validate that all unused arguments are zero. - /// - /// NOTE: Resource handles passed here are assumed to be valid in the callers resource context. - in arguments: [8]usize; - - /// The return value of the function. - /// - /// NOTE: If a resource handle is returned, the resource will be strongly bound to the callers process. - /// - /// NOTE: If a resource is expected to be returned, but zero is returned, the kernel will pass the zero. - /// This allows returning optional resources. Userland has to validate that rules for non-zero only returns. - out result: usize; - - /// `interface` is not a valid interface resource. - error InvalidHandle; - - /// `function` does not exist. - error InvalidFunction; - - /// The kernel validation of the arguments failed. - /// - /// This can have two reasons: - /// - An unused argument is non-zero. - /// - A resource argument is not a valid resource handle. - error InvalidArg; - - /// Returned in the following cases: - /// - The invoked function returns a resource handle, but this - /// resource handle was neither valid nor zero. - /// - The invoked function return value is unused, but the - /// function returned a non-zero value. - /// - /// LORE: This error is sadly the best way to handle implementation bugs - /// in the interface. As the implementor process has already surrendered - /// control back to the kernel, there's no channel back to the implementor - /// to inform it about misbehaviour. - error BadReturnValue; - - error SystemResources; - } - - /// Schedules an overlapped interface operation. - /// - /// **Execution Flow:** - /// 1. Kernel validates arguments against signature. - /// 2. Kernel allocates an internal Request State. - /// 3. Kernel marshals input resources. - /// 4. Kernel context-switches to Interface process (temporarily). - /// 5. Kernel calls `async_handler`. - /// 6. Kernel context-switches back and returns the ARC to the caller. - async_call Invoke { - in interface: Interface; - - /// Index of the asynchronous operation that shall be invoked. - in operation: u8; - - /// The arguments passed to `operation`. - /// - /// NOTE: The kernel will perform marshalling as defined in the operation signature. - /// - /// NOTE: The kernel will validate that all unused arguments are zero. - /// - /// NOTE: Resource handles passed here are assumed to be valid in the callers resource context. - in arguments: [8]usize; - - /// The results of the operation. - /// - /// NOTE: Filled by the kernel when the interface implementor calls `complete_request`. - /// - /// NOTE: Resource handles returned here are bound strongly to the schedulers resource context. - out results: [8]usize; - - /// `interface` is not a valid interface resource. - error InvalidHandle; - - /// `operation` does not exist. - error InvalidFunction; - - /// The kernel validation of the arguments failed. - /// - /// This can have two reasons: - /// - An unused argument is non-zero. - /// - A resource argument is not a valid resource handle. - error InvalidArg; - - /// The operation was failed by a call to `fail_request`. - error RequestFailed; - - error SystemResources; - } - - /// Completes a pending asynchronous request (called by the interface implementor). - /// - /// NOTE: This consumes the `request` token and wakes the caller (completing their ARC). - syscall complete_request { - /// The request token passed to `AsyncHandler`. - in request: RequestToken; - - /// The return values/resources. - /// - /// NOTE: Resources in this array will be marshalled to the caller as specified - /// in the operation signature. - /// - /// NOTE: Resource handles passed here are assumed to be valid in the calling thread's current resource context. - in results: [8]usize; - - /// `request` is not a valid pending request. - error InvalidHandle; - - /// The kernel validation of the results failed. - /// - /// This can have two reasons: - /// - An unused result is non-zero. - /// - A resource result is not a valid resource handle. - error InvalidArg; - - error SystemResources; - } - - /// Rejects/fails a pending asynchronous request (called by the interface implementor). - /// - /// This completes the caller's ARC with a generic `RequestFailed` error. - /// - /// NOTE: This consumes the `request` token and wakes the caller (completing their ARC). - syscall fail_request { - /// The request token passed to `AsyncHandler`. - in request: RequestToken; - - /// `request` is not a valid pending request. - error InvalidHandle; - } -} - -//? TODO: Review this namespace. -/// -/// The I/O namespace contains APIs to interface with external hardware like serial ports, I²C busses and so on. -/// -namespace io { - - /// - /// Functions and types related to serial busses like RS232, RS485 or similar. - /// - namespace serial { - enum SerialPortID : u32 { - ... - } - - syscall enumerate { - in list: ?[]SerialPortID; - out count: usize; - } - - /// Queries information about the given serial port id. - syscall query_metadata { - in id: SerialPortID; - in name_buf: ?[]u8; - - out name_len: usize; - - /// The given id does not exist. - error NotFound; - } - - resource SerialPort { } - - syscall open { - in id: SerialPortID; - out port: SerialPort; - - /// The given id does not exist. - error NotFound; - - /// The resource is already opened. - error ResourceBusy; - } - - //? /// - //? /// Changes the configuration of a serial port and returns the new configuration. - //? /// - //? /// This function can also be used to query the current configuration by requesting no changes. - //? /// - //? async_call configure { - //? in port: SerialPort; - - //? in baud_rate: ?u32; - //? in word_size: ?u8; - //? in stop_bits: ?StopBits; - //? in parity: ?Parity; - //? in control_flow: ?ControlFlow; - - //? /// Selects which software control flow words control the transmitter - //? /// activity. - //? /// - //? /// NOTE: This is usually the same as `sw_control_flow_tx`. - //? in sw_control_flow_rx: ?SoftwareControlFlow; - - //? /// Selects which software control flow words are transmitted when - //? /// the own receive buffer is full. - //? /// - //? /// NOTE: This is usually the same as `sw_control_flow_rx`. - //? in sw_control_flow_tx: ?SoftwareControlFlow; - - //? in acceptable_baud_error: f32; - - //? out current_baud_rate: u32; - //? out current_data_bits: u8; - //? out current_stop_bits: StopBits; - //? out current_parity: Parity; - //? out current_control_flow: ControlFlow; - //? out current_sw_control_flow_rx: SoftwareControlFlow; - //? out current_sw_control_flow_tx: SoftwareControlFlow; - - //? error InvalidHandle; - - //? /// The actual baud rate diverges more than `acceptable_baud_error` from the requested baud rate. - //? error ImpreciseBaudRate; - - //? /// The requested word size is not supported by this serial port. - //? error UnsupportedDataBits; - - //? /// The requested number of stop bits is not supported by this serial port. - //? error UnsupportedStopBits; - - //? /// The requested parity is not supported by this serial port. - //? error UnsupportedParity; - - //? /// The requested control flow (or its configuration) is not supported by this serial port. - //? error UnsupportedControlFlow; - //? } - - //? /// Changes the output control lanes of the serial port. - //? async_call control { - //? in port: SerialPort; - - //? /// The new state that should be applied for DTR (Data Terminal Ready). - //? in dtr: ?bool; - - //? /// The new state that should be applied for RTS (Request To Send). - //? /// - //? /// NOTE: This is also called `RTR` when used for modern hardware control flow. - //? in rts: ?bool; - - //? error InvalidHandle; - - //? /// The serial port does not support changing the control flow mode. - //? error Unsupported; - - //? /// The control lanes cannot be changed as hardware control flow is active. - //? error ControlFlowActive; - //? } - - /// Reads all control lanes of the serial port. - async_call query_control { - in port: SerialPort; - - /// Data Terminal Ready - out dtr: bool; - - /// Data Carrier Detect - out dcd: bool; - - /// Data Set Ready - out dsr: bool; - - /// Ring Indicator - out ring: bool; - - /// Request To Send - /// - /// NOTE: This is also called `RTR` when used for modern hardware control flow. - out rts: bool; - - /// Clear To Send - out cts: bool; - - error InvalidHandle; - - /// The serial port does not support control flow lanes. - error Unsupported; - } - - /// Writes data to the serial port. - async_call Write { - in port: SerialPort; - in data: bytestr; - - /// Number of words written - out written: usize; - - error InvalidHandle; - - /// The serial port uses a word size that needs more than 8 bits per word. - error WordSizeMismatch; - } - - /// Reads data from a serial port. - async_call Read { - in port: SerialPort; - in data: bytebuf; - - /// Number of words read from the serial port. - out read: usize; - - /// This contains the reason why not *all* data was read. - out stop_reason: SerialPortError; - - error InvalidHandle; - - /// The serial port uses a word size that needs more than 8 bits per word. - error WordSizeMismatch; - } - - /// - /// Sends a break signal. - /// - /// LEARN: A break signal means that the TX line is held *low* for a given - /// duration larger than a single word. This way, the receiver can - /// recognize an event that will be transferred "out of band" and - /// allows to send an event to the receiver. - /// - /// In DMX, this is used to signal the start of a new frame. - /// - async_call Break { - in port: SerialPort; - - in duration: clock.Duration; - - error InvalidHandle; - - /// The serial port does not support sending breaks. - error Unsupported; - } - - enum SerialPortError : u8 { - item none = 0; - item break_detected = 1; - item parity_error = 2; - item framing_error = 3; - } - - enum StopBits : u8 { - item one = 1; - item one_and_half = 2; - item two = 3; - } - - enum Parity : u8 { - /// No parity will be used. - item none = 0; - - /// The parity bit will contain a `0` if the sum of all data bits are even. - item even = 1; - - /// The parity bit will contain a `0` if the sum of all data bits are odd. - item odd = 2; - - /// The parity bit will always contain a `1`. - item mark = 3; - - /// The parity bit will always contain a `0`. - item space = 4; - } - - enum ControlFlow : u8 { - /// No explicit control flow is used. - item none = 0; - - /// This mode is usually called the *hardware control flow* and uses two - /// signals that are connected cross-over between both communication partners. - /// - /// - `RTR` is an active-low signal called *Ready To Receive* which signals the - /// opposite part that we can actually receive data right now. If the signal - /// is high, the opposite part may not send data. - /// - `CTS` is an active-low signal called *Clear To Send* which receives the `RTR` - /// signal from the opposite part. When this signal is low, our transmitter is - /// allowed to send data. If the signal is high, the transmitter has to stop sending - /// and a timeout may occur. - /// - /// LORE: This is usually called RTS/CTS control flow, but - /// in that configuration the RTS (request to send) signal - /// is repurposed into a RTR (ready to receive) signal which - /// tells the communication partner that you are able to receive - /// data. - /// In Ashet OS, the technically correct term is used as we don't - /// use the legacy half-duplex control flow where a request to - /// send is performed. - item rtr_cts = 1; - - /// This mode is usually called *software control flow* and uses two special bytes - /// that can inhibit or allow transmitting bytes. - /// - /// The two special word are called `XON` (Transmitter On) and `XOFF` (Transmitter Off). - /// - /// When a receiver receives the `XOFF` word, it turns off the sender after the current - /// byte has been processed. As soon as the `XON` word is received, the sender is allowed - /// to continue sending data. - /// - /// `XON` and `XOFF` can be both sent by software or hardware. - /// - /// NOTE: Using this control flow prevents sending *raw binary* data, as `XON` and `XOFF` - /// are regular data words that can be contained in the sent data. - /// - /// Thus, this control flow mode should only be used with textual data which does - /// not conflict with the chosen control characters. - /// - item xon_xoff = 2; - } - - struct SoftwareControlFlow { - /// The data word which will turn on the transmitter. - /// - /// NOTE: This is usually the ASCII `DC1` character which is encoded as the value `0x11`. - /// - /// LORE: Serial ports usually support 5 to 8 data bits, but some serial ports - /// can also use much higher word sizes. - /// - /// This is allows a future expansion to support word sizes up to 32 bits - /// without breaking existing code. - field x_on: u32 = 0x11; //? ASCII DC1 / XON - - /// The data word which will turn off the transmitter. - /// - /// NOTE: This is usually the ASCII `DC3` character which is encoded as the value `0x13`. - /// - /// LORE: Serial ports usually support 5 to 8 data bits, but some serial ports - /// can also use much higher word sizes. - /// - /// This is allows a future expansion to support word sizes up to 32 bits - /// without breaking existing code. - field x_off: u32 = 0x13; //? ASCII DC3 / XOFF - } - } - - /// - /// Functions and types related to the I²C bus. - /// - namespace i2c { - enum BusID : u32 { - ... - } - - syscall enumerate { - in list: ?[]BusID; - - out count: usize; - } - - /// Queries information about the given I²C bus id. - syscall query_metadata { - in id: BusID; - in name_buf: ?[]u8; - - out name_len: usize; - - /// The given id does not exist. - error NotFound; - } - - resource Bus { } - - syscall open { - in id: BusID; - - out bus: Bus; - - /// The given id does not exist. - error NotFound; - - error SystemResources; - } - - /// Performs a sequence of I²C operations on the bus without interruption. - /// - /// This function allows to ping devices, read or write data from/to the devices. - /// - /// The operations are performed first-to-last without interruption. If an operation fails, the - /// sequence will be aborted. - /// - /// LORE: This function was introduced instead of separate read and write functions, as it's - /// sometimes necessary for certain I²C ICs (like EEPROMS) to have "atomic" read-after-write - /// operations. - /// - /// As both a singular read and a singular write can be expressed as a batch of one operation, - /// this is the only available function on the I²C bus. - /// - /// **Example:** - /// Typical I²C EEPROMS have an internally maintained "memory cursor" which is advanced for every - /// read operation. All write operations will update the cursor also for read operations. - /// - /// This means that in an OS context, where scheduling can interrupt our process, performing a split - /// "update cursor" write operation and "read data" read operation can be interrupted by another process - /// also scheduling writes inbetween. This means that after we've set up our memory cursor to the desired - /// address another process would change that before we read and we'll read data from the wrong memory - /// location. - /// - /// To prevent such a situation, this batch interface was introduced. - /// - async_call Execute { - in bus: Bus; - - /// A mutable sequence of I²C operations. Will be processed first-to last and - /// the pointed `Operation`s will be changed during execution to report results. - in sequence: []Operation; - - /// The number of successfully processed elements from `sequence`. - /// - /// On success, the call returns exactly the length of the sequence, otherwise - /// it returns the index of the element that failed. - out count: usize; - - error InvalidHandle; - - /// An operation in the `sequence` contained an invalid address. - error InvalidAddress; - - /// An operation that isn't `ping` was trying to process zero bytes. - error EmptyOperation; - - /// An error happened during processing. Read `sequence[].error` to see which operations failed. - error ExecutionFailed; - } - - struct Operation { - /// The 7- or 10 bit device that should be addressed. - /// - /// NOTE: Values above `1023` will always make the batch execution fail. - field address: u16; - - /// The kind of operation that should be performed. - field type: Type; - - /// The data which should either be written or read. - /// - /// NOTE: If the `operation` is `BatchOp.read`, the buffer will - /// overwritten by the OS. All other operations treat this - /// buffer as immutable. - field data: bytebuf; - - /// The number of processed bytes inside `data`. - /// - /// NOTE: This field can be left uninitialized and will be overwritten by the OS - /// with the result of the operation. - /// If the batch item wasn't scheduled, the resulting value will be zero. - field processed: usize = 0; - - /// The error that happened when processing this batch item. On success, this will - /// be set to `Error.none`. - /// - /// NOTE: This field can be left uninitialized and will be overwritten by the OS - /// with the result of the operation. - /// If the batch item wasn't scheduled, the resulting value will be `Error.aborted`. - field @"error": Error; - - enum Type : u8 { - /// The device will be addressed, but no read or write operation will be performed. - /// This can be used to detect if certain devices are present. - /// - /// NOTE: Success of this operation can be detected by the resulting error code. - item ping = 0; - - /// Reads the given amount of bytes from the device. - item read = 1; - - /// Writes the given amount of bytes to the device. - item write = 2; - } - - enum Error : u8 { - /// This batch item was fully processed. - item none = 0; - - /// No device did acknowledge the address. - item device_not_found = 1; - - /// While writing the data, the device returned a NAK. - /// - /// NOTE: The ACK/NAK during the addressing phase is handled by `device_not_found`. - item no_acknowledge = 2; - - /// A previous batch item errored and this item wasn't executed at all. - item aborted = 3; - - /// A bus participant did stretch the clock for too long. - item timeout = 4; - - /// During the processing of the operation, a hardware error occurred. - item fault = 5; - - /// The operation would've operated on a reserved I2C address. - item reserved_address = 6; - } - } - } -} - -//? -//? Global Types -//? - - -struct Point { - const zero: Point = .{ .x = 0, .y = 0 }; - - field x: i16; - field y: i16; -} - -struct Size { - const empty: Size = .{ .width = 0, .height = 0 }; - const max: Size = .{ .width = 0xFFFF, .height = 0xFFFF }; - - field width: u16; - field height: u16; -} - -struct Rectangle { - field x: i16; - field y: i16; - field width: u16; - field height: u16; -} - -/// -/// An 8-bit color value with a specialized encoding suitable for embedding -/// a practical set of 256 colors. -/// -/// The color encoding is basically a HSV (hue, saturation, value) color with 8 bits, using -/// 3 bits for the hue, 3 bits for the value and 2 bits for the saturation. -/// -/// Naively mapping out the values to the HSV values has two problems though: -/// 1. A value of 0 maps all colors to black, meaning that we would have 64 different -/// types of blacks, which all would encode have the rgb value `(0, 0, 0)`. -/// 2. A saturation of 0 maps all colors to gray, effectively ignoring the hue. -/// This creates the situation that in addition to having 64 blacks, we would also -/// have each gray tone 8 times, wasting even more encoding space. -/// -/// To address these two problems, the color scheme uses a modified mapping: -/// -/// - `hue` is used without special interpretation. -/// - `value` maps to a range of `[1:8]` instead of `[0:7]`, allowing 8 different -/// values that are all not black. -/// - `saturation` is used without special interpretation except for zero: -/// If the `saturation` field is zero, `hue` and `value` are interpreted together as a 6 bit -/// integer storing the brightness of gray. -/// -/// This yields a color space which has the following properties: -/// -/// - 64 true gray levels ranging from black to white. -/// - 8 different hues (red, yellow, lime, green, cyan, blue, purple, magenta). -/// - 3 different levels of saturation for each non-gray color. -/// - black maps to `0x00` (but white does not map to `0xFF`). -/// -/// This means we have all 256 colors mapped to a distinct, meaningful color that still allows -/// programmatic conversion from and to the color without the need of a look-up table that -/// would require searching the correct color. -/// -/// NOTE: This color encoding shall be referred to as "Ashet HSV". -/// -/// LORE: This color encoding was developed over the course of several days, playing around with -/// many different encodings. -/// The color encodings/palettes were tested on a diverse set of images, including game screenshots, -/// photographs, artificial images, vector graphics and so on. -/// -/// The "Ashet HSV" encoding showed the best visual matches for most pictures, allowing both visual -/// fidelity on the color side, but also allowing both bright and dark images to work really well. -/// -bitstruct Color : u8 { - const black: Color = .{ .hue = 0, .value = 0, .saturation = 0 }; - const white: Color = .{ .hue = 7, .value = 7, .saturation = 0 }; - const red: Color = .{ .hue = 0, .value = 7, .saturation = 3 }; - const yellow: Color = .{ .hue = 1, .value = 7, .saturation = 3 }; - const lime: Color = .{ .hue = 2, .value = 7, .saturation = 3 }; - const green: Color = .{ .hue = 3, .value = 7, .saturation = 3 }; - const cyan: Color = .{ .hue = 4, .value = 7, .saturation = 3 }; - const blue: Color = .{ .hue = 5, .value = 7, .saturation = 3 }; - const purple: Color = .{ .hue = 6, .value = 7, .saturation = 3 }; - const magenta: Color = .{ .hue = 7, .value = 7, .saturation = 3 }; - - /// The hue of the color, encoded as 0 = 0° (red), 7 = 315° (magenta). - field hue: u3; - - /// The value of the color, with 0 = 12.5% brightness and 7 = 100% brightness. - field value: u3; - - /// The saturation of the color, encoded as 0 = desaturated, and 3 = fully saturated. - /// - /// NOTE: The value is encoded as the uppermost 2 bits, so a check if saturation is 0 can be - /// performed by doing a less-than operation interpreting the color as an integer. - field saturation: u2; - - struct RGB888 { - field r: u8; - field g: u8; - field b: u8; - } - - /// 32-bit ARGB format, [31:0] A:R:G:B 8:8:8:8 little endian - /// - /// Layed out as a `u32` encoding `0xAARRGGBB`. - enum ARGB8888 : u32 { ... } - - /// 32-bit ABGR format, [31:0] A:B:G:R 8:8:8:8 little endian - /// - /// Layed out as a `u32` encoding `0xAABBGGRR`. - enum ABGR8888 : u32 { ... } -} - -//? -//? TODO: Move these types into the proper namespaces or decide they -//? are actually top-level. -//? - - -struct UUID { - field bytes: [16]u8; -} diff --git a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch b/src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch deleted file mode 100644 index 39bdec43..00000000 --- a/src/tools/abi-mapper/tests/stress/ashet-1.0.abi.patch +++ /dev/null @@ -1,182 +0,0 @@ -diff --git c/src/tools/abi-mapper/tests/stress/ashet-1.0.abi w/src/tools/abi-mapper/tests/stress/ashet-1.0.abi -index 4c795bdab0..22a81adb11 100644 ---- c/src/tools/abi-mapper/tests/stress/ashet-1.0.abi -+++ w/src/tools/abi-mapper/tests/stress/ashet-1.0.abi -@@ -1297,7 +1297,7 @@ namespace clock { - /// is sufficient to fully cover the lifetime of the electronics, - /// users and probably even countries and societies. - /// Not quite infinity, but close enough for the computer it's running on. -- item infinity = 0xFFFF_FFFF_FFFF_FFFF; -+ item infinity = 0xFFFFFFFFFFFFFFFF; - - ... - } -@@ -2184,6 +2184,11 @@ namespace input { - /// - /// It may be empty if the kernel cannot provide one. - /// -+ struct DeviceMetadataLengths { -+ field name_len: usize; -+ field unique_id_len: usize; -+ } -+ - syscall query_device_metadata { - in id: DeviceId; - in name_buf: ?[]u8; -@@ -2192,8 +2197,7 @@ namespace input { - /// If not `null`, the kernel will fill this structure with metadata for the device. - in descriptor: ?*DeviceDescriptor; - -- out name_len: usize; -- out unique_id_len: usize; -+ out lengths: DeviceMetadataLengths; - - /// `id` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; -@@ -2238,7 +2242,7 @@ namespace input { - /// If the device is present in groups, the event is enqueued into those group queues. - syscall emit_device_event { - in device: DeviceId; -- in payload: InputEventPayload; -+ in payload: InputEvent.Payload; - - /// `device` is not valid anymore (e.g. device removed) or was never valid. - error InvalidDevice; -@@ -2413,7 +2417,7 @@ namespace input { - /// except it is marked synthetic. - syscall queue_event { - in group: InputGroup; -- in payload: InputEventPayload; -+ in payload: InputEvent.Payload; - in force: bool; - - /// `group` is not a valid input group resource. -@@ -5945,14 +5949,14 @@ namespace fs { - /// Encoding: - /// - UTF-8 - /// - NUL-padded (first NUL determines length, otherwise full array). -- field name: [max_fs_name_len]u8; -+ field name: [8]u8; - - /// String identifier of a file system driver (e.g. `FAT32`, `NFS`, ...) - /// - /// Encoding: - /// - UTF-8 - /// - NUL-padded (first NUL determines length, otherwise full array). -- field filesystem: [max_fs_type_len]u8; -+ field filesystem: [32]u8; - - bitstruct Flags : u16 { - /// This is the system boot filesystem. -@@ -6129,7 +6133,7 @@ namespace fs { - /// Additional packed information. - field flags: Flags; - -- enum FileType : u2 { -+ enum FileType : u8 { - item file = 0; - item directory = 1; - } -@@ -6144,7 +6148,7 @@ namespace fs { - /// `modified_date` is valid. - field modified_date_valid: bool; - -- reserve u12 = 0; -+ reserve u6 = 0; - } - } - -@@ -6156,7 +6160,7 @@ namespace fs { - /// NOTE: `len` is always `<= max_file_name_len`. - struct FileName { - field len: u8; -- field bytes: [max_file_name_len]u8; -+ field bytes: [120]u8; - } - - /// Creates a directory enumerator for `dir`. -@@ -7694,8 +7698,8 @@ namespace gui { - union WidgetEvent { - field event_type: Type; - -- field mouse: MouseEvent; -- field keyboard: KeyboardEvent; -+ field mouse: input.InputEvent; -+ field keyboard: input.InputEvent; - field control: WidgetControlMessage; - - //? TODO: Add event data -@@ -7853,8 +7857,8 @@ namespace gui { - union WindowEvent { - field event_type: Type; - -- field mouse: MouseEvent; -- field keyboard: KeyboardEvent; -+ field mouse: input.InputEvent; -+ field keyboard: input.InputEvent; - field widget_notify: WidgetNotifyEvent; - - enum Type : u16 { -@@ -8222,7 +8226,7 @@ namespace service { - - /// Defines the type of argument and the potential transformations the - /// kernel performs when passing the argument between caller and callee. -- enum MarshalType : u2 { -+ enum MarshalType : u8 { - /// The argument/result is unused. - /// Unused values must be set to zero or otherwise the call/return is invalid. - item unused = 0; -@@ -8256,7 +8260,7 @@ namespace service { - /// The kernel interprets resource handles in `results` in the *calling thread's current resource context*. - /// For each non-zero valid handle, it creates a strong binding for the resource context that scheduled - /// the original `Invoke` request and returns the translated handle in `Invoke.results`. -- item resource = 3; -+ item @"resource" = 3; - } - - /// Defines the signature of a function or overlapped operation. -@@ -8267,7 +8271,7 @@ namespace service { - /// NOTE: For both `inputs` and `outputs`: As soon as an index in the - /// array is `MarshalType.unused`, all following items must also be - /// `MarshalType.unused`. -- bitstruct FunctionSignature : u32 { -+ struct FunctionSignature { - /// Defines the number and type of the input arguments. - /// `inputs[0]` is the first argument, `inputs[7]` is the eighth argument. - field inputs: [8]MarshalType; -@@ -8305,7 +8309,7 @@ namespace service { - /// NOTE: The handler should perform a strong binding of resources inside `arguments` if it must - /// retain independent access even if the caller later unbinds/releases its handles. - /// This still does not prevent explicit destruction of the resource. -- typedef AsyncHandler = fnptr(context: ?*anyopaque, request: RequestToken, operation: u8, arguments: *const [8]usize) void; -+ typedef AsyncHandler = fnptr(anyptr,RequestToken, u8, *const [8]usize) void; - - /// The signature of the asynchronous cancellation handler function registered by an interface. - /// -@@ -8320,7 +8324,7 @@ namespace service { - /// - /// NOTE: A handler function should not yield the executing thread, as this will generate - /// hard to debug scenarios. -- typedef CancelHandler = fnptr(context: ?*anyopaque, request: RequestToken) void; -+ typedef CancelHandler = fnptr(anyptr,RequestToken) void; - - /// The signature of a synchronous function call registered by an interface. - /// -@@ -8336,7 +8340,7 @@ namespace service { - /// hard to debug scenarios. - /// - /// NOTE: `arguments` must be assumed invalid after the return of the callback. -- typedef Function = fnptr(context: ?*anyopaque, arguments: *const [8]usize) usize; -+ typedef Function = fnptr(anyptr,*const [8]usize) usize; - - /// Creates a new interface that can be invoked by other processes. - /// -@@ -8373,7 +8377,7 @@ namespace service { - /// NOTE: This can be used to implement a stateful interface that allows a process to create - /// the same interface more than once and still have context which of the interfaces were - /// called. -- in context: ?*anyopaque; -+ in context: anyptr; - - /// The synchronous implementation functions (vtable). - /// From 7dd6741d14408c5060bcb217c51ebb42959e5660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 21:28:58 +0200 Subject: [PATCH 33/36] Installs libgtk-3 in smoketest CI --- .github/workflows/smoketest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index 6bb5fe74..9debd1b2 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -63,7 +63,7 @@ jobs: - name: Install QEMU uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: qemu-system + packages: qemu-system libgtk-3-dev version: 1.1 - name: Build ${{ matrix.platform.kernel }} From 074cfa6820f0201715f92e63b48158ac726c0960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 21:45:08 +0200 Subject: [PATCH 34/36] Adds paranoia debugging option with assertions in fmt_id --- src/abi/utility/render_zig_code.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/abi/utility/render_zig_code.zig b/src/abi/utility/render_zig_code.zig index 3074835a..6cae94eb 100644 --- a/src/abi/utility/render_zig_code.zig +++ b/src/abi/utility/render_zig_code.zig @@ -5,7 +5,6 @@ const code_writer = @import("code_writer.zig"); const patch_parser = @import("patch_parser.zig"); -const fmt_id = std.zig.fmtId; const fmt_escapes = std.zig.fmtString; const model = abi_parser.model; @@ -856,6 +855,7 @@ const ZigRenderer = struct { try zr.writer.writeln("}"); } if (arc.native_outputs.len == 1) { + assert_unpadded_name(arc.native_outputs[0].name); try zr.writer.writeln(""); try zr.writer.println("pub fn get_output(arc: *const @This()) !*const @FieldType(Outputs, \"{s}\") {{", .{arc.native_outputs[0].name}); zr.writer.indent(); @@ -1279,6 +1279,15 @@ const FqnFmt = struct { } }; +fn assert_unpadded_name(name: []const u8) void { + std.debug.assert(std.mem.trim(u8, name, " \r\n\t").len == name.len); +} + +fn fmt_id(id: []const u8) @TypeOf(std.zig.fmtId(id)) { + assert_unpadded_name(id); + return std.zig.fmtId(id); +} + fn fmt_local(id: []const u8) std.fmt.Formatter([]const u8, format_local) { return .{ .data = id }; } From d765ff7097162c2e84926a2628399c77c0e649fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 22:04:56 +0200 Subject: [PATCH 35/36] Uploads .zig-cache on failure --- .github/workflows/build.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 779fc8b3..95404278 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,7 @@ name: Build env: ZIG_VERSION: "0.15.2" ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-global-cache + ZIG_LOCAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache on: push: @@ -54,6 +55,16 @@ jobs: run: | zig build --global-cache-dir "$ZIG_GLOBAL_CACHE_DIR" ${{ matrix.platform }} + - name: Upload .zig-cache on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && (matrix.os == 'windows-latest' || matrix.os == 'macos-latest') }} + with: + name: zig-cache-build-${{ matrix.os }}-${{ matrix.platform }} + path: ${{ env.ZIG_LOCAL_CACHE_DIR }} + include-hidden-files: true + if-no-files-found: warn + retention-days: 7 + - name: Upload disk image uses: actions/upload-artifact@v4 if: ${{ matrix.os == 'ubuntu-latest' }} @@ -105,6 +116,16 @@ jobs: run: | zig build --global-cache-dir "$ZIG_GLOBAL_CACHE_DIR" tools + - name: Upload .zig-cache on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && (matrix.os == 'windows-latest' || matrix.os == 'macos-latest') }} + with: + name: zig-cache-tools-${{ matrix.os }} + path: ${{ env.ZIG_LOCAL_CACHE_DIR }} + include-hidden-files: true + if-no-files-found: warn + retention-days: 7 + - name: Upload tools uses: actions/upload-artifact@v4 with: From 87faa72be5cb2334bec69aa4ca78ebe213f13cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 16 Jun 2026 23:18:22 +0200 Subject: [PATCH 36/36] Adjusts formatting code such that it renders escape codes --- src/abi/utility/render_zig_code.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abi/utility/render_zig_code.zig b/src/abi/utility/render_zig_code.zig index 6cae94eb..f6ac5233 100644 --- a/src/abi/utility/render_zig_code.zig +++ b/src/abi/utility/render_zig_code.zig @@ -857,7 +857,7 @@ const ZigRenderer = struct { if (arc.native_outputs.len == 1) { assert_unpadded_name(arc.native_outputs[0].name); try zr.writer.writeln(""); - try zr.writer.println("pub fn get_output(arc: *const @This()) !*const @FieldType(Outputs, \"{s}\") {{", .{arc.native_outputs[0].name}); + try zr.writer.println("pub fn get_output(arc: *const @This()) !*const @FieldType(Outputs, \"{f}\") {{", .{fmt_escapes(arc.native_outputs[0].name)}); zr.writer.indent(); try zr.writer.writeln("try arc.check_error();"); try zr.writer.println("return &arc.outputs.{f};", .{fmt_id(arc.native_outputs[0].name)});