diff --git a/bench/site_facts.py b/bench/site_facts.py
index 577c2de..371c6d3 100755
--- a/bench/site_facts.py
+++ b/bench/site_facts.py
@@ -22,20 +22,23 @@
stated flatly.
RECORDED. Seconds, bytes, milliseconds. These are a machine, not a language,
-and README.md is scrupulous about saying so: it names the exact box and it
-publishes other sessions reading 0.16 / 1.29 / 0.19 and 0.22 / 1.81 / 0.27
-where the table's read 0.13 / 1.17 / 0.16. So a recorded number is never emitted on its own. It is
-emitted as a group carrying the machine that produced it, the command that
-prints it, and every session README.md publishes, so the site can draw the
-spread and lead with the ratio. The ratio is the claim. The second is a
-circumstance.
-
-README.md is the source for the recorded class because that is where this
-project publishes these measurements. Parsing it means the site restates the
-repository rather than remembering it. When a pattern below stops matching,
-that is a failure and not a default: a sentence this was written to cover was
-reworded, so the site would be quoting something nobody is maintaining. Same
-rule `doc_numbers.py` already applies to itself.
+and the repository is scrupulous about saying so: BENCHMARKS.md names the
+exact box and publishes other sessions reading 0.16 / 1.29 / 0.19 and
+0.22 / 1.81 / 0.27 where the table's read 0.13 / 1.17 / 0.16. So a recorded
+number is never emitted on its own. It is emitted as a group carrying the
+machine that produced it, the command that prints it, and every session
+BENCHMARKS.md publishes, so the site can draw the spread and lead with the
+ratio. The ratio is the claim. The second is a circumstance.
+
+README.md and BENCHMARKS.md are the source for the recorded class because
+that is where this project publishes these measurements: the claims a reader
+meets first in README.md, and since 0.20.1 the build, run-time and server
+tables behind them in BENCHMARKS.md. Each pattern below names the file it
+reads. Parsing them means the site restates the repository rather than
+remembering it. When a pattern stops matching, that is a failure and not a
+default: a sentence this was written to cover was reworded or moved, so the
+site would be quoting something nobody is maintaining. Same rule
+`doc_numbers.py` already applies to itself.
"""
from __future__ import annotations
@@ -64,7 +67,8 @@
def flat(text: str) -> str:
- """README.md wraps at about 80 columns, so match against one long line."""
+ """The repository's prose wraps at about 80 columns, so match against one
+ long line."""
return re.sub(r"\s+", " ", text)
@@ -301,74 +305,85 @@ def context_scope() -> dict:
}
-# (key, pattern, group names). A miss is an error, never a default.
-RECORDED_PATTERNS: list[tuple[str, str, tuple[str, ...]]] = [
- ("machine", r"\(([^()]*Ryzen[^()]*)\)", ("name",)),
+# The two files the recorded class is published in.
+README = "README.md"
+BENCHMARKS = "BENCHMARKS.md"
+
+# (key, file, pattern, group names). A miss is an error, never a default.
+RECORDED_PATTERNS: list[tuple[str, str, str, tuple[str, ...]]] = [
+ ("machine", BENCHMARKS, r"\(([^()]*Ryzen[^()]*)\)", ("name",)),
(
"edit_loop",
+ BENCHMARKS,
r"\| \*\*one module's body\*\* \| \*\*([\d.]+)\*\* \| ([\d.]+) \| ([\d.]+) \|",
("iyi", "crystal", "go"),
),
(
"edit_loop_sessions",
+ BENCHMARKS,
r"other sessions on the same box read (\d+\.\d+ / \d+\.\d+ / \d+\.\d+) and\s+"
r"(\d+\.\d+ / \d+\.\d+ / \d+\.\d+) on the first row",
("a", "b"),
),
- ("edit_loop_ratio", r"costs \*\*([\d.]+)x less\*\*", ("times",)),
+ ("edit_loop_ratio", README, r"costs \*\*([\d.]+)x less\*\*", ("times",)),
(
"hello_binary",
+ README,
r"is a (?:\*\*)?([\d,]+) KB(?:\*\*)? binary that starts in (?:\*\*)?([\d.]+) ms",
("kb", "ms"),
),
(
"crystal_binary",
+ README,
r"library is ([\d,]+) KB and ([\d.]+) ms",
("kb", "ms"),
),
(
"full_build",
+ BENCHMARKS,
r"\| generated pair, ([\d,]+) lines \| ([\d.]+) s \| \*\*([\d.]+) s\*\* \|",
("lines", "iyi", "go"),
),
(
"hello_build",
+ BENCHMARKS,
r"\| `hello` \(([\d,]+) lines\) \| \*\*([\d.]+) s\*\* \| ([\d.]+) s \|",
("lines", "iyi", "go"),
),
- ("front_end", r"answers `hello` in \*\*([\d.]+) s\*\*", ("seconds",)),
+ ("front_end", README, r"answers `hello` in \*\*([\d.]+) s\*\*", ("seconds",)),
(
"runtime_string",
+ BENCHMARKS,
r"\| string building \| ([\d.]+)x \| \*\*([\d.]+)x\*\* \|",
("as_it_runs", "gc_off"),
),
(
"runtime_hash",
+ BENCHMARKS,
r"\| hash insert and read \| ([\d.]+)x \| ([\d.]+)x \|",
("as_it_runs", "gc_off"),
),
(
"runtime_array",
+ BENCHMARKS,
r"\| array append and read \| ([\d.]+)x \| ([\d.]+)x \|",
("as_it_runs", "gc_off"),
),
(
"runtime_arithmetic",
+ BENCHMARKS,
r"\| arithmetic \| ([\d.]+)x \| ([\d.]+)x \|",
("as_it_runs", "gc_off"),
),
- (
- "prelude_vs_crystal",
- r"([\d,]+) lines of Crystal's standard library instead of ([\d,]+)",
- ("crystal", "iyi"),
- ),
(
"context_pack",
+ README,
r"at \*\*(\d+)–(\d+)%\*\* of the sources' size",
("size_low", "size_high"),
),
(
"context_tokens",
+ README,
r"\*\*(\d+)–(\d+)% fewer prompt tokens\*\*",
("low", "high"),
),
@@ -380,17 +395,19 @@ def context_scope() -> dict:
# up, and the site says that rather than a count nobody maintains.
(
"context_runs",
+ README,
r"over the runs written up in ([A-Z_]+\.md §\d+)",
("where",),
),
(
"context_rounds",
+ README,
r"rounds to green tied ([a-z]+) times and lost\s+([a-z]+)\.",
("tied", "lost"),
),
]
-# Which command prints each recorded group, quoted from README.md's own
+# Which command prints each recorded group, quoted from the repository's own
# attribution of it. The site shows this beside the number.
COMMANDS: dict[str, str] = {
"edit_loop": "python3 bench/incremental.py",
@@ -421,16 +438,16 @@ def context_scope() -> dict:
}
-def recorded(readme: str) -> tuple[dict, list[str]]:
- text = flat(readme)
+def recorded() -> tuple[dict, list[str]]:
+ texts = {name: flat((REPO / name).read_text()) for name in (README, BENCHMARKS)}
out: dict = {}
missing: list[str] = []
- for key, pattern, names in RECORDED_PATTERNS:
- m = re.search(pattern, text)
+ for key, source, pattern, names in RECORDED_PATTERNS:
+ m = re.search(pattern, texts[source])
if not m:
missing.append(
- f"{key}: /{pattern}/ no longer matches README.md. The sentence "
- f"this quoted was reworded or removed, so the site would be "
+ f"{key}: /{pattern}/ no longer matches {source}. The sentence "
+ f"this quoted was reworded or moved, so the site would be "
f"publishing a number nobody is maintaining"
)
continue
@@ -439,16 +456,18 @@ def recorded(readme: str) -> tuple[dict, list[str]]:
def sessions(raw: dict) -> list[dict[str, float]]:
- """Every session README.md publishes for the edit loop, not just the best.
-
- The spread is the point. README.md prints three readings and says to read
- the columns against each other because they pay the same machine together,
- so the site draws all of them and leads with the ratio they agree on.
-
- README.md prints a third column, another compiler's, on the same row. The
- site publishes iyi against the compiler it forked and nothing else, so that
- column is parsed, because a drift in the row is still a drift, and then
- dropped.
+ """Every session BENCHMARKS.md publishes for the edit loop, not just the
+ best.
+
+ The spread is the point. BENCHMARKS.md prints three readings and says to
+ read the columns against each other because they pay the same machine
+ together, so the site draws all of them and leads with the ratio they
+ agree on.
+
+ BENCHMARKS.md prints a third column, another compiler's, on the same row.
+ The site publishes iyi against the compiler it forked and nothing else, so
+ that column is parsed, because a drift in the row is still a drift, and
+ then dropped.
"""
# The table's own row is the first reading; the sentence under it gives
# the other two sessions.
@@ -465,8 +484,8 @@ def machine(name: str) -> str:
def build() -> tuple[dict, list[str]]:
- readme = (REPO / "README.md").read_text()
- raw, missing = recorded(readme)
+ readme = (REPO / README).read_text()
+ raw, missing = recorded()
if missing:
return {}, missing
@@ -487,7 +506,7 @@ def build() -> tuple[dict, list[str]]:
"recorded": {
"edit_loop": {
# The claim, which is machine-independent and held across every
- # session README.md publishes.
+ # session BENCHMARKS.md publishes.
"claim": {
"kind": "ratio",
"value": float(raw["edit_loop_ratio"]["times"]),
@@ -547,14 +566,6 @@ def build() -> tuple[dict, list[str]]:
"command": COMMANDS["runtime_string"],
"subject": "the same program under both libraries",
},
- "library_size": {
- "iyi": int(raw["prelude_vs_crystal"]["iyi"].replace(",", "")),
- "crystal": int(raw["prelude_vs_crystal"]["crystal"].replace(",", "")),
- "unit": "lines",
- "machine": None,
- "command": "python3 bench/doc_numbers.py",
- "subject": "what a program has",
- },
"front_end": {
"seconds": float(raw["front_end"]["seconds"]),
"unit": "s",
@@ -586,7 +597,7 @@ def build() -> tuple[dict, list[str]]:
"bench/doc_numbers.py measured(), the bench's own project "
"generator, and git"
),
- "recorded_source": "README.md, parsed",
+ "recorded_source": "README.md and BENCHMARKS.md, parsed",
"generator": "bench/site_facts.py",
},
}
diff --git a/doc/STACK.md b/doc/STACK.md
index d1e6457..b6ba8a5 100644
--- a/doc/STACK.md
+++ b/doc/STACK.md
@@ -43,8 +43,9 @@ site/
reads the iyi tree at it, and refuses a recording
taken from anything else
facts.mjs runs bench/site_facts.py, writes generated/facts.json
- targets.mjs reads the platforms out of iyi's CI workflow and the
- run set out of README.md, writes generated/targets.json
+ targets.mjs reads the platforms out of iyi's CI workflow, the
+ run set out of README.md and the object audit out
+ of INSTALL.md, writes generated/targets.json
releases.mjs reads CHANGELOG.md's release headings, writes
generated/releases.json
samples.mjs transcludes samples/iyi, samples/tour, records/break
@@ -76,12 +77,12 @@ This is the load-bearing part, and it is the reason the site cannot drift.
repository through `bench/site_facts.py`, which has two sources. Structural
numbers come from `bench/doc_numbers.py`'s `measured()`, the same function that
already gates `README.md` in CI, so the page and the gate cannot disagree.
-Recorded numbers (seconds, bytes, milliseconds) are parsed out of `README.md`,
-because that is where this project publishes them, and they are emitted only as
-groups carrying the machine that produced them, the command that prints them,
-and every session the README publishes. If any pattern stops matching, the
-script fails rather than defaulting, which is the same rule
-`bench/doc_numbers.py` applies to itself.
+Recorded numbers (seconds, bytes, milliseconds) are parsed out of `README.md`
+and `BENCHMARKS.md`, because that is where this project publishes them, and
+they are emitted only as groups carrying the machine that produced them, the
+command that prints them, and every session BENCHMARKS.md publishes. If any
+pattern stops matching, the script fails rather than defaulting, which is the
+same rule `bench/doc_numbers.py` applies to itself.
The build chain runs `bench/site_facts.py` and writes
`src/generated/facts.json`. The components read that file. `Measure`
diff --git a/records/agent.json b/records/agent.json
index 40e4ea6..6447e61 100644
--- a/records/agent.json
+++ b/records/agent.json
@@ -1,10 +1,10 @@
{
"recorded": {
- "compiler": "iyi 0.20.0 (built on Crystal 1.22.0-dev [224ed74fc] (2026-11-02))",
- "commit": "224ed74fc7ba9d78345dfd10f21ece4b499702fa",
+ "compiler": "iyi 0.20.1 (built on Crystal 1.22.0-dev [91d012b1e] (2026-11-04))",
+ "commit": "91d012b1ed978c7df5cc033624ec161a2daf01b6",
"machine": "Linux 7.0.0-38-generic x86_64, QEMU Virtual CPU version 2.5+",
"command": "npm run record:agent",
- "when": "2026-11-02T07:19:31.432Z"
+ "when": "2026-11-04T04:32:53.960Z"
},
"project": [
{
@@ -104,7 +104,7 @@
"command": "iyi test --json .",
"exitCode": 0,
"payload": "stdout",
- "stdout": "{\"tests\":[{\"file\":\"./app_test.iyi\",\"status\":\"pass\",\"seconds\":0.394}],\"passed\":1,\"failed\":0,\"skipped\":0}\n",
+ "stdout": "{\"tests\":[{\"file\":\"./app_test.iyi\",\"status\":\"pass\",\"seconds\":0.281}],\"passed\":1,\"failed\":0,\"skipped\":0}\n",
"stderr": ""
},
{
@@ -167,7 +167,7 @@
},
"serverInfo": {
"name": "iyi",
- "version": "0.20.0"
+ "version": "0.20.1"
}
},
"tools": [
diff --git a/records/diagnostics.json b/records/diagnostics.json
index 2d10ef1..029a842 100644
--- a/records/diagnostics.json
+++ b/records/diagnostics.json
@@ -1,10 +1,10 @@
{
"recorded": {
- "compiler": "iyi 0.20.0 (built on Crystal 1.22.0-dev [224ed74fc] (2026-11-02))",
- "commit": "224ed74fc7ba9d78345dfd10f21ece4b499702fa",
+ "compiler": "iyi 0.20.1 (built on Crystal 1.22.0-dev [91d012b1e] (2026-11-04))",
+ "commit": "91d012b1ed978c7df5cc033624ec161a2daf01b6",
"machine": "Linux 7.0.0-38-generic x86_64, QEMU Virtual CPU version 2.5+",
"command": "npm run record:diagnostics",
- "when": "2026-11-02T07:19:23.048Z"
+ "when": "2026-11-04T04:32:47.944Z"
},
"cases": [
{
diff --git a/records/highlight.json b/records/highlight.json
index e4b069a..f2a119a 100644
--- a/records/highlight.json
+++ b/records/highlight.json
@@ -1,10 +1,10 @@
{
"recorded": {
- "compiler": "iyi 0.20.0 (built on Crystal 1.22.0-dev [224ed74fc] (2026-11-02))",
- "commit": "224ed74fc7ba9d78345dfd10f21ece4b499702fa",
+ "compiler": "iyi 0.20.1 (built on Crystal 1.22.0-dev [91d012b1e] (2026-11-04))",
+ "commit": "91d012b1ed978c7df5cc033624ec161a2daf01b6",
"machine": "Linux 7.0.0-38-generic x86_64, QEMU Virtual CPU version 2.5+",
"command": "npm run record:highlight",
- "when": "2026-11-02T07:19:35.714Z"
+ "when": "2026-11-04T04:32:57.045Z"
},
"files": {
"samples/iyi/app/formal.iyi": "# A second module, exporting a `title` of its own. See `../modules.iyi`.\nmodule app/formal\n\npub def title : String\n "esteemed colleague"\nend\n\npub def address(name : String) : String\n "Good day, #{name}."\nend\n",
@@ -15,7 +15,7 @@
"samples/iyi/calc/ast.iyi": "module calc/ast\n\n# What an evaluated program can go wrong with. Both are ordinary members of the\n# union an evaluation returns (SPEC.md III.1), so a caller either handles them\n# or propagates with `!`.\npub struct UnknownName\n getter name : String\n\n def initialize(@name : String)\n end\nend\n\nimpl Error for UnknownName\n def message : String\n "unknown name '#{name}'"\n end\nend\n\npub struct DividedByZero\n def initialize\n end\nend\n\nimpl Error for DividedByZero\n def message : String\n "divided by zero"\n end\nend\n\n# The one thing every node can do. A trait rather than a base class, because\n# there is nothing to inherit and R-3 has no opinion to break here.\npub trait Node\n abstract def evaluate(scope : Hash(String, Int32)) : Int32 | UnknownName | DividedByZero\nend\n\npub struct Number\n getter value : Int32\n\n def initialize(@value : Int32)\n end\nend\n\nimpl Node for Number\n def evaluate(scope : Hash(String, Int32)) : Int32 | UnknownName | DividedByZero\n value\n end\nend\n\npub struct Name\n getter name : String\n\n def initialize(@name : String)\n end\nend\n\nimpl Node for Name\n def evaluate(scope : Hash(String, Int32)) : Int32 | UnknownName | DividedByZero\n return UnknownName.new(name) unless scope.has_key?(name)\n scope[name]\n end\nend\n\npub class Binary\n getter op : Char\n getter left : Number | Name | Binary\n getter right : Number | Name | Binary\n\n def initialize(@op : Char, @left : Number | Name | Binary, @right : Number | Name | Binary)\n end\nend\n\nimpl Node for Binary\n def evaluate(scope : Hash(String, Int32)) : Int32 | UnknownName | DividedByZero\n a = left.evaluate(scope)!\n b = right.evaluate(scope)!\n\n if op == '+'\n a + b\n elsif op == '-'\n a - b\n elsif op == '*'\n a * b\n else\n return DividedByZero.new if b == 0\n a // b\n end\n end\nend\n",
"samples/iyi/calc/lexer.iyi": "module calc/lexer\n\n# What the scanner produces. A token is its kind, the text it covers and where\n# it started, because an error that cannot say where is half an error.\npub struct Token\n getter kind : String\n getter text : String\n getter at : Int32\n\n def initialize(@kind : String, @text : String, @at : Int32)\n end\nend\n\npub struct BadCharacter\n getter at : Int32\n getter found : Char\n\n def initialize(@at : Int32, @found : Char)\n end\nend\n\nimpl Error for BadCharacter\n def message : String\n "unexpected character at #{at}"\n end\nend\n\npub def scan(source : String) : Array(Token) | BadCharacter\n tokens = [] of Token\n index = 0\n\n while index < source.size\n c = source[index]\n\n # A line ends in CRLF on Windows: `echo 2 + 3 * 4| iyi run\n # samples\\iyi\\calc.iyi` in cmd answered "unexpected character at 9",\n # the `\\r`.\n if c == ' ' || c == '\\n' || c == '\\t' || c == '\\r'\n index = index + 1\n elsif digit?(c)\n start = index\n while index < source.size && digit?(source[index])\n index = index + 1\n end\n tokens << Token.new("number", source[start, index - start], start)\n elsif letter?(c)\n start = index\n while index < source.size && (letter?(source[index]) || digit?(source[index]))\n index = index + 1\n end\n tokens << Token.new("name", source[start, index - start], start)\n elsif operator?(c)\n tokens << Token.new("op", source[index, 1], index)\n index = index + 1\n else\n return BadCharacter.new(index, c)\n end\n end\n\n tokens << Token.new("end", "", index)\n tokens\nend\n\ndef digit?(c : Char) : Bool\n c >= '0' && c <= '9'\nend\n\ndef letter?(c : Char) : Bool\n (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'\nend\n\ndef operator?(c : Char) : Bool\n c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')' || c == '='\nend\n",
"samples/iyi/calc/parser.iyi": "module calc/parser\n\nimport calc/lexer::{Token}\nimport calc/ast::{Number, Name, Binary}\n\npub struct SyntaxError\n getter at : Int32\n getter wanted : String\n\n def initialize(@at : Int32, @wanted : String)\n end\nend\n\nimpl Error for SyntaxError\n def message : String\n "expected #{wanted} at #{at}"\n end\nend\n\n# One statement: `name = expression`, or an expression on its own, which the\n# runner prints. Kept as a pair rather than a node type, because a statement is\n# not something this language evaluates twice.\npub struct Statement\n getter target : String\n getter body : Number | Name | Binary\n\n def initialize(@target : String, @body : Number | Name | Binary)\n end\nend\n\n# A cursor over the tokens. A struct with an index rather than a shifting array,\n# because backing up one token is the parser's whole trick.\npub class Reader\n getter tokens : Array(Token)\n getter at : Int32\n\n def initialize(@tokens : Array(Token))\n @at = 0\n end\n\n def peek : Token\n tokens[at]\n end\n\n def advance : Token\n token = tokens[at]\n @at = at + 1\n token\n end\nend\n\npub def parse(tokens : Array(Token)) : Array(Statement) | SyntaxError\n reader = Reader.new(tokens)\n statements = [] of Statement\n\n while reader.peek.kind != "end"\n statements << statement(reader)!\n end\n\n statements\nend\n\ndef statement(reader : Reader) : Statement | SyntaxError\n first = reader.peek\n\n if first.kind == "name" && reader.tokens[reader.at + 1].text == "="\n reader.advance\n reader.advance\n Statement.new(first.text, expression(reader)!)\n else\n Statement.new("", expression(reader)!)\n end\nend\n\ndef expression(reader : Reader) : Number | Name | Binary | SyntaxError\n left = term(reader)!\n\n while reader.peek.kind == "op" && (reader.peek.text == "+" || reader.peek.text == "-")\n op = reader.advance.text[0]\n left = Binary.new(op, left, term(reader)!)\n end\n\n left\nend\n\ndef term(reader : Reader) : Number | Name | Binary | SyntaxError\n left = factor(reader)!\n\n while reader.peek.kind == "op" && (reader.peek.text == "*" || reader.peek.text == "/")\n op = reader.advance.text[0]\n left = Binary.new(op, left, factor(reader)!)\n end\n\n left\nend\n\ndef factor(reader : Reader) : Number | Name | Binary | SyntaxError\n token = reader.advance\n\n if token.kind == "number"\n Number.new(token.text.to_i)\n elsif token.kind == "name"\n Name.new(token.text)\n elsif token.text == "("\n inside = expression(reader)!\n closing = reader.advance\n return SyntaxError.new(closing.at, "')'") unless closing.text == ")"\n inside\n else\n SyntaxError.new(token.at, "a number, a name or '('")\n end\nend\n",
- "samples/iyi/calc.iyi": "# A language, in the language — the biggest program this prelude can hold.\n#\n# echo "x = 2 + 3 * 4\n# x * 10" | ./bin/iyi run samples/iyi/calc.iyi\n#\n# The other samples each document a rule. This one documents nothing and is\n# here for a different reason: everything else in `samples/iyi` is a page long,\n# and a language that has only been used for pages has not been used. This is\n# three modules and a scanner, a parser and an evaluator, the shape of every\n# real program that reads something, written against iyi's own 25,725-line\n# library and nothing else.\n#\n# What it cost to write is recorded where it landed. `String#[]`, `String#[](start,\n# count)`, `String#to_i` and `read_input` are in the prelude because this file\n# asked for them, which is the rule the prelude grows by (SPEC.md "0.1.0",\n# item 3). Nothing else was missing.\nmodule samples/calc\n\nimport calc/lexer::{scan, BadCharacter}\nimport calc/parser::{parse, Statement, SyntaxError}\nimport calc/ast::{Number, Name, Binary, UnknownName, DividedByZero}\n\n# What runs when nothing is piped in, so that the sample is a program rather\n# than a prompt. CI runs it this way. Its last two lines are errors on\n# purpose, and the demo exits 0 with them.\nDEMO = "x = 2 + 3 * 4\ny = (x - 4) / 2\nx + y * 10\nmissing + 1\n1 / 0"\n\n# Whether every statement answered. An error goes to standard error, and\n# input with one makes the program exit 1: all of them went to standard\n# output at exit 0, so `echo 1 / 0 | iyi run samples/iyi/calc.iyi && next`\n# ran `next`.\npub def run(source : String) : Bool\n tokens = scan(source)\n if tokens.is_a?(BadCharacter)\n stderr.puts "error: #{tokens.message}"\n return false\n end\n\n program = parse(tokens)\n if program.is_a?(SyntaxError)\n stderr.puts "error: #{program.message}"\n return false\n end\n\n answered = true\n scope = {} of String => Int32\n program.each do |statement|\n value = statement.body.evaluate(scope)\n\n if value.is_a?(Int32)\n if statement.target.empty?\n puts value\n else\n scope[statement.target] = value\n end\n else\n # `value` is an error member of the union, and every one of them answers\n # `message` because they implement `Error` (SPEC.md III.1).\n stderr.puts "error: #{value.message}"\n answered = false\n end\n end\n answered\nend\n\n# Input that is only line ends is nothing piped in: PowerShell's `"" |`\n# sends a CRLF. And its pipe starts what it sends with a byte order mark,\n# which is not part of the program: "unexpected character at 0".\ntyped = read_input.lchop('\\uFEFF')\nif typed.strip.empty?\n run(DEMO)\nelsif !run(typed)\n exit 1\nend\n",
+ "samples/iyi/calc.iyi": "# A language, in the language — the biggest program this prelude can hold.\n#\n# echo "x = 2 + 3 * 4\n# x * 10" | ./bin/iyi run samples/iyi/calc.iyi\n#\n# The other samples each document a rule. This one documents nothing and is\n# here for a different reason: everything else in `samples/iyi` is a page long,\n# and a language that has only been used for pages has not been used. This is\n# three modules and a scanner, a parser and an evaluator, the shape of every\n# real program that reads something, written against iyi's own 25,763-line\n# library and nothing else.\n#\n# What it cost to write is recorded where it landed. `String#[]`, `String#[](start,\n# count)`, `String#to_i` and `read_input` are in the prelude because this file\n# asked for them, which is the rule the prelude grows by (SPEC.md "0.1.0",\n# item 3). Nothing else was missing.\nmodule samples/calc\n\nimport calc/lexer::{scan, BadCharacter}\nimport calc/parser::{parse, Statement, SyntaxError}\nimport calc/ast::{Number, Name, Binary, UnknownName, DividedByZero}\n\n# What runs when nothing is piped in, so that the sample is a program rather\n# than a prompt. CI runs it this way. Its last two lines are errors on\n# purpose, and the demo exits 0 with them.\nDEMO = "x = 2 + 3 * 4\ny = (x - 4) / 2\nx + y * 10\nmissing + 1\n1 / 0"\n\n# Whether every statement answered. An error goes to standard error, and\n# input with one makes the program exit 1: all of them went to standard\n# output at exit 0, so `echo 1 / 0 | iyi run samples/iyi/calc.iyi && next`\n# ran `next`.\npub def run(source : String) : Bool\n tokens = scan(source)\n if tokens.is_a?(BadCharacter)\n stderr.puts "error: #{tokens.message}"\n return false\n end\n\n program = parse(tokens)\n if program.is_a?(SyntaxError)\n stderr.puts "error: #{program.message}"\n return false\n end\n\n answered = true\n scope = {} of String => Int32\n program.each do |statement|\n value = statement.body.evaluate(scope)\n\n if value.is_a?(Int32)\n if statement.target.empty?\n puts value\n else\n scope[statement.target] = value\n end\n else\n # `value` is an error member of the union, and every one of them answers\n # `message` because they implement `Error` (SPEC.md III.1).\n stderr.puts "error: #{value.message}"\n answered = false\n end\n end\n answered\nend\n\n# Input that is only line ends is nothing piped in: PowerShell's `"" |`\n# sends a CRLF. And its pipe starts what it sends with a byte order mark,\n# which is not part of the program: "unexpected character at 0".\ntyped = read_input.lchop('\\uFEFF')\nif typed.strip.empty?\n run(DEMO)\nelsif !run(typed)\n exit 1\nend\n",
"samples/iyi/collections.iyi": "# The `Enumerable` port, exercised — SPEC.md II.6.\n#\n# ./bin/iyi run samples/iyi/collections.iyi\n#\n# `std/enumerable.iyi` holds the trait. This file implements it twice and calls\n# every method it defines. Both matter: a default method is only typed when it\n# is called, so a trait that merely compiles has proved nothing about whether\n# its bodies are sound.\n#\n# `Nums` and `Words` answer `Elem` differently — `Int32` and `String` — and each\n# gets the same 57 default methods from one `each`. That is the claim II.6\n# makes, running.\n\nmodule samples/collections\n\nimport std/enumerable::{Enumerable}\n\npub struct Nums\n def initialize(@a : Array(Int32))\n end\nend\n\nimpl Enumerable for Nums\n type Elem = Int32\n\n def each(& : Int32 -> Nil) : Nil\n @a.each { |x| yield x }\n end\nend\n\npub struct Words\n def initialize(@a : Array(String))\n end\nend\n\nimpl Enumerable for Words\n type Elem = String\n\n def each(& : String -> Nil) : Nil\n @a.each { |x| yield x }\n end\nend\n\nn = Nums.new([3, 1, 4, 1, 5])\nw = Words.new(["pear", "fig", "plum", "kiwi"])\n\nputs "to_a #{n.to_a.join(",")}"\nputs "size #{n.size}"\nputs "empty? #{n.empty?} #{Nums.new([] of Int32).empty?}"\nputs "all? #{n.all? { |x| x > 0 }}"\nputs "any? #{n.any? { |x| x > 4 }}"\nputs "none? #{n.none? { |x| x > 9 }}"\nputs "one? #{n.one? { |x| x == 4 }}"\nputs "count #{n.count { |x| x == 1 }}"\nputs "find #{n.find { |x| x > 3 }} #{n.find { |x| x > 9 }.inspect}"\nputs "index #{n.index { |x| x == 4 }} #{n.index { |x| x == 9 }.inspect}"\nputs "first?/first #{n.first?} #{n.first}"\nputs "map #{n.map { |x| x * 2 }.join(",")}"\nputs "flat_map #{n.flat_map { |x| [x, x] }.join(",")}"\nputs "map_with_index #{n.map_with_index { |x, i| x * i }.join(",")}"\nputs "select #{n.select { |x| x > 2 }.join(",")}"\nputs "reject #{n.reject { |x| x > 2 }.join(",")}"\nputs "partition #{n.partition { |x| x > 2 }.map(&.join("+")).join(" | ")}"\nputs "take/skip #{n.take(2).join(",")} #{n.skip(3).join(",")}"\nputs "take_while #{n.take_while { |x| x < 4 }.join(",")}"\nputs "skip_while #{n.skip_while { |x| x < 4 }.join(",")}"\nputs "reduce #{n.reduce(0) { |a, x| a + x }}"\nputs "each_with_index #{n.map_with_index { |x, i| i }.join(",")}"\nputs "each_with_object #{n.each_with_object([] of Int32) { |e, o| o << e * 10 }.join(",")}"\nputs "join #{n.join("-")}"\nputs "includes? #{n.includes?(4)} #{n.includes?(9)}"\nputs "max?/max #{n.max?} #{n.max}"\nputs "min?/min #{n.min?} #{n.min}"\nputs "sum/product #{n.sum} #{n.product}"\nputs "sum of nothing #{Nums.new([] of Int32).sum}"\nputs "tally #{n.tally[1]}"\nputs "sorted #{n.sorted.join(",")}"\nputs "max_by #{w.max_by { |s| s.size }}"\nputs "group_by #{w.group_by { |s| s.size }[4].join(",")}"\nputs "index_by #{w.index_by { |s| s.size }[3]}"\n\nputs "each_slice #{n.each_slice(2).map(&.join("+")).join(" ")}"\nputs "each_cons #{n.each_cons(2).map(&.join("+")).join(" ")}"\nputs "chunk_while #{n.chunk_while { |a, b| b >= a }.map(&.join("+")).join(" ")}"\nputs "first(n) #{n.first(3).join(",")}"\nputs "compact_map #{n.compact_map { |x| x > 2 ? x * 10 : nil }.join(",")}"\nputs "find_value #{n.find_value { |x| x > 3 ? "big" : nil }}"\nputs "accumulate #{n.accumulate(0) { |a, x| a + x }.join(",")}"\nputs "reduce? #{n.reduce? { |a, x| a + x }}"\nputs "in_groups_of #{n.in_groups_of(2, 0).map(&.join("+")).join(" ")}"\nputs "min_by #{w.min_by { |s| s.size }}"\nputs "max_of/min_of #{w.max_of { |s| s.size }} #{w.min_of { |s| s.size }}"\nputs "minmax #{n.minmax[0]},#{n.minmax[1]}"\nputs "tally_by #{w.tally_by { |s| s.size }[4]}"\nputs "sum_by #{w.sum_by { |s| s.size }}"\n\n# SPEC II.6 finding 6: one bound replaces `Indexable | Iterable | Iterator`,\n# and `O::Elem` names what the other collection yields. Four numbers for\n# four words: a shorter other collection is refused, as the other library\n# refuses it.\nputs "zip #{Nums.new([3, 1, 4, 1]).zip(w).map { |pair| "#{pair[0]}#{pair[1]}" }.join(" ")}"\n\ncycled = [] of Int32\nNums.new([1, 2]).cycle(2) { |x| cycled << x }\nputs "cycle #{cycled.join(",")}"\n\n# A trait with an associated type used as a type: one function, any implementer.\ndef describe(e : Enumerable) : String\n "#{e.size} items"\nend\n\nputs "as a type #{describe(n)} / #{describe(w)}"\n\n# ---------------------------------------------------------------------------\n# What this sample would report as an error\n# ---------------------------------------------------------------------------\n#\n# * `Words.new([...]).sum` — `sum` is bounded `where Elem : Num` and\n# `String` implements `Comparable` but not `Num`. The error names the element\n# type and the bound rather than failing somewhere inside the method body.\n#\n# * A second `impl Enumerable for Nums` — a trait with an associated type is\n# implementable once per type, because a second answer for `Elem` would make\n# every call on `Nums` ambiguous.\n#\n# * `impl Num for X` with no `impl Comparable for X` before it —\n# `Num : Comparable` requires it, and the impls have to be written in\n# dependency order.\n",
"samples/iyi/config.iyi": "# The first program that reads text - a `key = value` file, with the line\n# number in its error.\n#\n# ./bin/iyi run samples/iyi/config.iyi\n#\n# Text comes apart into lines, a line into its two halves around a `=`, and\n# each half loses its whitespace; a line that does not parse is an error\n# value carrying where it was (SPEC.md III.1), not a printed message. Like\n# `basics` and `inventory`, this documents no rule: it is here because the\n# prelude grows only when a program in this repository needs something\n# (SPEC.md 0.1.0 item 3), and cutting text is what the third program does.\nmodule samples/config\n\nstruct ParseError\n property line : Int32\n property message : String\n\n def initialize(@line : Int32, @message : String)\n end\nend\n\nimpl Error for ParseError\n def message : String\n "line #{@line}: #{@message}"\n end\nend\n\ndef parse(text : String) : Hash(String, String) | ParseError\n config = {} of String => String\n text.lines.each_with_index do |raw, index|\n line = raw.strip\n next if line.empty? || line.starts_with?('#')\n eq = line.index('=')\n return ParseError.new(index + 1, "no '=' in #{line.inspect}") unless eq\n key = line[0, eq].strip\n return ParseError.new(index + 1, "empty key") if key.empty?\n config[key] = line[eq + 1, line.size - eq - 1].strip\n end\n config\nend\n\ngood = "# the service\\nname = iyi\\nport= 8080\\n\\nmode =fast\\n"\ncase parse(good)\nin Hash(String, String)\n puts it # => {"name" => "iyi", "port" => "8080", "mode" => "fast"}\n puts it["port"].to_i + 1 # => 8081\n puts it.keys.join(", ") # => name, port, mode\nin ParseError then puts "error: #{it.message}"\nend\n\nbad = "name = iyi\\nbroken line\\n"\ncase parse(bad)\nin Hash(String, String) then puts "parsed"\nin ParseError then puts "error: #{it.message}" # => error: line 2: no '=' in "broken line"\nend\n\n# The pieces a parser cuts, on their own.\nputs "a,,b".split(',') # => ["a", "", "b"]\nputs "one\\ntwo\\n".lines # => ["one", "two"]\nputs " padded\\t".strip.inspect # => "padded"\nputs "config.iyi".ends_with?(".iyi") # => true\n",
"samples/iyi/cores.iyi": "# Work on every core - SPEC.md III.4.13, as a person writes it.\n#\n# ./bin/iyi run samples/iyi/cores.iyi\n#\n# `spawn` is a task on the thread that spawned it, and its block may touch\n# anything; `spawn_parallel` is a task a pool of workers runs on any core,\n# and its block may capture only what is `Share` (III.4.4). std/http's\n# `Server.serve` puts each connection on that pool, so its block is checked\n# the same way. When a block reaches module state through a call, the\n# compiler warns, and `@[SafeReach("why")]` on the state says why it is safe.\nmodule samples/cores\n\nimport std/http::{HTTP, Response, Server}\nimport std/socket::{IyiSocket}\nimport std/mutex::{Mutex}\n\ndef collatz_steps(n : Int64) : Int64\n steps = 0_i64\n while n != 1\n n = n % 2 == 0 ? n // 2 : 3 * n + 1\n steps += 1\n end\n steps\nend\n\n# 1. `spawn` and `spawn_parallel`.\n#\n# The tasks `spawn` starts share their spawner's thread, so they may append\n# to an `Array` they captured. The tasks `spawn_parallel` starts run on\n# whichever core is free; each answers its own value, and a task's answer\n# need not be `Share`, because it is handed over once, after the task ends.\nseen = [] of Int64\ngroup do |g|\n (1..3).each do |n|\n g.spawn do\n seen << collatz_steps(n.to_i64)\n 0\n end\n end\nend\nputs seen.sorted # => [0, 1, 7]\n\nlongest = 0_i64\ngroup do |g|\n handles = [] of IyiTask(Int64)\n 4.times do |part|\n handles << g.spawn_parallel do\n best = 0_i64\n start = part.to_i64 * 25_000 + 1\n (start..start + 24_999).each { |n| best = collatz_steps(n) if collatz_steps(n) > best }\n best\n end\n end\n handles.each do |handle|\n steps = handle.value\n longest = steps if steps.is_a?(Int64) && steps > longest\n end\nend\nputs longest # => 350, the longest chain below 100,000\n\n# 2. State the pool may share.\n#\n# A counter that every core adds to is an `Atomic`; an `Array` that every\n# core appends to sits behind a `Mutex`. Both are `Share`, so a parallel\n# block may capture them.\nhits = Atomic(Int64).new(0_i64)\npaths = Mutex.new { [] of String }\n\n# 3. Module state a call reaches.\n#\n# A parallel block that names `GREETING` is refused, because an `Array` is\n# not `Share`. One that only calls `greeting` used to compile with nothing\n# said; now the compiler warns at the block, naming the path. This constant\n# is set before the server starts and only read afterwards, so it says so.\n@[SafeReach("set once before the server starts, then only read")]\nGREETING = ["hello", "from", "every", "core"]\n\ndef greeting : String\n GREETING.join(" ")\nend\n\n# 4. A server on every core.\n#\n# `Server.serve` runs each connection on the pool's workers, so its block is\n# checked as `spawn_parallel`'s is. It stops through a channel: closing the\n# listener from inside the block would capture it, and `/stop` may arrive on\n# any worker. `workers: 1` keeps the one-thread server, whose block may\n# capture anything.\nlistener = IyiSocket.listen("127.0.0.1", 0)\nbase = "http://127.0.0.1:#{listener.local_port}"\nstop = Channel(Int32).new(1)\ngroup do |g|\n g.spawn do\n stop.receive\n listener.close\n 0\n end\n g.spawn do\n Server.serve(listener) do |req|\n hits.add(1_i64)\n path = req.path\n paths.lock { |list| list << path }\n if path == "/stop"\n stop.send(0)\n Response.new(200, "bye")\n else\n Response.new(200, greeting)\n end\n end\n 0\n end\n g.spawn do\n puts HTTP.get(base + "/").or_panic.body # => hello from every core\n HTTP.get(base + "/stop").or_panic\n 0\n end\nend\nputs hits.get # => 2\npaths.lock { |list| puts list.sorted } # => ["/", "/stop"]\n",
diff --git a/records/stdlib.json b/records/stdlib.json
index 41d10b5..74348df 100644
--- a/records/stdlib.json
+++ b/records/stdlib.json
@@ -1,10 +1,10 @@
{
"recorded": {
- "compiler": "iyi 0.20.0 (built on Crystal 1.22.0-dev [224ed74fc] (2026-11-02))",
- "commit": "224ed74fc7ba9d78345dfd10f21ece4b499702fa",
+ "compiler": "iyi 0.20.1 (built on Crystal 1.22.0-dev [91d012b1e] (2026-11-04))",
+ "commit": "91d012b1ed978c7df5cc033624ec161a2daf01b6",
"machine": "Linux 7.0.0-38-generic x64, QEMU Virtual CPU version 2.5+",
"command": "npm run record:stdlib",
- "when": "2026-11-02T07:19:39.580Z"
+ "when": "2026-11-04T04:32:59.606Z"
},
"modules": [
{
@@ -274,8 +274,8 @@
{
"name": "std/http",
"path": "src/std/http.iyi",
- "sourceSha256": "c834c001d26e1a45a8e7163e78255438004bb09caad82542270e011ef7dc82c3",
- "surface": "module std/http\n\n# A body as it arrives: from a connection, framed by its length, by chunks\n# or by the connection's close, or from a string already in hand. `read`\n# answers the next piece, \"\" once the body is done; what the other end\n# does wrong is an `HTTPError`, and a connection that fails or is\n# cancelled answers that. The first failure is kept: every read after it\n# answers the same. A body coded `gzip`, `deflate` or `zstd` is decoded\n# as it is read when the client asked for that.\n#\n# A body has a limit: past it a read answers an `HTTPError` with status\n# 413. The server's is `Server::MAX_BODY`; a client's is none.\npub class BodyReader\n # The body's coding undone from here on: `Envelope::Gzip` for gzip,\n # `Envelope::Zlib` for HTTP's deflate - which some servers send as the\n # bare stream instead, without zlib's two bytes of header, so the first\n # two bytes are looked at first, and a pair that is not a zlib header\n # (RFC 1950 §2.2: method 8, a multiple of 31) is read as the bare\n # stream. A body that does not decode is an `HTTPError` naming the\n # coding and why.\n def decode(envelope : Envelope) : Nil\n # The body's `zstd` coding (RFC 8878) undone from here on, by std/zstd's\n # `ZstdReader` as the pieces arrive, frames one after another. A frame\n # that asks for a window past *max_window* bytes is refused rather than\n # given the memory (a window outside 1 KiB to 512 MiB is a panic), and a\n # body that does not decode is an `HTTPError` naming the coding and why.\n def decode_zstd(max_window : Int32 = Zstd::WINDOW_LIMIT) : Nil\n # Reads past the rest of the body, *limit* bytes at most: whether it\n # ended within them. A server reads past what a block left unread, so\n # the next request on the connection starts where it should.\n def drain(limit : Int64) : Bool\n # Whether every byte of the body has been read, the framing's end too.\n def finished? : Bool\n # A body that arrives as an HTTP/2 stream's DATA: refused past *limit*.\n protected def self.frames(h2 : H2Connection, id : Int32, limit : Int64) : BodyReader\n def initialize(conn : Conn | ::Nil, kind : Int32, length : Int64, limit : Int64, text : String = \"\", hello : Proc(Bool) | ::Nil = nil)\n protected def on_frames(h2 : H2Connection, id : Int32) : Nil\n # The body's bytes as the framing delivers them, before any decoding.\n protected def raw(max : Int32) : String | Cancelled | SocketError | HTTPError\n # The next piece of the body, at most *max* bytes; \"\" when it is done.\n def read(max : Int32 = 65536) : String | Cancelled | SocketError | HTTPError\n # The rest of the body as one string, refused past *limit* bytes (413).\n # The pieces are kept as they arrive and joined once: a builder of the\n # length a head declared was made before any of it had come, and a head\n # that declared 64 MB and sent two bytes cost the server 64 MB.\n def read_all(limit : Int64 = 2147483647_i64) : String | Cancelled | SocketError | HTTPError\n protected def remember(fault : HTTPError | Cancelled | SocketError) : HTTPError | Cancelled | SocketError\n # A body that is a string already in hand.\n def self.text(text : String) : BodyReader\n # Whether the body ends where the connection does: nothing can follow\n # it on the connection.\n def to_close? : Bool\nend\n\n# What a streamed body is written to: an answer's `Response.stream`\n# block's, and a client's upload from an `IyiIO`. Each `write` goes out as\n# one chunk (RFC 9112 §7.1), or as it is where the connection's close ends\n# the body, so a block that writes a line a second is read a line a\n# second. Under a server's `compress`, each write is a gzip piece flushed\n# whole (std/compress's `Deflater`), which decodes as it arrives too. The\n# first failure is kept, and every write after it answers it; `finish`,\n# which the server calls when the block returns, writes the end.\npub class BodyWriter\n # The end of the body: the gzip trailer, and the last chunk with the\n # blank line after it. Once; the server's call after a block that\n # finished its own body writes nothing.\n def finish : Nil | Cancelled | SocketError\n def initialize(socket : IyiSocket | TlsStream | H2Out, chunked : Bool, deflater : Deflater | ::Nil = nil)\n def write(text : String) : Nil | Cancelled | SocketError\n # Bytes of the body written so far, before any framing or coding.\n def written : Int64\nend\n\n# What `HTTP.request`'s `jar:` keeps cookies in: the `Cookie` header a\n# request to a URL carries, and an answer's `Set-Cookie` lines taken in.\n# std/cookie's `CookieJar` is one; std/http itself reads no clock, so the\n# expiry of a cookie is the store's to keep.\npub trait CookieStore\n abstract def header_for(url : String) : String | ::Nil\n abstract def update(url : String, response : Response) : Nil\nend\n\npub struct HTTP\n # For a request target in absolute form (RFC 9112 §3.2.2), where its\n # authority starts: after `http://` or `https://`, in any case.\n def self.absolute_form(target : String) : Int32 | ::Nil\n # Whether a list of codings a client accepts (`Accept-Encoding`, RFC\n # 9110 §12.5.3) takes *coding*: named, or `*`, with a `q` above 0.\n def self.accepts?(list : String, coding : String) : Bool\n # A 1xx, 204 or 304: an answer with no content (RFC 9110 §15).\n def self.bodiless?(status : Int32) : Bool\n # Where a chunked body that starts at *from* ends in `text` — the byte\n # after the last chunk's terminating CRLF (trailers included) — nil when\n # it has not all arrived, or why it is not a chunked body. What\n # `decode_chunked` will then read whole.\n def self.chunked_end(text : String, from : Int32 = 0) : Int32 | String | Nil\n # How much a list of codings a client accepts wants *coding*, as its\n # `q` in thousandths: the weight it is named with, else `*`'s, else 0 -\n # and 0 refuses it.\n def self.coding_weight(list : String, coding : String) : Int32\n # A `Content-Length` as a number, or nil when it is not one. Repeated\n # lines arrive joined, and are one length only when every one says the\n # same (RFC 9112 §6.3). A length past 2147483647 is still a length, too\n # large for either side, and is read as one past it: `to_i?` answered\n # nil for it, and 3000000000 was refused as \"not a number\".\n def self.content_length(value : String) : Int64 | ::Nil\n # A control character other than a tab, or DEL: what a field value may\n # not hold (RFC 9110 §5.5), nor a request target or a reason phrase.\n def self.control?(text : String) : Bool\n # The pairs of a request's `Cookie` header, `a=1; b=2` (RFC 6265 §4.2),\n # a value's quotes taken off; the first of a name is kept, as a server\n # reads the most specific cookie first.\n def self.cookie_pairs(value : String) : Hash(String, String)\n # A chunked body (RFC 9112 §7.1) as one string: each chunk's size line\n # is hexadecimal, extensions after `;` are ignored, and a zero-length\n # chunk ends it. What follows the last chunk is trailers, which nothing\n # here reads.\n def self.decode_chunked(text : String) : String\n def self.delete(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n def self.find_headers_end(text : String, from : Int32 = 0) : Int32 | ::Nil\n # The request as it goes on the wire, whole. `Host`, `Connection` and\n # `Content-Length` are the request's own lines; a caller's header of\n # that name is refused rather than sent twice. A body's length is\n # always written for the methods a body belongs to, so `POST` with\n # nothing to say still says `Content-Length: 0`. A byte of the path past\n # 127 goes as `%` and two hex digits: a request target is ASCII (RFC\n # 9112 §3.2, RFC 3986 §2.1), and `/ü` went out as its UTF-8 bytes, raw.\n def self.format_request(method : String, path : String, host : String, body : String = \"\", headers : Hash(String, String) = {} of String => String) : String\n def self.get(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # *text* in gzip, whole: an answer's body under a server's `compress`.\n def self.gzip(text : String) : String\n # A writer whose body goes out in gzip as it is written.\n def self.gzip_writer(socket : IyiSocket | TlsStream | H2Out, chunked : Bool) : BodyWriter\n def self.has_break?(text : String) : Bool\n # Whether a list of tokens (RFC 9110 §5.6.1), its repeated lines joined\n # with \", \", holds *token*, in any case.\n def self.has_token?(list : String, token : String) : Bool\n def self.head(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The value of a chunk size's hex digits, nil for anything else or for\n # one past what an Int32 holds.\n def self.hex(text : String) : Int32 | ::Nil\n # *n* in lowercase hexadecimal: a chunk's size line.\n def self.hex_text(n : Int64) : String\n # A Host value - the `a` of `Host: a` - or an absolute-form target's\n # authority, made of what a host and its port are spelled with (RFC 3986\n # §3.2.2, §3.2.3): a name, an IPv4 address or an IPv6 one in brackets,\n # and digits after a colon. Empty is one too, for a target with no\n # authority (RFC 9112 §3.2). A blank, a `/`, an `@` - the bytes another\n # host or a path begins with - are not.\n def self.host?(text : String) : Bool\n # The endpoints *answer*'s ServiceMode records name for a client whose\n # configuration offers *alpn* by ALPN (none: it speaks http/1.1 alone),\n # to an origin on *port*, in the order to try them: a record whose\n # mandatory keys are not all ones std/dns reads, or whose protocols -\n # alpn, and http/1.1 unless no-default-alpn - hold none of the client's,\n # names none (RFC 9460 sections 7.1.2 and 8), and neither does one whose\n # target is not a host name. Each is the record's target, or its owner\n # for \".\"; its port, or *port*; its `ech` where the ECHConfigList holds\n # a config std/tls can use (RFC 9849 section 6.1), nil otherwise; and\n # *alpn* with the protocols the record's alpn lists first, in *alpn*'s\n # order otherwise.\n def self.https_endpoints(answer : SvcbAnswer, alpn : Array(String), port : Int32) : Array(HttpsEndpoint)\n def initialize\n # One request, and the block's answer to the response, its body still\n # arriving: `response.body_io.read` takes it a piece at a time, and\n # `response.body` all of it. The connection closes when the block\n # returns. A body of *body* is a string, sent with its length, or an\n # `IyiIO`, sent chunked as it is read - which a redirect that keeps the\n # method (307, 308) cannot send again, so that answer is handed to the\n # block as it is.\n #\n # The redirects followed are RFC 9110 §15.4's: 301 and 302 make a POST a\n # GET, 303 makes anything but a HEAD a GET, and 307 and 308 keep the\n # method and the body. `Authorization`, `Proxy-Authorization` and\n # `Cookie` written by the caller are not sent to another scheme, host or\n # port - `https://` to `http://` at one host included, as curl does (Go\n # strips them for another host) - and a jar's `Secure` cookies go over\n # `https://` alone. The downgrade itself is followed, as Go's client and\n # `curl -L` follow it; any other header the caller wrote, and a 307 or\n # 308's body, go on in the clear. One more than *redirects* is an\n # `HTTPError`; with 0 the first answer is the block's.\n #\n # *timeout_ms* bounds the connect, and each read and write after it -\n # a pause, not the whole exchange, as Python's `timeout` is: a body that\n # keeps arriving is read for as long as it takes. One that runs out is a\n # `SocketError` whose `timed_out?` is true. 0, the default, waits as\n # long as the server does, as Go's and Python's clients do; a client\n # that talks to servers it does not control should give one.\n #\n # *proxy* is a proxy's URL - `http://user:password@host:port`, or\n # `host:port` alone - and every request goes through it, every redirect\n # too: `http://` as an absolute-form request to the proxy (RFC 9112\n # §3.2.2), which carries `Proxy-Authorization: Basic` when the URL has\n # a user; `https://` through a tunnel the proxy opens with `CONNECT\n # host:port` (RFC 9110 §9.3.6), the credential on the CONNECT alone and\n # TLS to the server inside it, its certificate checked as without a\n # proxy. A caller's own `Proxy-Authorization` is the proxy's too: sent\n # on the CONNECT when the URL has no user, and never inside the tunnel.\n # A CONNECT answered with anything but a 2xx - 407 when the proxy wants\n # credentials, 403, 502 - is an `HTTPError` naming the proxy and its\n # answer. `http://` through a proxy is HTTP/1.1 whatever\n # *prior_knowledge* says. Which proxy, if any, a URL's environment\n # names is `proxy_for`'s, and std/http_proxy's `HTTPProxy.from_env`.\n def self.open(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true, client : HTTPClient | ::Nil = nil, timeout_ms : Int32 = 0, proxy : String | ::Nil = nil, &block : (Response -> R)) : R | Cancelled | SocketError | HTTPError forall R\n # The field lines after the first, or why they are not fields. A name\n # repeated, in any case, is one field, its values joined in order with\n # \", \" (RFC 9110 §5.3): into a Hash by name, a second line spelled the\n # same replaced the first and one spelled otherwise was an entry\n # `header` never reached, so two different `Content-Length`s were read\n # as whichever came first or last. A value with a control character in\n # it - a CR or LF that ends no line, a NUL - is refused (§5.5) rather than\n # handed to whoever reads it or echoes it into a header.\n #\n # *unfold*, for an answer: a line that begins with a blank continues the\n # field above it (obs-fold, RFC 9112 §5.2), and a user agent joins it on\n # with one space; read as a field of its own, `HTTP.get` panicked with\n # \"header without a colon: \\\" b\\\"\". A server may refuse one, and this\n # one does.\n #\n # *apart*, when given, is handed each repeated field's values by the\n # name in the answer, a line each, before they are joined.\n def self.parse_fields(lines : Array(String), unfold : Bool = false, apart : Hash(String, Array(String)) | ::Nil = nil) : Hash(String, String) | String\n # The response as it came off the wire, whole. `head` says the request\n # was a `HEAD`, whose answer carries the headers of a body it does not\n # send. An interim answer - `100 Continue`, `103 Early Hints` - is read\n # past to the final one after it (RFC 9110 §15.2). A `101` switches\n # protocols, and HTTP ends with it. What is not a response panics with\n # why: this is the text a program has in hand; the client answers the\n # same sentences as `HTTPError`s.\n def self.parse_response(text : String, head : Bool = false) : Response\n # A response's head - the text before its blank line - read: status,\n # reason, the fields, and the lines of those on more than one. A line\n # that begins with a blank continues the field above it (obs-fold, RFC\n # 9112 §5.2), which a user agent joins on with one space.\n def self.parse_response_head(text : String) : ::Tuple(Int32, String, Hash(String, String), Hash(String, Array(String)) | ::Nil) | HTTPError\n # Where *text* goes on at *from* past the empty lines there: what a\n # server skips before a request line (RFC 9112 §2.2).\n def self.past_empty_lines(text : String, from : Int32) : Int32\n def self.post(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The proxy a request for *url* goes through, given the values of\n # `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` - nil when it goes\n # direct. Go's `golang.org/x/net/http/httpproxy` is the rule, line for\n # line, as its documentation and tests state it:\n #\n # * `https://` takes *https_proxy*, `http://` *http_proxy*, any other\n # scheme none. A proxy written without a scheme is `http://`'s, and is\n # answered with it, ready for `open`'s *proxy*.\n # * `localhost` (in lower case: Go's comparison) and a loopback address\n # (127.0.0.0/8, ::1, the first as IPv4-mapped too) never go through a\n # proxy.\n # * *no_proxy* is a comma-separated list, each entry trimmed and read\n # without regard to case: `*` alone sends everything direct; an IP\n # address or a CIDR range (`10.0.0.0/8`, `2001:db8::/32`) matches the\n # addresses in it, IPv4 and its IPv4-mapped form alike, and a CIDR\n # range of one family never the other's; a domain matches itself and\n # every name under it (`foo.com`: `foo.com`, `bar.foo.com`, not\n # `barfoo.com`); with a leading dot, or `*.`, only the names under it\n # (`.y.com`: `x.y.com`, not `y.com`). An address or domain may carry\n # a port (`1.2.3.4:80`, `[::1]:443`, `foo.com:8080`), and matches\n # then on that port alone, the URL's own or its scheme's default.\n # An entry that is none of these (`:80`) is passed over.\n # * A host is compared as IDNA's ASCII form, as Go compares it.\n #\n # Pure: the values are the caller's, so std/http reaches no environment;\n # std/http_proxy reads them from it.\n def self.proxy_for(url : String, http_proxy : String | ::Nil, https_proxy : String | ::Nil, no_proxy : String | ::Nil = nil) : String | ::Nil\n def self.put(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The reason phrase a status carries (RFC 9110 §15), for a response the\n # server writes; a status this table does not know gets its class's.\n def self.reason_for(status : Int32) : String\n # One request and its whole answer: the answer as `open` hands it, its\n # body read. Redirects are followed, *redirects* at most; *jar* keeps\n # the cookies the answers set and sends them back; *decompress* asks\n # for gzip, deflate and zstd and undoes them. A refused or reset\n # connection answers `SocketError`; a task cancelled answers\n # `Cancelled`; what the server does wrong answers `HTTPError`. `!`\n # passes any of them up, `.or_panic` refuses it.\n def self.request(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true, timeout_ms : Int32 = 0, proxy : String | ::Nil = nil) : Response | Cancelled | SocketError | HTTPError\n # A request's head: the request line, `Host`, `Connection` (*connection*:\n # `close`, or `Upgrade`), the caller's headers, and the body's framing -\n # *length*, chunks, or none.\n def self.request_head(method : String, path : String, host : String, headers : Hash(String, String), length : Int64 | ::Nil, chunked : Bool, connection : String) : String\n # Two field names for the same field: ASCII case is the only difference\n # a name may carry.\n def self.same_name?(a : String, b : String) : Bool\n def self.split_crlf(text : String) : Array(String)\n # A status line read: the status and its reason, or why it is not one.\n def self.status_line(line : String) : ::Tuple(Int32, String) | HTTPError\n # What an `https://` request is checked against and offers: the\n # system's roots, and ALPN's `http/1.1`, unless a program sets its own -\n # roots of its own for a server it runs, say.\n def self.tls_config : TlsConfig\n def self.tls_config=(config : TlsConfig) : TlsConfig\n # A token (RFC 9110 §5.6.2): what a method or a field name is made of.\n def self.token?(text : String) : Bool\n # Without the blanks at either end (RFC 9112 §5.1: field values are\n # trimmed of the whitespace around them).\n def self.trim(s : String) : String\n # The 101 an upgrade to *protocol* is answered with, and the connection,\n # which is the caller's from then on: std/websocket's handshake. Any\n # other answer is an `HTTPError` naming it.\n #\n # *proxy* is `open`'s, and so is its credential, on the CONNECT alone;\n # but an upgrade goes through a tunnel for `http://` as for `https://`,\n # the handshake inside it in origin form. `Upgrade` belongs to the\n # connection it is sent on (RFC 9110 §7.8), and a proxy forwarding a\n # request removes `Connection` and every field it names (§7.6.1), so a\n # handshake handed to a forward proxy in absolute form reaches the\n # server, at best, as a plain GET: the gate's proxy, which drops\n # `Connection`, leaves std/websocket's server a request it answers 400.\n def self.upgrade(url : String, protocol : String, headers : Hash(String, String) = {} of String => String, timeout_ms : Int32 = 0, proxy : String | ::Nil = nil) : Upgraded | Cancelled | SocketError | HTTPError\nend\n\n# Requests that keep their connections: an `HTTPClient` holds the connections\n# its answers left open, by host and port, and the next request to that\n# server goes over one of them instead of a new one - the TCP handshake,\n# and the slow start after it, paid once rather than per request. Its\n# methods are `HTTP`'s, redirects, cookies and decoding included:\n#\n# ```\n# client = HTTPClient.new\n# client.get(\"http://127.0.0.1:8080/a\")\n# client.get(\"http://127.0.0.1:8080/b\") # the same connection\n# client.close\n# ```\n#\n# A connection goes back once its answer has been read to the end (inside\n# `open`, up to 64 KiB left unread is read past for it) and the answer\n# allows it; at most *idle* wait per server, and the rest close. A server\n# may close an idle connection whenever it likes (RFC 9112 §9.3.1), so a\n# request whose kept connection turns out closed - the write fails, or it\n# ends before a byte of an answer - is sent again once on a new one, when\n# its method is idempotent (RFC 9110 §9.2.2) and its body a string;\n# anything else answers the failure. One task at a time per request: two\n# tasks sending at once take two connections, and a client is not for\n# sharing across threads. An `https://` connection is kept under the\n# `HTTP.tls_config` it was made with, and handed only to a request made\n# under the same one (`TlsMadeUnder`): after `HTTP.tls_config=` the next\n# request makes a connection of its own, checked against the new roots.\n# A client made with *tls* uses that configuration instead of\n# `HTTP.tls_config`, and one made with *resolver* finds its hosts by it\n# instead of `Resolver.system` - an encrypted resolver, or a hosts text\n# that pins a name to an address: std/secure_dns's `DnsOverHttps` is a\n# client of its own with both, so its server is checked against the roots\n# it was given and reached at the address it was given.\n#\n# A client resumes TLS sessions (RFC 8446 §2.2): the tickets an\n# `https://` server sends are kept for its origin - scheme, host and\n# port, whichever proxy's tunnel they came through - under the TLS\n# configuration they came under, the client's own *tls* or else\n# `HTTP.tls_config` (`TlsMadeUnder`, as a kept connection is), and a new\n# connection to that origin offers the newest, each once.\n# A resumed handshake checks no certificate - the ticket stands for the\n# check the first one made - so a ticket is never offered to another\n# origin, nor under another configuration: after `HTTP.tls_config=`, or\n# roots, an identity, `now`, `min_version` or `require_staple` assigned\n# in place - in `HTTP.tls_config` or in the client's own *tls* - the next\n# connection is a full handshake under the new ones, and that origin's\n# tickets from under the old one are dropped. (std/tls's `TlsSession`\n# holds each ticket to its configuration too; this client drops them\n# where that one would only pass them over.) At most *origin_tickets* are\n# kept an origin - 4 by default, so that as many connections as *idle*\n# keeps by default can resume at once, where std/tls's server and\n# OpenSSL's send 2 a handshake - and *tickets* in all, 256 by default: as\n# many origins as that leaves room for at *origin_tickets* each, the one\n# longest unused dropped first. 0 for either keeps none, and nothing\n# resumes. `close`\n# leaves them. Nothing goes early (0-RTT), whatever a ticket allows: the\n# module's header says why. `connections_resumed`, `tickets_held` and\n# `Response#tls_resumed?` say what was kept and used. What it saves, by\n# bench/std_http_resume.iyi built --release on a 12-core machine other\n# jobs shared (medians of 200, the middle of three runs): the handshake\n# took 0.96 ms full and 0.51 resumed against std/http's server, 1.03 and\n# 0.57 against OpenSSL 3.5's s_server - and a GET on a new connection,\n# which waits about 40 ms at each iyi end that is not the handshake's,\n# 82.98 ms and 82.00, and 42.04 and 40.96.\n#\n# Over HTTP/2 - `https://` where ALPN chose `h2`, or `http://` with\n# *prior_knowledge* (h2c, RFC 9113 §3.3) - a kept connection carries the\n# next request on a new stream, until the server says GOAWAY.\n#\n# HTTPS records (RFC 9460), when `use_https_records` is set: before a new\n# connection to an `https://` origin - not through a proxy, and not for\n# an upgrade - the client asks its resolver for the origin's HTTPS\n# records (`Resolver#https_records`, AliasMode followed) and connects by\n# the first ServiceMode record it can use: one whose mandatory keys it\n# knows - the seven std/dns reads, port and no-default-alpn mandatory by\n# being there (section 9) - and whose protocols (alpn, and http/1.1\n# unless no-default-alpn) hold one of the configuration's `alpn`, or\n# http/1.1 where that is empty. Its target - the record's owner for \".\"\n# - and its port, or the origin's, are where the connection goes; its\n# `ech` is the connection's `ech_configs` (RFC 9848); and the protocols\n# it lists go first in the ClientHello's ALPN, which still offers every\n# one the configuration does (section 7.1.2: a record says which\n# protocols the endpoint has, and cannot take one away). The certificate\n# is checked, and the server named inside, for the origin's host,\n# whatever the target (section 2.3). An `ech` whose ECHConfigList holds\n# no config std/tls can use is passed over, as RFC 9849 section 6.1 has\n# it. The address hints are not read: the target's addresses are asked\n# for as any host's are.\n#\n# An endpoint that does not connect gives way to the next, then to the\n# last AliasMode target and to the origin itself (section 3) - unless\n# every endpoint carries `ech`, when the request fails rather than name\n# the server in the clear (RFC 9848 section 5.1). A TLS refusal is the\n# request's failure, but for one: a server that turned the encrypted\n# ClientHello down, its certificate checked for the config's\n# public_name, is asked once more on a new connection to the same\n# address, under the retry_configs it sent, or without ECH where it sent\n# none, which turns ECH off there (RFC 9849 section 6.1.6); a refusal of\n# that is the failure. No records (NXDOMAIN, NODATA), a set section 2.2\n# refuses, an AliasMode \".\" (the service says it is not there), or no\n# record the client can use: the connection is made as without records.\n# So is one whose lookup failed through a resolver that asks in the\n# clear, as section 3.1 lets a client choose; through encrypted\n# upstreams the failure is the request's, as section 3.1 asks, for\n# there the one who can drop the answer is an attacker taking the\n# records away. `localhost` and addresses are not asked about, and an\n# `http://` URL is not made `https://` by its records (section 9.5's\n# SHOULD): the program wrote the scheme.\n#\n# A record's `ech` and protocols make a configuration of their own - a\n# copy of the client's, made once and kept for the origin while the same\n# ones come under the same configuration - so that a kept connection and\n# a ticket are handed on only under the record they were made by\n# (`TlsMadeUnder` holds `ech_configs`). Off by default: every new\n# connection then waits on one DNS answer or more before its ClientHello\n# (RFC 9848 section 5.3 forbids sending it sooner), with no cache to\n# spare the next, and the records may move a connection to another host\n# and port, which is the program's to ask for.\npub class HTTPClient\n # Every idle connection closed. The client may be used after; it opens\n # new ones.\n def close : Nil\n # Connections this client has opened, and requests sent over one it\n # kept: what a program reads to see that its connections are kept.\n def connections_made : Int32\n # Connections this client opened whose TLS handshake resumed a session\n # by a ticket it kept.\n def connections_resumed : Int32\n def connections_reused : Int32\n def delete(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n def get(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n protected def give_back(key : String, conn : Conn) : Nil\n def head(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The configuration a connection to *origin* is made under by a record\n # whose `ech` is *ech* (nil: none) and whose protocols put *alpn* in its\n # order, *base* being the client's own or `HTTP.tls_config`: *base*\n # itself where those are its own, and otherwise a copy of it with them -\n # the same object every time while *base* is the same (`TlsMadeUnder`),\n # so that a connection kept and a ticket held under it serve the next\n # connection by the same record. It becomes the one the origin's kept\n # connections are taken under (`https_current`). The origin's\n # configurations from under another *base* go.\n protected def https_config(origin : String, base : TlsConfig, ech : Array(UInt8) | ::Nil, alpn : Array(String)) : TlsConfig\n # The configuration *origin*'s last connection by HTTPS records was made\n # under, while *base* is the one it came from - *base* itself where it\n # was made as without records: what a kept connection to the origin\n # must have been made under to be taken. Nil when none was made by them\n # under *base*, and then none is taken.\n protected def https_current(origin : String, base : TlsConfig) : TlsConfig | ::Nil\n # *timeout_ms* and *proxy* are `HTTP.open`'s, for every request the\n # client sends; *tls* and *resolver* are this client's own (above);\n # *origin_tickets* and *tickets* bound the TLS tickets kept, by origin\n # and in all.\n def initialize(idle : Int32 = 4, timeout_ms : Int32 = 0, prior_knowledge : Bool = false, proxy : String | ::Nil = nil, tls : TlsConfig | ::Nil = nil, resolver : Resolver | ::Nil = nil, origin_tickets : Int32 = 4, tickets : Int32 = 256)\n protected def made : Nil\n def open(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true, &block : (Response -> R)) : R | Cancelled | SocketError | HTTPError forall R\n def post(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # `http://` spoken as HTTP/2 from the first byte, without asking: for a\n # server known to take it.\n def prior_knowledge : Bool\n # The proxy every request goes through, `HTTP.open`'s *proxy*; its\n # connections are kept under it.\n def proxy : String | ::Nil\n def put(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n def request(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true) : Response | Cancelled | SocketError | HTTPError\n # What turns a host name into addresses for this client's connections,\n # instead of `Resolver.system`: encrypted upstreams, or a hosts text\n # alone that pins a name to an address, as std/secure_dns's\n # `DnsOverHttps` pins its server's.\n def resolver : Resolver | ::Nil\n protected def resumed : Nil\n # The session a new connection to *origin* under *tls* offers a ticket\n # from and keeps the server's new ones in; nil when the client keeps\n # none. The origin's tickets from under another configuration are\n # dropped, not left to take room that `TlsSession` would never offer\n # them from. The session handed out is the newest used; past the room\n # *tickets* leaves, the oldest goes.\n protected def session_for(origin : String, tls : TlsConfig) : TlsSession | ::Nil\n # A kept connection to *key*, nil when none waits. An HTTP/2 connection\n # that can take no more requests is closed rather than handed out, and\n # so is an `https://` one made under another TLS configuration than\n # *tls*, the one the request is made under (`TlsMadeUnder`): keyed by\n # scheme, host and port alone, a connection whose server was checked\n # against one set of roots carried requests after `HTTP.tls_config=`\n # named others that would have refused it.\n protected def take(key : String, tls : TlsConfig | ::Nil = nil) : Conn | ::Nil\n # The TLS tickets held for *origin* - `https://host:port`, the host in\n # lower case and the port written even where it is 443 - or for every\n # origin when nil.\n def tickets_held(origin : String | ::Nil = nil) : Int32\n # What this client's `https://` requests are checked against and offer,\n # instead of `HTTP.tls_config`: roots of its own, or ALPN's `h2` first.\n def tls : TlsConfig | ::Nil\n # Whether a new `https://` connection is made by the origin's HTTPS\n # records (the class's header); off by default.\n def use_https_records : Bool\n # Whether a new `https://` connection is made by the origin's HTTPS\n # records (the class's header); off by default.\n def use_https_records=(value : Bool) : Bool\nend\n\n# What the other end did that HTTP does not allow, as a value: `reason` is\n# the sentence, `status` the answer a server gives a request that did it\n# (400, or 413 and 431 for the limits). `message` says it is HTTP's.\npub struct HTTPError\n def initialize(reason : String, status : Int32 = 400)\n def reason : String\n def status : Int32\n def to_s : String\nend\n\n# One endpoint an origin's HTTPS records name, as `HTTP.https_endpoints`\n# reads it: the host and port to connect to, the ECHConfigList to encrypt\n# the ClientHello under (nil: none), and the protocols to offer by ALPN,\n# in order.\npub struct HttpsEndpoint\n def alpn : Array(String)\n def ech : Array(UInt8) | ::Nil\n def host : String\n def initialize(host : String, port : Int32, ech : Array(UInt8) | ::Nil, alpn : Array(String))\n def port : Int32\nend\n\n# A `multipart/form-data` body (RFC 7578) read a part at a time, as it\n# arrives: `next_part` answers the next `Part`, nil after the last, and a\n# part's `read` answers its bytes up to the boundary, \"\" at its end. A part\n# not read to its end is read past by the next `next_part`. The body of a\n# request is `Request#multipart`'s; any `BodyReader` and boundary will do.\n# A part's head is refused past 16 KiB, and a body that ends before its\n# closing boundary is an `HTTPError`.\npub class Multipart\n # The boundary a `Content-Type` of `multipart/...` names, nil when it\n # names none - or one of a length RFC 2046 §5.1.1 does not allow, 1 to\n # 70 characters, which `new` refuses with a panic. The type is the\n # peer's: `Request#multipart` handed a client's 90-character boundary\n # through to that panic (found by bench/std_fuzz_exercise.sh).\n def self.boundary(content_type : String) : String | ::Nil\n # Where *pattern* is in what is held, -1 when it is not there yet.\n protected def find(pattern : String) : Int32\n def initialize(body : BodyReader, boundary : String)\n # The next part, nil after the last.\n def next_part : Part | Nil | Cancelled | SocketError | HTTPError\n # Up to *max* bytes of the part being read, \"\" at its boundary. What is\n # held back is what may yet be the start of the boundary.\n protected def part_read(max : Int32) : String | Cancelled | SocketError | HTTPError\n # One more piece of the body onto what is held: false at its end.\n protected def pull : Bool | Cancelled | SocketError | HTTPError\nend\n\n# A multipart/form-data body to send: fields and files in order. Its\n# boundary is chosen when the body is first asked for - `finish`,\n# `content_type` or `boundary`, whichever comes first, after which no\n# part is added - as `iyi-` and 32 hex digits of the parts' SHA-1, made\n# again from itself in the rare case a part holds it: so it is none of\n# the parts' bytes by construction rather than by chance, the same\n# fields make the same body, and std/http reaches no source of\n# randomness for it. A boundary given to `new` is used as it is, and a\n# part that holds it is refused.\npub class MultipartBuilder\n def boundary : String\n def content_type : String\n def field(name : String, value : String) : self\n def file(name : String, filename : String, data : String, content_type : String = MIME.from_extension(filename)) : self\n # The body, closing boundary and all.\n def finish : String\n # The boundary ends a part where it follows a CRLF, the part's first\n # bytes included, which follow the head's blank line.\n protected def self.holds?(data : String, mark : String) : Bool\n def initialize(boundary : String | ::Nil = nil)\n # A name inside quotes, as browsers write one: a quote and a line break\n # percent-encoded (the WHATWG form encoding), so no field can end the\n # parameter or the line early.\n protected def self.quoted(text : String) : String\nend\n\n# One part of a multipart body: its head's fields, its name and file name\n# from `Content-Disposition`, and its bytes, read up to its boundary.\npub class Part\n def content_type : String | ::Nil\n # The file name a browser sent the part under, `filename=`.\n def filename : String | ::Nil\n def finished? : Bool\n def header(name : String) : String | ::Nil\n def headers : Hash(String, String)\n def initialize(owner : Multipart, headers : Hash(String, String))\n # The form field's name, `name=` of `Content-Disposition`.\n def name : String | ::Nil\n # The part's next bytes, at most *max*; \"\" at its end.\n def read(max : Int32 = 65536) : String | Cancelled | SocketError | HTTPError\n # The rest of the part as one string, refused past *limit* bytes.\n def read_all(limit : Int64 = 2147483647_i64) : String | Cancelled | SocketError | HTTPError\nend\n\n# What a server's block is handed: one request. Its body is `body`, read\n# whole before the block ran, or under `stream_bodies` read by the block\n# from `body_io` as it arrives; `body` then reads the rest itself, and a\n# body that cannot be read - the client went, or sent one that is not\n# HTTP - ends the connection with that sentence rather than be answered as\n# if it were whole.\npub class Request\n def body : String\n def body_io : BodyReader\n # The pairs of the request's `Cookie` header, the first of a name kept.\n def cookies : Hash(String, String)\n # The fields of a form: the query's, and an\n # `application/x-www-form-urlencoded` body's after them. The body is\n # read for it.\n def form : Params\n def header(name : String) : String | ::Nil\n def headers : Hash(String, String)\n # A request whose body is in hand: what `parse_request` answers.\n def initialize(method : String, path : String, query : String | ::Nil, headers : Hash(String, String), body : String, version : String = \"HTTP/1.1\", remote_address : String | ::Nil = nil)\n # A request whose body is still on the connection.\n def initialize(method : String, path : String, query : String | ::Nil, headers : Hash(String, String), version : String, reader : BodyReader, remote_address : String | ::Nil = nil)\n def method : String\n # The body as `multipart/form-data` parts, read as they arrive; nil when\n # the `Content-Type` is not multipart or names no boundary\n # (`Multipart.boundary`).\n def multipart : Multipart | ::Nil\n def path : String\n # The client's certificate chain, leaf first, as std/tls checked it:\n # under `serve(tls:)` with `client_roots` set, the certificate the\n # client presented; empty otherwise.\n def peer_certificates : Array(Certificate)\n # The client's certificate chain, leaf first, as std/tls checked it:\n # under `serve(tls:)` with `client_roots` set, the certificate the\n # client presented; empty otherwise.\n def peer_certificates=(value : Array(Certificate)) : Array(Certificate)\n def query : String | ::Nil\n # The client's address, as the server's socket saw it (no port): nil\n # for a request a program made, or when the client had already gone. A\n # proxy in front is what this names; `X-Forwarded-For` is the proxy's\n # word, and a header any client can write.\n def remote_address : String | ::Nil\n def tls_resumed=(resumed : Bool) : Bool\n # Whether the TLS connection the request came over resumed a session by\n # a ticket, under `serve(tls:)`: its `peer_certificates` are then the\n # ticket's. False in the clear.\n def tls_resumed? : Bool\n def version : String\nend\n\n# A response: what a server's block answers, and what a client is handed.\n#\n# To write one: `Response.new(200, \"hello\")`, a status and a string;\n# `Response.new(200, io)`, a stream the server copies out and closes, with\n# the caller's `Content-Length` when it gives one and chunked otherwise;\n# `Response.stream(200) { |w| w.write(\"...\") }`, whose block writes as it\n# goes; `Response.upgrade(headers) { |socket, leftover| ... }`, a 101 after\n# which the connection is the block's. `add_header` writes a field on a\n# line of its own, as each `Set-Cookie` must be.\n#\n# What a client is handed: the status, reason and headers, and the body -\n# `body` whole, or `body_io` as it arrives inside `HTTP.open`'s block.\npub class Response\n # One more line of the field *name*, written on a line of its own: what\n # two `Set-Cookie`s need, since the \", \" a field's lines are joined with\n # is part of a cookie's date.\n def add_header(name : String, value : String) : Nil\n # The lines of each field written on more than one, by its name in\n # `headers`; nil when every field is on one line.\n def apart : Hash(String, Array(String)) | ::Nil\n # The body: whole. Inside `HTTP.open`'s block it is read here, the first\n # time it is asked for, and a body that cannot be read panics with why;\n # `body_io` answers that as a value.\n def body : String\n # The body as it arrives. For a response a program made, its string.\n def body_io : BodyReader\n # Whether the client undid the body's `gzip`, `deflate` or `zstd`\n # coding; the `Content-Encoding` and `Content-Length` that described it\n # are gone from the headers then.\n def decoded? : Bool\n # A header by name, as the server spelled it or otherwise: a field\n # name is case-insensitive (RFC 9110 §5.1), and `content-length` is as\n # common on the wire as `Content-Length`.\n def header(name : String) : String | ::Nil\n # A field's lines apart, in the order they came: `header` joins them\n # with \", \", and `Set-Cookie` is the field that join breaks (RFC 9110\n # §5.3), since a cookie's `Expires` date has a comma in it - two cookies\n # read as \"a=1; Expires=Wed, 21 Oct 2026 07:28:00 GMT, b=2\", which no\n # split takes back apart. Empty for a field that is not there.\n def header_values(name : String) : Array(String)\n def headers : Hash(String, String)\n # A response a client read: its body comes from *reader*, and *url* is\n # where it came from.\n def initialize(status : Int32, reason : String, headers : Hash(String, String), apart : Hash(String, Array(String)) | ::Nil, reader : BodyReader, url : String, decoded : Bool)\n def initialize(status : Int32, reason : String, headers : Hash(String, String), body : String, apart : Hash(String, Array(String)) | ::Nil = nil)\n # A response to write: the reason is the status's own, the headers the\n # caller's. `Response.new(200, \"hello\")` is the common shape.\n def initialize(status : Int32, body : String = \"\", headers : Hash(String, String) = {} of String => String)\n # A response whose body is *source*, copied out by the server and\n # closed after.\n def initialize(status : Int32, source : IyiIO, headers : Hash(String, String) = {} of String => String)\n # The body read whole into the response, which is answered; why not,\n # when it cannot be.\n def load : Response | Cancelled | SocketError | HTTPError\n def reason : String\n def source : IyiIO | ::Nil\n def status : Int32\n # A response whose body the block writes as it goes, chunked.\n def self.stream(status : Int32, headers : Hash(String, String) = {} of String => String, &writer : (BodyWriter -> Nil)) : Response\n def success? : Bool\n def takeover : Proc(IyiSocket | TlsStream, String, Nil) | ::Nil\n protected def takeover=(takeover : Proc(IyiSocket | TlsStream, String, Nil)) : Proc(IyiSocket | TlsStream, String, Nil)\n def tls_resumed=(resumed : Bool) : Bool\n # Whether the `https://` connection the answer came over resumed a TLS\n # session by a ticket an `HTTPClient` kept, rather than a full\n # handshake; false in the clear and for one a program made.\n def tls_resumed? : Bool\n # A 101 Switching Protocols with *headers* (`Upgrade` and the\n # protocol's own), after which the connection is the block's: it is\n # handed the connection - the socket, or under `serve(tls:)` the\n # `TlsStream` over it - and the bytes the client sent after its request,\n # and the connection closes when it returns.\n def self.upgrade(headers : Hash(String, String), &takeover : (IyiSocket | TlsStream, String -> Nil)) : Response\n # The URL the answer came from, after any redirects; nil for one a\n # program made.\n def url : String | ::Nil\n def writer : Proc(BodyWriter, Nil) | ::Nil\n protected def writer=(writer : Proc(BodyWriter, Nil)) : Proc(BodyWriter, Nil)\nend\n\npub struct Server\n # The headers a coded body goes with: the coding, and `Vary` so that a\n # cache keeps the answers apart. A `Content-Length` the block gave is\n # the plain body's, and goes.\n protected def self.coded(response : Response, coding : String) : Response\n # The coding *response* goes in to the client of *req*, or nil: one the\n # client takes (RFC 9110 §12.5.3, a `q` of 0 refusing it), for an\n # answer with a body and a type worth compressing, and no coding of its\n # own. A whole body goes `zstd` when the client's `q` for it is at least\n # its `q` for gzip, and `gzip` otherwise. The tie goes to zstd for the\n # server's time: measured on this machine, release builds, on\n # src/std/http.iyi (199,134 bytes) `Gzip.compress` wrote 54,484 bytes in\n # 5 ms and `Zstd.compress` 61,753 in 1 ms, and on CHANGELOG.md (1.5 MB)\n # 552,446 in 58 ms against 589,785 in 13 ms - a quarter of the time, for\n # an eighth more bytes at most. A stream - an `IyiIO` or a\n # `Response.stream` - goes gzip or as it is: std/compress's `Deflater`\n # writes each piece as it comes, where std/zstd's compressor takes its\n # input whole, and a frame a write would code each piece alone.\n def self.coding_for(req : Request, response : Response) : String | ::Nil\n # A string body compressed whole in *coding*, `zstd` or `gzip`, with\n # the headers that say so. `Zstd.compress`'s frame needs a window of\n # 1 MiB at most, inside the 8 MB RFC 9659 §3 holds an encoder to.\n protected def self.compressed(response : Response, coding : String) : Response\n # A response's head. `Content-Length` is *length*'s - the caller's own\n # line is checked against it, and kept only where no body follows it: a\n # HEAD, or a 304 that names the length a 200 would have (RFC 9110 §8.6) -\n # or `Transfer-Encoding: chunked` when *chunked*, or neither for a body\n # the connection's close ends. `Connection` says what the connection will\n # do next, except on an *upgrade*, whose own line says it. A 1xx, 204 or\n # 304 has no content (RFC 9110 §15), and writes neither a body nor a\n # length of its own (§8.6): the body was read by the client as the start\n # of the next answer, and `Content-Length: 0` on a 304 told a cache that\n # the resource had become empty. A field added on lines of its own\n # (`add_header`) is written a line each.\n def self.format_head(response : Response, head : Bool, close : Bool, length : Int64 | ::Nil, chunked : Bool, upgrade : Bool = false) : String\n # The response as it goes on the wire, whole: `format_head` for its\n # string body, and the body.\n def self.format_response(response : Response, head : Bool, close : Bool) : String\n # One connection: requests until the client is done. A request that is\n # not HTTP is answered with why and the connection closed, since the\n # bytes after it cannot be trusted to be a request either. Requests that\n # came in one read are read where they lie, one after another. A\n # connection that opens with HTTP/2's preface is served as HTTP/2. Under\n # *tls* the handshake comes first, and the connection ends with\n # close_notify. A connection this side wrote last on - an answer, a\n # refusal, a GOAWAY - closes by lingering (`Conn#close`), so the reset\n # a close with the client's bytes unread is cannot take what was\n # written from the client before it reads it. One that timed out or was\n # cancelled, with nothing of this side's waiting to be read, closes at\n # once, and so does one whose client said `close` and sent nothing past\n # its request (`serve_h1`).\n def self.handle(client : IyiSocket, handler : Proc(Request, Response), listener : IyiSocket | ServeStop | Nil = nil, idle : Hash(UInt64, Conn) | ::Nil = nil, stream_bodies : Bool = false, compress : Bool = false, timeout_ms : Int32 = 0, tls : TlsServerConfig | TlsFrozenServerConfig | Nil = nil, request_timeout_ms : Int32 = 0) : Nil\n def initialize\n # A request's head - the text before its blank line - read and checked,\n # or why it is not one.\n def self.parse_head(text : String) : RequestHead | HTTPError\n # A request at *from* in `text`, if a whole one is there. No request and\n # no error means \"not yet\". `consumed` counts from the start of `text`.\n def self.parse_request(text : String, from : Int32 = 0) : ParsedRequest\n # Serve `listener` until it is closed, on *workers* threads: each\n # connection is a task on the pool `spawn_parallel` starts (SPEC.md\n # III.4.13), started by whichever worker is free and served there to\n # its end, while the thread that called this accepts. The block runs on\n # the connection's worker, once per request, on several workers at\n # once; what it answers is written back.\n # The close ends a connection waiting on its client too, for its next\n # request or for the rest of one, and one that is answering ends when its\n # answer is written: a keep-alive client that sat idle kept `serve` from\n # returning for as long as it stayed, and so did one that had sent\n # what has arrived of a request so far - one byte of it was enough.\n #\n # *stream_bodies*: the block reads each request's body from `body_io`\n # rather than being handed it whole. *compress*: an answer whose type\n # std/mime calls compressible goes zstd or gzip to a client that\n # accepts one (`coding_for`).\n #\n # *timeout_ms*: a request's head must have arrived within it, counted\n # from when the server starts waiting for one - a kept-alive connection's\n # idle time included - and no read of a body nor write of an answer may\n # wait longer; a connection that does is closed. Without it, a client\n # that sends a byte of its head every few seconds (Slowloris) holds a\n # connection and its fiber for as long as it likes, and so does one that\n # reads none of its answer. A minute by default, nginx's\n # `client_header_timeout`; 0 for none. A connection handed over by\n # `Response.upgrade` waits as its protocol likes.\n #\n # *request_timeout_ms*: the whole request - head and body - must have\n # arrived within it, counted from the same moment as *timeout_ms*, as\n # Go's `ReadTimeout` beside its `ReadHeaderTimeout`; the head is held to\n # the sooner of the two. Without it a body sent a byte every few seconds\n # was bounded per read alone, and held its connection for as long as it\n # dripped. A block that reads the body itself (*stream_bodies*) reads\n # under it until the block returns, and what it leaves unread is read\n # past under it too. Over HTTP/2 the wait for each request and its body\n # is held to it on the connection the same way, the head to the sooner\n # of the two. Five minutes by default, which a 64 MiB body (`MAX_BODY`)\n # meets at 224 KB/s; a server taking large uploads over slow links\n # raises it. 0 for none. A connection past it is closed, unanswered, as\n # one past *timeout_ms* is.\n #\n # *max_connections*: at most this many connections are served at once;\n # 1024 by default, 0 for no limit. Past it the server stops accepting\n # until one ends, so the next connection waits in the kernel's listen\n # queue - queued, not refused, as Go's `netutil.LimitListener` does - and\n # is served in its turn; a client that gives up waiting goes unserved.\n # Each served connection holds its place until it closes, a kept-alive\n # one between requests too, which *timeout_ms* bounds. The process's\n # own limit on descriptors may come first.\n #\n # *tls*: each connection is TLS (std/tls's `TlsServer`, 1.3 or 1.2) under this\n # configuration, its handshake held to *timeout_ms* too and run on the\n # worker that serves it; one whose handshake fails is closed, the\n # client told why by an alert. Inside it a connection that opens with\n # HTTP/2's preface - as a client whose ALPN chose `h2` does, when\n # `tls.alpn` offers it - is served HTTP/2, and any other HTTP/1.1. A\n # `Response.upgrade` block over TLS is handed the `TlsStream`, so a\n # WebSocket is `wss://`. On more than one worker the config is a\n # `TlsFrozenServerConfig` - `config.frozen` - since every worker reads\n # it at once: its ticket keys are one set behind a lock, so a ticket a\n # connection on one worker was sent resumes on any other, and an\n # identity source given to `frozen` is a block the compiler checked. A\n # `TlsServerConfig` itself is refused at the call (\"Server.serve hands\n # `tls : TlsServerConfig` to other threads, several at once, and it is\n # not Share: ... Pass `config.frozen` ...\"), not frozen here: `frozen`\n # is a copy, so a property assigned after the server started would be\n # served by no worker and nothing would say so, and a config with an\n # `identity_source` cannot be frozen without its source given again as\n # a block the compiler checks. Under `workers: 1` either is served.\n #\n # *workers*: the pool's size, `IyiWorkers.count` by default - `IYI_WORKERS`\n # from the environment, else the cores this process may run on. The\n # pool is the process's one, and it does not grow or shrink once it has\n # started, so a count other than the one it started with panics. Every\n # worker runs the block at once, so the compiler checks it where it is\n # written as it checks a parallel task's (`@[Share]`, SPEC.md III.4.4):\n # every value it captures must be `Share` - a table behind a `Mutex`\n # (std/mutex), a count in an `Atomic`, a `SharedRouter` (std/router) -\n # every captured local assigned once, and the constants and class\n # variables it names `Share`. The refusal names the variable, why its\n # type is not `Share`, and the two ways out: the lock, or `workers: 1`.\n # `workers: 1`, written as the literal, serves on the calling thread\n # alone - each connection a task beside the accept loop, as `spawn`\n # makes one - and its block is not checked: a block that calls a\n # `Router`, appends to an `Array` or keeps a handle that is not `Share`\n # is served there. A count in a variable is checked whatever it holds,\n # since the compiler cannot know it is 1. A count of 1 from the default\n # (`IYI_WORKERS=1`, one core) serves on the calling thread too.\n #\n # A connection stays on the worker that started it (III.4.13, part 1):\n # connections spread over the workers free as they arrive, and one kept\n # alive is not moved later. A block that computes without parking holds\n # its worker meanwhile, as any task does. The listener is the calling\n # thread's, and a close of it is made there: a block that stops the\n # server sends on a `Channel` a task beside the server receives from,\n # and that task closes it. At the stop each connection waiting on its\n # client is closed on its own worker, sent close_notify there first\n # under *tls*.\n def self.serve(listener : IyiSocket, stream_bodies : Bool = false, compress : Bool = false, timeout_ms : Int32 = 60000, tls : C = nil, request_timeout_ms : Int32 = REQUEST_TIMEOUT, max_connections : Int32 = MAX_CONNECTIONS, workers : Int32 = IyiWorkers.count, &handler : (Request -> Response)) : Nil forall C\n # `serve` with a value for its handler rather than a block: a\n # `SharedProc`, a `SharedRouter` (std/router), or one of the program's\n # own, whose `call(request : Request) : Response` answers each request,\n # as Go's `http.Handler` does. On more than one worker it must be\n # `Share` (SPEC.md III.4.4), since every connection on every worker\n # calls the one value at once: the compiler refuses one that is not at\n # the argument, naming the field that failed and the two ways out - a\n # count in an `Atomic`, a pool or a cache behind a `Mutex`, a\n # `SharedRouter` for a `Router`, or `workers: 1`, under which any value\n # with that `call` is served on the calling thread, a `Router`\n # included. What the check does not see is the edge `spawn_parallel`\n # has too: a method the handler calls that writes a module's state. A\n # struct handler is copied into each connection's task, and an `Atomic`\n # in it with it: what the connections share is a class, or held by\n # one.\n #\n # The rest is the block form's: *stream_bodies*, *compress*,\n # *timeout_ms*, *tls* (`config.frozen` on more than one worker),\n # *request_timeout_ms*, *max_connections* and *workers*, HTTP/2 by its\n # preface, `Response.upgrade`, and a close of *listener* - on the\n # thread that called this - ending every connection waiting on its\n # client and returning once the others have answered.\n def self.serve(listener : IyiSocket, handler : H, stream_bodies : Bool = false, compress : Bool = false, timeout_ms : Int32 = 60000, tls : C = nil, request_timeout_ms : Int32 = REQUEST_TIMEOUT, max_connections : Int32 = MAX_CONNECTIONS, workers : Int32 = IyiWorkers.count) : Nil forall H, C\n protected def self.writer_for(socket : IyiSocket | TlsStream, chunked : Bool, gzip : Bool) : BodyWriter\nend\n\n# A TLS connection used as a socket is: read into a buffer, written to,\n# closed, its timeouts set - by a `Conn`, a `BodyWriter`, and a connection\n# handed over by an upgrade (`wss://` at either end). What TLS refuses is a\n# `SocketError` carrying TLS's own sentence (`layer_reason`), so answers\n# keep the types they have over a plain socket.\npub class TlsStream\n # The socket closed at once, nothing sent or read first.\n def abort : Nil\n # close_notify, the sending half shut, and what the peer still sends\n # read past for a bounded time before the socket closes\n # (`TlsConnection#close`).\n def close : Nil\n # close_notify and the sending half shut; reads go on until the peer's\n # own (`TlsConnection#close_write`).\n def close_write : SocketError | ::Nil\n def closed? : Bool\n def initialize(tls : TlsConnection, host : String)\n # The peer's certificate chain, leaf first, as the handshake checked it:\n # the server's, at a client; the client's, at a server that asked.\n def peer_certificates : Array(Certificate)\n # Up to *count* bytes into *buffer*: 0 once the peer closed with\n # close_notify.\n def read(buffer : Pointer(UInt8), count : Int32) : Int32 | Cancelled | SocketError\n # Up to *max_bytes* as a string, what is held first: \"\" once the peer\n # closed with close_notify. `IyiSocket#read`'s form, for a block an\n # upgrade hands either.\n def read(max_bytes : Int32 = 4096) : String | Cancelled | SocketError\n # How long a read or a write of the connection under it may wait.\n def read_timeout_ms=(ms : Int32 | ::Nil) : Int32 | ::Nil\n # Whether the handshake resumed a session (`TlsConnection#resumed?`).\n def resumed? : Bool\n def write(text : String) : Int32 | Cancelled | SocketError\n def write_timeout_ms=(ms : Int32 | ::Nil) : Int32 | ::Nil\nend\n\n# What `HTTP.upgrade` answers: the 101, and the connection, which is the\n# caller's now - the socket, and the bytes the server sent after its head.\npub class Upgraded\n def initialize(response : Response, socket : IyiSocket | TlsStream, leftover : String)\n def leftover : String\n def response : Response\n # The connection: the socket, or for `https://` the `TlsStream` over it.\n def socket : IyiSocket | TlsStream\nend\n\nimpl Error for HTTPError\n def message : String\nend\n"
+ "sourceSha256": "c7ddc59f99e543868102459ccd62385e941d6ac8e85de415eb34e30e57f71dcd",
+ "surface": "module std/http\n\n# A body as it arrives: from a connection, framed by its length, by chunks\n# or by the connection's close, or from a string already in hand. `read`\n# answers the next piece, \"\" once the body is done; what the other end\n# does wrong is an `HTTPError`, and a connection that fails or is\n# cancelled answers that. The first failure is kept: every read after it\n# answers the same. A body coded `gzip`, `deflate` or `zstd` is decoded\n# as it is read when the client asked for that.\n#\n# A body has a limit: past it a read answers an `HTTPError` with status\n# 413. The server's is `Server::MAX_BODY`; a client's is none.\npub class BodyReader\n # The body's coding undone from here on: `Envelope::Gzip` for gzip,\n # `Envelope::Zlib` for HTTP's deflate - which some servers send as the\n # bare stream instead, without zlib's two bytes of header, so the first\n # two bytes are looked at first, and a pair that is not a zlib header\n # (RFC 1950 §2.2: method 8, a multiple of 31) is read as the bare\n # stream. A body that does not decode is an `HTTPError` naming the\n # coding and why.\n def decode(envelope : Envelope) : Nil\n # The body's `zstd` coding (RFC 8878) undone from here on, by std/zstd's\n # `ZstdReader` as the pieces arrive, frames one after another. A frame\n # that asks for a window past *max_window* bytes is refused rather than\n # given the memory (a window outside 1 KiB to 512 MiB is a panic), and a\n # body that does not decode is an `HTTPError` naming the coding and why.\n def decode_zstd(max_window : Int32 = Zstd::WINDOW_LIMIT) : Nil\n # Reads past the rest of the body, *limit* bytes at most: whether it\n # ended within them. A server reads past what a block left unread, so\n # the next request on the connection starts where it should.\n def drain(limit : Int64) : Bool\n # Whether every byte of the body has been read, the framing's end too.\n def finished? : Bool\n # A body that arrives as an HTTP/2 stream's DATA: refused past *limit*.\n protected def self.frames(h2 : H2Connection, id : Int32, limit : Int64) : BodyReader\n def initialize(conn : Conn | ::Nil, kind : Int32, length : Int64, limit : Int64, text : String = \"\", hello : Proc(Bool) | ::Nil = nil)\n # Whether a server reading past the rest of this body, so that the next\n # request starts where it should, would close instead: its length says\n # more than *limit* bytes are left, or its client still waits for the\n # `100 Continue` its `Expect` asked for - which, written after the\n # answer, the client reads as its next request's, and the body it\n # holds back is then waited for. A chunked body's rest is not known\n # until it is read.\n def left_past?(limit : Int64) : Bool\n protected def on_frames(h2 : H2Connection, id : Int32) : Nil\n # The body's bytes as the framing delivers them, before any decoding.\n protected def raw(max : Int32) : String | Cancelled | SocketError | HTTPError\n # The next piece of the body, at most *max* bytes; \"\" when it is done.\n def read(max : Int32 = 65536) : String | Cancelled | SocketError | HTTPError\n # The rest of the body as one string, refused past *limit* bytes (413).\n # The pieces are kept as they arrive and joined once: a builder of the\n # length a head declared was made before any of it had come, and a head\n # that declared 64 MB and sent two bytes cost the server 64 MB.\n def read_all(limit : Int64 = 2147483647_i64) : String | Cancelled | SocketError | HTTPError\n protected def remember(fault : HTTPError | Cancelled | SocketError) : HTTPError | Cancelled | SocketError\n # A body that is a string already in hand.\n def self.text(text : String) : BodyReader\n # Whether the body ends where the connection does: nothing can follow\n # it on the connection.\n def to_close? : Bool\nend\n\n# What a streamed body is written to: an answer's `Response.stream`\n# block's, and a client's upload from an `IyiIO`. Each `write` goes out as\n# one chunk (RFC 9112 §7.1), or as it is where the connection's close ends\n# the body, so a block that writes a line a second is read a line a\n# second. Under a server's `compress`, each write is a gzip piece flushed\n# whole (std/compress's `Deflater`), which decodes as it arrives too. The\n# first failure is kept, and every write after it answers it; `finish`,\n# which the server calls when the block returns, writes the end.\npub class BodyWriter\n # The end of the body: the gzip trailer, and the last chunk with the\n # blank line after it. Once; the server's call after a block that\n # finished its own body writes nothing.\n def finish : Nil | Cancelled | SocketError\n def initialize(socket : IyiSocket | TlsStream | H2Out, chunked : Bool, deflater : Deflater | ::Nil = nil)\n def write(text : String) : Nil | Cancelled | SocketError\n # Bytes of the body written so far, before any framing or coding.\n def written : Int64\nend\n\n# What `HTTP.request`'s `jar:` keeps cookies in: the `Cookie` header a\n# request to a URL carries, and an answer's `Set-Cookie` lines taken in.\n# std/cookie's `CookieJar` is one; std/http itself reads no clock, so the\n# expiry of a cookie is the store's to keep.\npub trait CookieStore\n abstract def header_for(url : String) : String | ::Nil\n abstract def update(url : String, response : Response) : Nil\nend\n\npub struct HTTP\n # For a request target in absolute form (RFC 9112 §3.2.2), where its\n # authority starts: after `http://` or `https://`, in any case.\n def self.absolute_form(target : String) : Int32 | ::Nil\n # Whether a list of codings a client accepts (`Accept-Encoding`, RFC\n # 9110 §12.5.3) takes *coding*: named, or `*`, with a `q` above 0.\n def self.accepts?(list : String, coding : String) : Bool\n # A 1xx, 204 or 304: an answer with no content (RFC 9110 §15).\n def self.bodiless?(status : Int32) : Bool\n # Where a chunked body that starts at *from* ends in `text` — the byte\n # after the last chunk's terminating CRLF (trailers included) — nil when\n # it has not all arrived, or why it is not a chunked body. What\n # `decode_chunked` will then read whole.\n def self.chunked_end(text : String, from : Int32 = 0) : Int32 | String | Nil\n # How much a list of codings a client accepts wants *coding*, as its\n # `q` in thousandths: the weight it is named with, else `*`'s, else 0 -\n # and 0 refuses it.\n def self.coding_weight(list : String, coding : String) : Int32\n # A `Content-Length` as a number, or nil when it is not one. Repeated\n # lines arrive joined, and are one length only when every one says the\n # same (RFC 9112 §6.3). A length past 2147483647 is still a length, too\n # large for either side, and is read as one past it: `to_i?` answered\n # nil for it, and 3000000000 was refused as \"not a number\".\n def self.content_length(value : String) : Int64 | ::Nil\n # A control character other than a tab, or DEL: what a field value may\n # not hold (RFC 9110 §5.5), nor a request target or a reason phrase.\n def self.control?(text : String) : Bool\n # The pairs of a request's `Cookie` header, `a=1; b=2` (RFC 6265 §4.2),\n # a value's quotes taken off; the first of a name is kept, as a server\n # reads the most specific cookie first.\n def self.cookie_pairs(value : String) : Hash(String, String)\n # A chunked body (RFC 9112 §7.1) as one string: each chunk's size line\n # is hexadecimal, extensions after `;` are ignored, and a zero-length\n # chunk ends it. What follows the last chunk is trailers, which nothing\n # here reads.\n def self.decode_chunked(text : String) : String\n def self.delete(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n def self.find_headers_end(text : String, from : Int32 = 0) : Int32 | ::Nil\n # The request as it goes on the wire, whole. `Host`, `Connection` and\n # `Content-Length` are the request's own lines; a caller's header of\n # that name is refused rather than sent twice. A body's length is\n # always written for the methods a body belongs to, so `POST` with\n # nothing to say still says `Content-Length: 0`. A byte of the path past\n # 127 goes as `%` and two hex digits: a request target is ASCII (RFC\n # 9112 §3.2, RFC 3986 §2.1), and `/ü` went out as its UTF-8 bytes, raw.\n def self.format_request(method : String, path : String, host : String, body : String = \"\", headers : Hash(String, String) = {} of String => String) : String\n def self.get(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # *text* in gzip, whole: an answer's body under a server's `compress`.\n def self.gzip(text : String) : String\n # A writer whose body goes out in gzip as it is written.\n def self.gzip_writer(socket : IyiSocket | TlsStream | H2Out, chunked : Bool) : BodyWriter\n def self.has_break?(text : String) : Bool\n # Whether a list of tokens (RFC 9110 §5.6.1), its repeated lines joined\n # with \", \", holds *token*, in any case.\n def self.has_token?(list : String, token : String) : Bool\n def self.head(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The value of a chunk size's hex digits, nil for anything else or for\n # one past what an Int32 holds.\n def self.hex(text : String) : Int32 | ::Nil\n # *n* in lowercase hexadecimal: a chunk's size line.\n def self.hex_text(n : Int64) : String\n # A Host value - the `a` of `Host: a` - or an absolute-form target's\n # authority, made of what a host and its port are spelled with (RFC 3986\n # §3.2.2, §3.2.3): a name, an IPv4 address or an IPv6 one in brackets,\n # and digits after a colon. Empty is one too, for a target with no\n # authority (RFC 9112 §3.2). A blank, a `/`, an `@` - the bytes another\n # host or a path begins with - are not.\n def self.host?(text : String) : Bool\n # The endpoints *answer*'s ServiceMode records name for a client whose\n # configuration offers *alpn* by ALPN (none: it speaks http/1.1 alone),\n # to an origin on *port*, in the order to try them: a record whose\n # mandatory keys are not all ones std/dns reads, or whose protocols -\n # alpn, and http/1.1 unless no-default-alpn - hold none of the client's,\n # names none (RFC 9460 sections 7.1.2 and 8), and neither does one whose\n # target is not a host name. Each is the record's target, or its owner\n # for \".\"; its port, or *port*; its `ech` where the ECHConfigList holds\n # a config std/tls can use (RFC 9849 section 6.1), nil otherwise; and\n # *alpn* with the protocols the record's alpn lists first, in *alpn*'s\n # order otherwise.\n def self.https_endpoints(answer : SvcbAnswer, alpn : Array(String), port : Int32) : Array(HttpsEndpoint)\n def initialize\n # One request, and the block's answer to the response, its body still\n # arriving: `response.body_io.read` takes it a piece at a time, and\n # `response.body` all of it. The connection closes when the block\n # returns. A body of *body* is a string, sent with its length, or an\n # `IyiIO`, sent chunked as it is read - which a redirect that keeps the\n # method (307, 308) cannot send again, so that answer is handed to the\n # block as it is.\n #\n # The redirects followed are RFC 9110 §15.4's: 301 and 302 make a POST a\n # GET, 303 makes anything but a HEAD a GET, and 307 and 308 keep the\n # method and the body. `Authorization`, `Proxy-Authorization` and\n # `Cookie` written by the caller are not sent to another scheme, host or\n # port - `https://` to `http://` at one host included, as curl does (Go\n # strips them for another host) - and a jar's `Secure` cookies go over\n # `https://` alone. The downgrade itself is followed, as Go's client and\n # `curl -L` follow it; any other header the caller wrote, and a 307 or\n # 308's body, go on in the clear. One more than *redirects* is an\n # `HTTPError`; with 0 the first answer is the block's.\n #\n # *timeout_ms* bounds the connect, and each read and write after it -\n # a pause, not the whole exchange, as Python's `timeout` is: a body that\n # keeps arriving is read for as long as it takes. One that runs out is a\n # `SocketError` whose `timed_out?` is true. 0, the default, waits as\n # long as the server does, as Go's and Python's clients do; a client\n # that talks to servers it does not control should give one.\n #\n # *proxy* is a proxy's URL - `http://user:password@host:port`, or\n # `host:port` alone - and every request goes through it, every redirect\n # too: `http://` as an absolute-form request to the proxy (RFC 9112\n # §3.2.2), which carries `Proxy-Authorization: Basic` when the URL has\n # a user; `https://` through a tunnel the proxy opens with `CONNECT\n # host:port` (RFC 9110 §9.3.6), the credential on the CONNECT alone and\n # TLS to the server inside it, its certificate checked as without a\n # proxy. A caller's own `Proxy-Authorization` is the proxy's too: sent\n # on the CONNECT when the URL has no user, and never inside the tunnel.\n # A CONNECT answered with anything but a 2xx - 407 when the proxy wants\n # credentials, 403, 502 - is an `HTTPError` naming the proxy and its\n # answer. `http://` through a proxy is HTTP/1.1 whatever\n # *prior_knowledge* says. Which proxy, if any, a URL's environment\n # names is `proxy_for`'s, and std/http_proxy's `HTTPProxy.from_env`.\n def self.open(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true, client : HTTPClient | ::Nil = nil, timeout_ms : Int32 = 0, proxy : String | ::Nil = nil, &block : (Response -> R)) : R | Cancelled | SocketError | HTTPError forall R\n # The field lines after the first, or why they are not fields. A name\n # repeated, in any case, is one field, its values joined in order with\n # \", \" (RFC 9110 §5.3): into a Hash by name, a second line spelled the\n # same replaced the first and one spelled otherwise was an entry\n # `header` never reached, so two different `Content-Length`s were read\n # as whichever came first or last. A value with a control character in\n # it - a CR or LF that ends no line, a NUL - is refused (§5.5) rather than\n # handed to whoever reads it or echoes it into a header.\n #\n # *unfold*, for an answer: a line that begins with a blank continues the\n # field above it (obs-fold, RFC 9112 §5.2), and a user agent joins it on\n # with one space; read as a field of its own, `HTTP.get` panicked with\n # \"header without a colon: \\\" b\\\"\". A server may refuse one, and this\n # one does.\n #\n # *apart*, when given, is handed each repeated field's values by the\n # name in the answer, a line each, before they are joined.\n def self.parse_fields(lines : Array(String), unfold : Bool = false, apart : Hash(String, Array(String)) | ::Nil = nil) : Hash(String, String) | String\n # The response as it came off the wire, whole. `head` says the request\n # was a `HEAD`, whose answer carries the headers of a body it does not\n # send. An interim answer - `100 Continue`, `103 Early Hints` - is read\n # past to the final one after it (RFC 9110 §15.2). A `101` switches\n # protocols, and HTTP ends with it. What is not a response panics with\n # why: this is the text a program has in hand; the client answers the\n # same sentences as `HTTPError`s.\n def self.parse_response(text : String, head : Bool = false) : Response\n # A response's head - the text before its blank line - read: status,\n # reason, the fields, and the lines of those on more than one. A line\n # that begins with a blank continues the field above it (obs-fold, RFC\n # 9112 §5.2), which a user agent joins on with one space.\n def self.parse_response_head(text : String) : ::Tuple(Int32, String, Hash(String, String), Hash(String, Array(String)) | ::Nil) | HTTPError\n # Where *text* goes on at *from* past the empty lines there: what a\n # server skips before a request line (RFC 9112 §2.2).\n def self.past_empty_lines(text : String, from : Int32) : Int32\n def self.post(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The proxy a request for *url* goes through, given the values of\n # `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` - nil when it goes\n # direct. Go's `golang.org/x/net/http/httpproxy` is the rule, line for\n # line, as its documentation and tests state it:\n #\n # * `https://` takes *https_proxy*, `http://` *http_proxy*, any other\n # scheme none. A proxy written without a scheme is `http://`'s, and is\n # answered with it, ready for `open`'s *proxy*.\n # * `localhost` (in lower case: Go's comparison) and a loopback address\n # (127.0.0.0/8, ::1, the first as IPv4-mapped too) never go through a\n # proxy.\n # * *no_proxy* is a comma-separated list, each entry trimmed and read\n # without regard to case: `*` alone sends everything direct; an IP\n # address or a CIDR range (`10.0.0.0/8`, `2001:db8::/32`) matches the\n # addresses in it, IPv4 and its IPv4-mapped form alike, and a CIDR\n # range of one family never the other's; a domain matches itself and\n # every name under it (`foo.com`: `foo.com`, `bar.foo.com`, not\n # `barfoo.com`); with a leading dot, or `*.`, only the names under it\n # (`.y.com`: `x.y.com`, not `y.com`). An address or domain may carry\n # a port (`1.2.3.4:80`, `[::1]:443`, `foo.com:8080`), and matches\n # then on that port alone, the URL's own or its scheme's default.\n # An entry that is none of these (`:80`) is passed over.\n # * A host is compared as IDNA's ASCII form, as Go compares it.\n #\n # Pure: the values are the caller's, so std/http reaches no environment;\n # std/http_proxy reads them from it.\n def self.proxy_for(url : String, http_proxy : String | ::Nil, https_proxy : String | ::Nil, no_proxy : String | ::Nil = nil) : String | ::Nil\n def self.put(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The reason phrase a status carries (RFC 9110 §15), for a response the\n # server writes; a status this table does not know gets its class's.\n def self.reason_for(status : Int32) : String\n # One request and its whole answer: the answer as `open` hands it, its\n # body read. Redirects are followed, *redirects* at most; *jar* keeps\n # the cookies the answers set and sends them back; *decompress* asks\n # for gzip, deflate and zstd and undoes them. A refused or reset\n # connection answers `SocketError`; a task cancelled answers\n # `Cancelled`; what the server does wrong answers `HTTPError`. `!`\n # passes any of them up, `.or_panic` refuses it.\n def self.request(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true, timeout_ms : Int32 = 0, proxy : String | ::Nil = nil) : Response | Cancelled | SocketError | HTTPError\n # A request's head: the request line, `Host`, `Connection` (*connection*:\n # `close`, or `Upgrade`), the caller's headers, and the body's framing -\n # *length*, chunks, or none.\n def self.request_head(method : String, path : String, host : String, headers : Hash(String, String), length : Int64 | ::Nil, chunked : Bool, connection : String) : String\n # Two field names for the same field: ASCII case is the only difference\n # a name may carry.\n def self.same_name?(a : String, b : String) : Bool\n def self.split_crlf(text : String) : Array(String)\n # A status line read: the status and its reason, or why it is not one.\n def self.status_line(line : String) : ::Tuple(Int32, String) | HTTPError\n # What an `https://` request is checked against and offers: the\n # system's roots, and ALPN's `http/1.1`, unless a program sets its own -\n # roots of its own for a server it runs, say.\n def self.tls_config : TlsConfig\n def self.tls_config=(config : TlsConfig) : TlsConfig\n # A token (RFC 9110 §5.6.2): what a method or a field name is made of.\n def self.token?(text : String) : Bool\n # Without the blanks at either end (RFC 9112 §5.1: field values are\n # trimmed of the whitespace around them).\n def self.trim(s : String) : String\n # The 101 an upgrade to *protocol* is answered with, and the connection,\n # which is the caller's from then on: std/websocket's handshake. Any\n # other answer is an `HTTPError` naming it.\n #\n # *proxy* is `open`'s, and so is its credential, on the CONNECT alone;\n # but an upgrade goes through a tunnel for `http://` as for `https://`,\n # the handshake inside it in origin form. `Upgrade` belongs to the\n # connection it is sent on (RFC 9110 §7.8), and a proxy forwarding a\n # request removes `Connection` and every field it names (§7.6.1), so a\n # handshake handed to a forward proxy in absolute form reaches the\n # server, at best, as a plain GET: the gate's proxy, which drops\n # `Connection`, leaves std/websocket's server a request it answers 400.\n def self.upgrade(url : String, protocol : String, headers : Hash(String, String) = {} of String => String, timeout_ms : Int32 = 0, proxy : String | ::Nil = nil) : Upgraded | Cancelled | SocketError | HTTPError\nend\n\n# Requests that keep their connections: an `HTTPClient` holds the connections\n# its answers left open, by host and port, and the next request to that\n# server goes over one of them instead of a new one - the TCP handshake,\n# and the slow start after it, paid once rather than per request. Its\n# methods are `HTTP`'s, redirects, cookies and decoding included:\n#\n# ```\n# client = HTTPClient.new\n# client.get(\"http://127.0.0.1:8080/a\")\n# client.get(\"http://127.0.0.1:8080/b\") # the same connection\n# client.close\n# ```\n#\n# A connection goes back once its answer has been read to the end (inside\n# `open`, up to 64 KiB left unread is read past for it) and the answer\n# allows it; at most *idle* wait per server, and the rest close. A server\n# may close an idle connection whenever it likes (RFC 9112 §9.3.1), so a\n# request whose kept connection turns out closed - the write fails, or it\n# ends before a byte of an answer - is sent again once on a new one, when\n# its method is idempotent (RFC 9110 §9.2.2) and its body a string;\n# anything else answers the failure. One task at a time per request: two\n# tasks sending at once take two connections, and a client is not for\n# sharing across threads. An `https://` connection is kept under the\n# `HTTP.tls_config` it was made with, and handed only to a request made\n# under the same one (`TlsMadeUnder`): after `HTTP.tls_config=` the next\n# request makes a connection of its own, checked against the new roots.\n# A client made with *tls* uses that configuration instead of\n# `HTTP.tls_config`, and one made with *resolver* finds its hosts by it\n# instead of `Resolver.system` - an encrypted resolver, or a hosts text\n# that pins a name to an address: std/secure_dns's `DnsOverHttps` is a\n# client of its own with both, so its server is checked against the roots\n# it was given and reached at the address it was given.\n#\n# A client resumes TLS sessions (RFC 8446 §2.2): the tickets an\n# `https://` server sends are kept for its origin - scheme, host and\n# port, whichever proxy's tunnel they came through - under the TLS\n# configuration they came under, the client's own *tls* or else\n# `HTTP.tls_config` (`TlsMadeUnder`, as a kept connection is), and a new\n# connection to that origin offers the newest, each once.\n# A resumed handshake checks no certificate - the ticket stands for the\n# check the first one made - so a ticket is never offered to another\n# origin, nor under another configuration: after `HTTP.tls_config=`, or\n# roots, an identity, `now`, `min_version` or `require_staple` assigned\n# in place - in `HTTP.tls_config` or in the client's own *tls* - the next\n# connection is a full handshake under the new ones, and that origin's\n# tickets from under the old one are dropped. (std/tls's `TlsSession`\n# holds each ticket to its configuration too; this client drops them\n# where that one would only pass them over.) At most *origin_tickets* are\n# kept an origin - 4 by default, so that as many connections as *idle*\n# keeps by default can resume at once, where std/tls's server and\n# OpenSSL's send 2 a handshake - and *tickets* in all, 256 by default: as\n# many origins as that leaves room for at *origin_tickets* each, the one\n# longest unused dropped first. 0 for either keeps none, and nothing\n# resumes. `close`\n# leaves them. Nothing goes early (0-RTT), whatever a ticket allows: the\n# module's header says why. `connections_resumed`, `tickets_held` and\n# `Response#tls_resumed?` say what was kept and used. What it saves, by\n# bench/std_http_resume.iyi built --release on a 12-core machine other\n# jobs shared (medians of 200, the middle of three runs): the handshake\n# took 0.96 ms full and 0.51 resumed against std/http's server, 1.03 and\n# 0.57 against OpenSSL 3.5's s_server - and a GET on a new connection,\n# which waits about 40 ms at each iyi end that is not the handshake's,\n# 82.98 ms and 82.00, and 42.04 and 40.96.\n#\n# Over HTTP/2 - `https://` where ALPN chose `h2`, or `http://` with\n# *prior_knowledge* (h2c, RFC 9113 §3.3) - a kept connection carries the\n# next request on a new stream, until the server says GOAWAY.\n#\n# HTTPS records (RFC 9460), when `use_https_records` is set: before a new\n# connection to an `https://` origin - not through a proxy, and not for\n# an upgrade - the client asks its resolver for the origin's HTTPS\n# records (`Resolver#https_records`, AliasMode followed) and connects by\n# the first ServiceMode record it can use: one whose mandatory keys it\n# knows - the seven std/dns reads, port and no-default-alpn mandatory by\n# being there (section 9) - and whose protocols (alpn, and http/1.1\n# unless no-default-alpn) hold one of the configuration's `alpn`, or\n# http/1.1 where that is empty. Its target - the record's owner for \".\"\n# - and its port, or the origin's, are where the connection goes; its\n# `ech` is the connection's `ech_configs` (RFC 9848); and the protocols\n# it lists go first in the ClientHello's ALPN, which still offers every\n# one the configuration does (section 7.1.2: a record says which\n# protocols the endpoint has, and cannot take one away). The certificate\n# is checked, and the server named inside, for the origin's host,\n# whatever the target (section 2.3). An `ech` whose ECHConfigList holds\n# no config std/tls can use is passed over, as RFC 9849 section 6.1 has\n# it. The address hints are not read: the target's addresses are asked\n# for as any host's are.\n#\n# An endpoint that does not connect gives way to the next, then to the\n# last AliasMode target and to the origin itself (section 3) - unless\n# every endpoint carries `ech`, when the request fails rather than name\n# the server in the clear (RFC 9848 section 5.1). A TLS refusal is the\n# request's failure, but for one: a server that turned the encrypted\n# ClientHello down, its certificate checked for the config's\n# public_name, is asked once more on a new connection to the same\n# address, under the retry_configs it sent, or without ECH where it sent\n# none, which turns ECH off there (RFC 9849 section 6.1.6); a refusal of\n# that is the failure. No records (NXDOMAIN, NODATA), a set section 2.2\n# refuses, an AliasMode \".\" (the service says it is not there), or no\n# record the client can use: the connection is made as without records.\n# So is one whose lookup failed through a resolver that asks in the\n# clear, as section 3.1 lets a client choose; through encrypted\n# upstreams the failure is the request's, as section 3.1 asks, for\n# there the one who can drop the answer is an attacker taking the\n# records away. `localhost` and addresses are not asked about, and an\n# `http://` URL is not made `https://` by its records (section 9.5's\n# SHOULD): the program wrote the scheme.\n#\n# A record's `ech` and protocols make a configuration of their own - a\n# copy of the client's, made once and kept for the origin while the same\n# ones come under the same configuration - so that a kept connection and\n# a ticket are handed on only under the record they were made by\n# (`TlsMadeUnder` holds `ech_configs`). Off by default: every new\n# connection then waits on one DNS answer or more before its ClientHello\n# (RFC 9848 section 5.3 forbids sending it sooner), with no cache to\n# spare the next, and the records may move a connection to another host\n# and port, which is the program's to ask for.\npub class HTTPClient\n # Every idle connection closed. The client may be used after; it opens\n # new ones.\n def close : Nil\n # Connections this client has opened, and requests sent over one it\n # kept: what a program reads to see that its connections are kept.\n def connections_made : Int32\n # Connections this client opened whose TLS handshake resumed a session\n # by a ticket it kept.\n def connections_resumed : Int32\n def connections_reused : Int32\n def delete(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n def get(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n protected def give_back(key : String, conn : Conn) : Nil\n def head(url : String, headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # The configuration a connection to *origin* is made under by a record\n # whose `ech` is *ech* (nil: none) and whose protocols put *alpn* in its\n # order, *base* being the client's own or `HTTP.tls_config`: *base*\n # itself where those are its own, and otherwise a copy of it with them -\n # the same object every time while *base* is the same (`TlsMadeUnder`),\n # so that a connection kept and a ticket held under it serve the next\n # connection by the same record. It becomes the one the origin's kept\n # connections are taken under (`https_current`). The origin's\n # configurations from under another *base* go.\n protected def https_config(origin : String, base : TlsConfig, ech : Array(UInt8) | ::Nil, alpn : Array(String)) : TlsConfig\n # The configuration *origin*'s last connection by HTTPS records was made\n # under, while *base* is the one it came from - *base* itself where it\n # was made as without records: what a kept connection to the origin\n # must have been made under to be taken. Nil when none was made by them\n # under *base*, and then none is taken.\n protected def https_current(origin : String, base : TlsConfig) : TlsConfig | ::Nil\n # *timeout_ms* and *proxy* are `HTTP.open`'s, for every request the\n # client sends; *tls* and *resolver* are this client's own (above);\n # *origin_tickets* and *tickets* bound the TLS tickets kept, by origin\n # and in all.\n def initialize(idle : Int32 = 4, timeout_ms : Int32 = 0, prior_knowledge : Bool = false, proxy : String | ::Nil = nil, tls : TlsConfig | ::Nil = nil, resolver : Resolver | ::Nil = nil, origin_tickets : Int32 = 4, tickets : Int32 = 256)\n protected def made : Nil\n def open(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true, &block : (Response -> R)) : R | Cancelled | SocketError | HTTPError forall R\n def post(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n # `http://` spoken as HTTP/2 from the first byte, without asking: for a\n # server known to take it.\n def prior_knowledge : Bool\n # The proxy every request goes through, `HTTP.open`'s *proxy*; its\n # connections are kept under it.\n def proxy : String | ::Nil\n def put(url : String, body : String | IyiIO = \"\", headers : Hash(String, String) = {} of String => String) : Response | Cancelled | SocketError | HTTPError\n def request(method : String, url : String, headers : Hash(String, String) = {} of String => String, body : String | IyiIO = \"\", redirects : Int32 = 10, jar : CookieStore | ::Nil = nil, decompress : Bool = true) : Response | Cancelled | SocketError | HTTPError\n # What turns a host name into addresses for this client's connections,\n # instead of `Resolver.system`: encrypted upstreams, or a hosts text\n # alone that pins a name to an address, as std/secure_dns's\n # `DnsOverHttps` pins its server's.\n def resolver : Resolver | ::Nil\n protected def resumed : Nil\n # The session a new connection to *origin* under *tls* offers a ticket\n # from and keeps the server's new ones in; nil when the client keeps\n # none. The origin's tickets from under another configuration are\n # dropped, not left to take room that `TlsSession` would never offer\n # them from. The session handed out is the newest used; past the room\n # *tickets* leaves, the oldest goes.\n protected def session_for(origin : String, tls : TlsConfig) : TlsSession | ::Nil\n # A kept connection to *key*, nil when none waits. An HTTP/2 connection\n # that can take no more requests is closed rather than handed out, and\n # so is an `https://` one made under another TLS configuration than\n # *tls*, the one the request is made under (`TlsMadeUnder`): keyed by\n # scheme, host and port alone, a connection whose server was checked\n # against one set of roots carried requests after `HTTP.tls_config=`\n # named others that would have refused it.\n protected def take(key : String, tls : TlsConfig | ::Nil = nil) : Conn | ::Nil\n # The TLS tickets held for *origin* - `https://host:port`, the host in\n # lower case and the port written even where it is 443 - or for every\n # origin when nil.\n def tickets_held(origin : String | ::Nil = nil) : Int32\n # What this client's `https://` requests are checked against and offer,\n # instead of `HTTP.tls_config`: roots of its own, or ALPN's `h2` first.\n def tls : TlsConfig | ::Nil\n # Whether a new `https://` connection is made by the origin's HTTPS\n # records (the class's header); off by default.\n def use_https_records : Bool\n # Whether a new `https://` connection is made by the origin's HTTPS\n # records (the class's header); off by default.\n def use_https_records=(value : Bool) : Bool\nend\n\n# What the other end did that HTTP does not allow, as a value: `reason` is\n# the sentence, `status` the answer a server gives a request that did it\n# (400, or 413 and 431 for the limits). `message` says it is HTTP's.\npub struct HTTPError\n def initialize(reason : String, status : Int32 = 400)\n def reason : String\n def status : Int32\n def to_s : String\nend\n\n# One endpoint an origin's HTTPS records name, as `HTTP.https_endpoints`\n# reads it: the host and port to connect to, the ECHConfigList to encrypt\n# the ClientHello under (nil: none), and the protocols to offer by ALPN,\n# in order.\npub struct HttpsEndpoint\n def alpn : Array(String)\n def ech : Array(UInt8) | ::Nil\n def host : String\n def initialize(host : String, port : Int32, ech : Array(UInt8) | ::Nil, alpn : Array(String))\n def port : Int32\nend\n\n# A `multipart/form-data` body (RFC 7578) read a part at a time, as it\n# arrives: `next_part` answers the next `Part`, nil after the last, and a\n# part's `read` answers its bytes up to the boundary, \"\" at its end. A part\n# not read to its end is read past by the next `next_part`. The body of a\n# request is `Request#multipart`'s; any `BodyReader` and boundary will do.\n# A part's head is refused past 16 KiB, and a body that ends before its\n# closing boundary is an `HTTPError`.\npub class Multipart\n # The boundary a `Content-Type` of `multipart/...` names, nil when it\n # names none - or one of a length RFC 2046 §5.1.1 does not allow, 1 to\n # 70 characters, which `new` refuses with a panic. The type is the\n # peer's: `Request#multipart` handed a client's 90-character boundary\n # through to that panic (found by bench/std_fuzz_exercise.sh).\n def self.boundary(content_type : String) : String | ::Nil\n # Where *pattern* is in what is held, -1 when it is not there yet.\n protected def find(pattern : String) : Int32\n def initialize(body : BodyReader, boundary : String)\n # The next part, nil after the last.\n def next_part : Part | Nil | Cancelled | SocketError | HTTPError\n # Up to *max* bytes of the part being read, \"\" at its boundary. What is\n # held back is what may yet be the start of the boundary.\n protected def part_read(max : Int32) : String | Cancelled | SocketError | HTTPError\n # One more piece of the body onto what is held: false at its end.\n protected def pull : Bool | Cancelled | SocketError | HTTPError\nend\n\n# A multipart/form-data body to send: fields and files in order. Its\n# boundary is chosen when the body is first asked for - `finish`,\n# `content_type` or `boundary`, whichever comes first, after which no\n# part is added - as `iyi-` and 32 hex digits of the parts' SHA-1, made\n# again from itself in the rare case a part holds it: so it is none of\n# the parts' bytes by construction rather than by chance, the same\n# fields make the same body, and std/http reaches no source of\n# randomness for it. A boundary given to `new` is used as it is, and a\n# part that holds it is refused.\npub class MultipartBuilder\n def boundary : String\n def content_type : String\n def field(name : String, value : String) : self\n def file(name : String, filename : String, data : String, content_type : String = MIME.from_extension(filename)) : self\n # The body, closing boundary and all.\n def finish : String\n # The boundary ends a part where it follows a CRLF, the part's first\n # bytes included, which follow the head's blank line.\n protected def self.holds?(data : String, mark : String) : Bool\n def initialize(boundary : String | ::Nil = nil)\n # A name inside quotes, as browsers write one: a quote and a line break\n # percent-encoded (the WHATWG form encoding), so no field can end the\n # parameter or the line early.\n protected def self.quoted(text : String) : String\nend\n\n# One part of a multipart body: its head's fields, its name and file name\n# from `Content-Disposition`, and its bytes, read up to its boundary.\npub class Part\n def content_type : String | ::Nil\n # The file name a browser sent the part under, `filename=`.\n def filename : String | ::Nil\n def finished? : Bool\n def header(name : String) : String | ::Nil\n def headers : Hash(String, String)\n def initialize(owner : Multipart, headers : Hash(String, String))\n # The form field's name, `name=` of `Content-Disposition`.\n def name : String | ::Nil\n # The part's next bytes, at most *max*; \"\" at its end.\n def read(max : Int32 = 65536) : String | Cancelled | SocketError | HTTPError\n # The rest of the part as one string, refused past *limit* bytes.\n def read_all(limit : Int64 = 2147483647_i64) : String | Cancelled | SocketError | HTTPError\nend\n\n# What a server's block is handed: one request. Its body is `body`, read\n# whole before the block ran, or under `stream_bodies` read by the block\n# from `body_io` as it arrives; `body` then reads the rest itself, and a\n# body that cannot be read - the client went, or sent one that is not\n# HTTP - ends the connection with that sentence rather than be answered as\n# if it were whole.\npub class Request\n def body : String\n def body_io : BodyReader\n # The pairs of the request's `Cookie` header, the first of a name kept.\n def cookies : Hash(String, String)\n # The fields of a form: the query's, and an\n # `application/x-www-form-urlencoded` body's after them. The body is\n # read for it.\n def form : Params\n def header(name : String) : String | ::Nil\n def headers : Hash(String, String)\n # A request whose body is in hand: what `parse_request` answers.\n def initialize(method : String, path : String, query : String | ::Nil, headers : Hash(String, String), body : String, version : String = \"HTTP/1.1\", remote_address : String | ::Nil = nil)\n # A request whose body is still on the connection.\n def initialize(method : String, path : String, query : String | ::Nil, headers : Hash(String, String), version : String, reader : BodyReader, remote_address : String | ::Nil = nil)\n def method : String\n # The body as `multipart/form-data` parts, read as they arrive; nil when\n # the `Content-Type` is not multipart or names no boundary\n # (`Multipart.boundary`).\n def multipart : Multipart | ::Nil\n def path : String\n # The client's certificate chain, leaf first, as std/tls checked it:\n # under `serve(tls:)` with `client_roots` set, the certificate the\n # client presented; empty otherwise.\n def peer_certificates : Array(Certificate)\n # The client's certificate chain, leaf first, as std/tls checked it:\n # under `serve(tls:)` with `client_roots` set, the certificate the\n # client presented; empty otherwise.\n def peer_certificates=(value : Array(Certificate)) : Array(Certificate)\n def query : String | ::Nil\n # The client's address, as the server's socket saw it (no port): nil\n # for a request a program made, or when the client had already gone. A\n # proxy in front is what this names; `X-Forwarded-For` is the proxy's\n # word, and a header any client can write.\n def remote_address : String | ::Nil\n def tls_resumed=(resumed : Bool) : Bool\n # Whether the TLS connection the request came over resumed a session by\n # a ticket, under `serve(tls:)`: its `peer_certificates` are then the\n # ticket's. False in the clear.\n def tls_resumed? : Bool\n def version : String\nend\n\n# A response: what a server's block answers, and what a client is handed.\n#\n# To write one: `Response.new(200, \"hello\")`, a status and a string;\n# `Response.new(200, io)`, a stream the server copies out and closes, with\n# the caller's `Content-Length` when it gives one and chunked otherwise;\n# `Response.stream(200) { |w| w.write(\"...\") }`, whose block writes as it\n# goes; `Response.upgrade(headers) { |socket, leftover| ... }`, a 101 after\n# which the connection is the block's. `add_header` writes a field on a\n# line of its own, as each `Set-Cookie` must be.\n#\n# What a client is handed: the status, reason and headers, and the body -\n# `body` whole, or `body_io` as it arrives inside `HTTP.open`'s block.\npub class Response\n # One more line of the field *name*, written on a line of its own: what\n # two `Set-Cookie`s need, since the \", \" a field's lines are joined with\n # is part of a cookie's date.\n def add_header(name : String, value : String) : Nil\n # The lines of each field written on more than one, by its name in\n # `headers`; nil when every field is on one line.\n def apart : Hash(String, Array(String)) | ::Nil\n # The body: whole. Inside `HTTP.open`'s block it is read here, the first\n # time it is asked for, and a body that cannot be read panics with why;\n # `body_io` answers that as a value.\n def body : String\n # The body as it arrives. For a response a program made, its string.\n def body_io : BodyReader\n # Whether the client undid the body's `gzip`, `deflate` or `zstd`\n # coding; the `Content-Encoding` and `Content-Length` that described it\n # are gone from the headers then.\n def decoded? : Bool\n # A header by name, as the server spelled it or otherwise: a field\n # name is case-insensitive (RFC 9110 §5.1), and `content-length` is as\n # common on the wire as `Content-Length`.\n def header(name : String) : String | ::Nil\n # A field's lines apart, in the order they came: `header` joins them\n # with \", \", and `Set-Cookie` is the field that join breaks (RFC 9110\n # §5.3), since a cookie's `Expires` date has a comma in it - two cookies\n # read as \"a=1; Expires=Wed, 21 Oct 2026 07:28:00 GMT, b=2\", which no\n # split takes back apart. Empty for a field that is not there.\n def header_values(name : String) : Array(String)\n def headers : Hash(String, String)\n # A response a client read: its body comes from *reader*, and *url* is\n # where it came from.\n def initialize(status : Int32, reason : String, headers : Hash(String, String), apart : Hash(String, Array(String)) | ::Nil, reader : BodyReader, url : String, decoded : Bool)\n def initialize(status : Int32, reason : String, headers : Hash(String, String), body : String, apart : Hash(String, Array(String)) | ::Nil = nil)\n # A response to write: the reason is the status's own, the headers the\n # caller's. `Response.new(200, \"hello\")` is the common shape.\n def initialize(status : Int32, body : String = \"\", headers : Hash(String, String) = {} of String => String)\n # A response whose body is *source*, copied out by the server and\n # closed after.\n def initialize(status : Int32, source : IyiIO, headers : Hash(String, String) = {} of String => String)\n # The body read whole into the response, which is answered; why not,\n # when it cannot be.\n def load : Response | Cancelled | SocketError | HTTPError\n def reason : String\n def source : IyiIO | ::Nil\n def status : Int32\n # A response whose body the block writes as it goes, chunked.\n def self.stream(status : Int32, headers : Hash(String, String) = {} of String => String, &writer : (BodyWriter -> Nil)) : Response\n def success? : Bool\n def takeover : Proc(IyiSocket | TlsStream, String, Nil) | ::Nil\n protected def takeover=(takeover : Proc(IyiSocket | TlsStream, String, Nil)) : Proc(IyiSocket | TlsStream, String, Nil)\n def tls_resumed=(resumed : Bool) : Bool\n # Whether the `https://` connection the answer came over resumed a TLS\n # session by a ticket an `HTTPClient` kept, rather than a full\n # handshake; false in the clear and for one a program made.\n def tls_resumed? : Bool\n # A 101 Switching Protocols with *headers* (`Upgrade` and the\n # protocol's own), after which the connection is the block's: it is\n # handed the connection - the socket, or under `serve(tls:)` the\n # `TlsStream` over it - and the bytes the client sent after its request,\n # and the connection closes when it returns.\n def self.upgrade(headers : Hash(String, String), &takeover : (IyiSocket | TlsStream, String -> Nil)) : Response\n # The URL the answer came from, after any redirects; nil for one a\n # program made.\n def url : String | ::Nil\n def writer : Proc(BodyWriter, Nil) | ::Nil\n protected def writer=(writer : Proc(BodyWriter, Nil)) : Proc(BodyWriter, Nil)\nend\n\npub struct Server\n # The headers a coded body goes with: the coding, and `Vary` so that a\n # cache keeps the answers apart. A `Content-Length` the block gave is\n # the plain body's, and goes.\n protected def self.coded(response : Response, coding : String) : Response\n # The coding *response* goes in to the client of *req*, or nil: one the\n # client takes (RFC 9110 §12.5.3, a `q` of 0 refusing it), for an\n # answer with a body and a type worth compressing, and no coding of its\n # own. A whole body goes `zstd` when the client's `q` for it is at least\n # its `q` for gzip, and `gzip` otherwise. The tie goes to zstd for the\n # server's time: measured on this machine, release builds, on\n # src/std/http.iyi (199,134 bytes) `Gzip.compress` wrote 54,484 bytes in\n # 5 ms and `Zstd.compress` 61,753 in 1 ms, and on CHANGELOG.md (1.5 MB)\n # 552,446 in 58 ms against 589,785 in 13 ms - a quarter of the time, for\n # an eighth more bytes at most. A stream - an `IyiIO` or a\n # `Response.stream` - goes gzip or as it is: std/compress's `Deflater`\n # writes each piece as it comes, where std/zstd's compressor takes its\n # input whole, and a frame a write would code each piece alone.\n def self.coding_for(req : Request, response : Response) : String | ::Nil\n # A string body compressed whole in *coding*, `zstd` or `gzip`, with\n # the headers that say so. `Zstd.compress`'s frame needs a window of\n # 1 MiB at most, inside the 8 MB RFC 9659 §3 holds an encoder to.\n protected def self.compressed(response : Response, coding : String) : Response\n # A response's head. `Content-Length` is *length*'s - the caller's own\n # line is checked against it, and kept only where no body follows it: a\n # HEAD, or a 304 that names the length a 200 would have (RFC 9110 §8.6) -\n # or `Transfer-Encoding: chunked` when *chunked*, or neither for a body\n # the connection's close ends. `Connection` says what the connection will\n # do next, except on an *upgrade*, whose own line says it. A 1xx, 204 or\n # 304 has no content (RFC 9110 §15), and writes neither a body nor a\n # length of its own (§8.6): the body was read by the client as the start\n # of the next answer, and `Content-Length: 0` on a 304 told a cache that\n # the resource had become empty. A field added on lines of its own\n # (`add_header`) is written a line each.\n def self.format_head(response : Response, head : Bool, close : Bool, length : Int64 | ::Nil, chunked : Bool, upgrade : Bool = false) : String\n # The response as it goes on the wire, whole: `format_head` for its\n # string body, and the body.\n def self.format_response(response : Response, head : Bool, close : Bool) : String\n # One connection: requests until the client is done. A request that is\n # not HTTP is answered with why and the connection closed, since the\n # bytes after it cannot be trusted to be a request either. Requests that\n # came in one read are read where they lie, one after another. A\n # connection that opens with HTTP/2's preface is served as HTTP/2. Under\n # *tls* the handshake comes first, and the connection ends with\n # close_notify. A connection this side wrote last on - an answer, a\n # refusal, a GOAWAY - closes by lingering (`Conn#close`), so the reset\n # a close with the client's bytes unread is cannot take what was\n # written from the client before it reads it. One that timed out or was\n # cancelled, with nothing of this side's waiting to be read, closes at\n # once, and so does one whose client said `close` and sent nothing past\n # its request (`serve_h1`).\n def self.handle(client : IyiSocket, handler : Proc(Request, Response), listener : IyiSocket | ServeStop | Nil = nil, idle : Hash(UInt64, Conn) | ::Nil = nil, stream_bodies : Bool = false, compress : Bool = false, timeout_ms : Int32 = 0, tls : TlsServerConfig | TlsFrozenServerConfig | Nil = nil, request_timeout_ms : Int32 = 0) : Nil\n def initialize\n # A request's head - the text before its blank line - read and checked,\n # or why it is not one.\n def self.parse_head(text : String) : RequestHead | HTTPError\n # A request at *from* in `text`, if a whole one is there. No request and\n # no error means \"not yet\". `consumed` counts from the start of `text`.\n def self.parse_request(text : String, from : Int32 = 0) : ParsedRequest\n # Serve `listener` until it is closed, on *workers* threads: each\n # connection is a task on the pool `spawn_parallel` starts (SPEC.md\n # III.4.13), started by whichever worker is free and served there to\n # its end, while the thread that called this accepts. The block runs on\n # the connection's worker, once per request, on several workers at\n # once; what it answers is written back.\n # The close ends a connection waiting on its client too, for its next\n # request or for the rest of one, and one that is answering ends when its\n # answer is written: a keep-alive client that sat idle kept `serve` from\n # returning for as long as it stayed, and so did one that had sent\n # what has arrived of a request so far - one byte of it was enough.\n #\n # *stream_bodies*: the block reads each request's body from `body_io`\n # rather than being handed it whole. *compress*: an answer whose type\n # std/mime calls compressible goes zstd or gzip to a client that\n # accepts one (`coding_for`).\n #\n # *timeout_ms*: a request's head must have arrived within it, counted\n # from when the server starts waiting for one - a kept-alive connection's\n # idle time included - and no read of a body nor write of an answer may\n # wait longer; a connection that does is closed. Without it, a client\n # that sends a byte of its head every few seconds (Slowloris) holds a\n # connection and its fiber for as long as it likes, and so does one that\n # reads none of its answer. A minute by default, nginx's\n # `client_header_timeout`; 0 for none. A connection handed over by\n # `Response.upgrade` waits as its protocol likes.\n #\n # *request_timeout_ms*: the whole request - head and body - must have\n # arrived within it, counted from the same moment as *timeout_ms*, as\n # Go's `ReadTimeout` beside its `ReadHeaderTimeout`; the head is held to\n # the sooner of the two. Without it a body sent a byte every few seconds\n # was bounded per read alone, and held its connection for as long as it\n # dripped. A block that reads the body itself (*stream_bodies*) reads\n # under it until the block returns, and what it leaves unread is read\n # past under it too. Over HTTP/2 the wait for each request and its body\n # is held to it on the connection the same way, the head to the sooner\n # of the two. Five minutes by default, which a 64 MiB body (`MAX_BODY`)\n # meets at 224 KB/s; a server taking large uploads over slow links\n # raises it. 0 for none. A connection past it is closed, unanswered, as\n # one past *timeout_ms* is.\n #\n # *max_connections*: at most this many connections are served at once;\n # 1024 by default, 0 for no limit. Past it the server stops accepting\n # until one ends, so the next connection waits in the kernel's listen\n # queue - queued, not refused, as Go's `netutil.LimitListener` does - and\n # is served in its turn; a client that gives up waiting goes unserved.\n # Each served connection holds its place until it closes, a kept-alive\n # one between requests too, which *timeout_ms* bounds. The process's\n # own limit on descriptors may come first.\n #\n # *tls*: each connection is TLS (std/tls's `TlsServer`, 1.3 or 1.2) under this\n # configuration, its handshake held to *timeout_ms* too and run on the\n # worker that serves it; one whose handshake fails is closed, the\n # client told why by an alert. Inside it a connection that opens with\n # HTTP/2's preface - as a client whose ALPN chose `h2` does, when\n # `tls.alpn` offers it - is served HTTP/2, and any other HTTP/1.1. A\n # `Response.upgrade` block over TLS is handed the `TlsStream`, so a\n # WebSocket is `wss://`. On more than one worker the config is a\n # `TlsFrozenServerConfig` - `config.frozen` - since every worker reads\n # it at once: its ticket keys are one set behind a lock, so a ticket a\n # connection on one worker was sent resumes on any other, and an\n # identity source given to `frozen` is a block the compiler checked. A\n # `TlsServerConfig` itself is refused at the call (\"Server.serve hands\n # `tls : TlsServerConfig` to other threads, several at once, and it is\n # not Share: ... Pass `config.frozen` ...\"), not frozen here: `frozen`\n # is a copy, so a property assigned after the server started would be\n # served by no worker and nothing would say so, and a config with an\n # `identity_source` cannot be frozen without its source given again as\n # a block the compiler checks. Under `workers: 1` either is served.\n #\n # *workers*: the pool's size, `IyiWorkers.count` by default - `IYI_WORKERS`\n # from the environment, else the cores this process may run on. The\n # pool is the process's one, and it does not grow or shrink once it has\n # started, so a count other than the one it started with panics. Every\n # worker runs the block at once, so the compiler checks it where it is\n # written as it checks a parallel task's (`@[Share]`, SPEC.md III.4.4):\n # every value it captures must be `Share` - a table behind a `Mutex`\n # (std/mutex), a count in an `Atomic`, a `SharedRouter` (std/router) -\n # every captured local assigned once, and the constants and class\n # variables it names `Share`. The refusal names the variable, why its\n # type is not `Share`, and the two ways out: the lock, or `workers: 1`.\n # `workers: 1`, written as the literal, serves on the calling thread\n # alone - each connection a task beside the accept loop, as `spawn`\n # makes one - and its block is not checked: a block that calls a\n # `Router`, appends to an `Array` or keeps a handle that is not `Share`\n # is served there. A count in a variable is checked whatever it holds,\n # since the compiler cannot know it is 1. A count of 1 from the default\n # (`IYI_WORKERS=1`, one core) serves on the calling thread too.\n #\n # A connection stays on the worker that started it (III.4.13, part 1):\n # connections spread over the workers free as they arrive, and one kept\n # alive is not moved later. A block that computes without parking holds\n # its worker meanwhile, as any task does. The listener is the calling\n # thread's, and a close of it is made there: a block that stops the\n # server sends on a `Channel` a task beside the server receives from,\n # and that task closes it. At the stop each connection waiting on its\n # client is closed on its own worker, sent close_notify there first\n # under *tls*.\n def self.serve(listener : IyiSocket, stream_bodies : Bool = false, compress : Bool = false, timeout_ms : Int32 = 60000, tls : C = nil, request_timeout_ms : Int32 = REQUEST_TIMEOUT, max_connections : Int32 = MAX_CONNECTIONS, workers : Int32 = IyiWorkers.count, &handler : (Request -> Response)) : Nil forall C\n # `serve` with a value for its handler rather than a block: a\n # `SharedProc`, a `SharedRouter` (std/router), or one of the program's\n # own, whose `call(request : Request) : Response` answers each request,\n # as Go's `http.Handler` does. On more than one worker it must be\n # `Share` (SPEC.md III.4.4), since every connection on every worker\n # calls the one value at once: the compiler refuses one that is not at\n # the argument, naming the field that failed and the two ways out - a\n # count in an `Atomic`, a pool or a cache behind a `Mutex`, a\n # `SharedRouter` for a `Router`, or `workers: 1`, under which any value\n # with that `call` is served on the calling thread, a `Router`\n # included. What the check does not see is the edge `spawn_parallel`\n # has too: a method the handler calls that writes a module's state. A\n # struct handler is copied into each connection's task, and an `Atomic`\n # in it with it: what the connections share is a class, or held by\n # one.\n #\n # The rest is the block form's: *stream_bodies*, *compress*,\n # *timeout_ms*, *tls* (`config.frozen` on more than one worker),\n # *request_timeout_ms*, *max_connections* and *workers*, HTTP/2 by its\n # preface, `Response.upgrade`, and a close of *listener* - on the\n # thread that called this - ending every connection waiting on its\n # client and returning once the others have answered.\n def self.serve(listener : IyiSocket, handler : H, stream_bodies : Bool = false, compress : Bool = false, timeout_ms : Int32 = 60000, tls : C = nil, request_timeout_ms : Int32 = REQUEST_TIMEOUT, max_connections : Int32 = MAX_CONNECTIONS, workers : Int32 = IyiWorkers.count) : Nil forall H, C\n protected def self.writer_for(socket : IyiSocket | TlsStream, chunked : Bool, gzip : Bool) : BodyWriter\nend\n\n# A TLS connection used as a socket is: read into a buffer, written to,\n# closed, its timeouts set - by a `Conn`, a `BodyWriter`, and a connection\n# handed over by an upgrade (`wss://` at either end). What TLS refuses is a\n# `SocketError` carrying TLS's own sentence (`layer_reason`), so answers\n# keep the types they have over a plain socket.\npub class TlsStream\n # The socket closed at once, nothing sent or read first.\n def abort : Nil\n # close_notify, the sending half shut, and what the peer still sends\n # read past for a bounded time before the socket closes\n # (`TlsConnection#close`).\n def close : Nil\n # close_notify and the sending half shut; reads go on until the peer's\n # own (`TlsConnection#close_write`).\n def close_write : SocketError | ::Nil\n def closed? : Bool\n def initialize(tls : TlsConnection, host : String)\n # The peer's certificate chain, leaf first, as the handshake checked it:\n # the server's, at a client; the client's, at a server that asked.\n def peer_certificates : Array(Certificate)\n # Up to *count* bytes into *buffer*: 0 once the peer closed with\n # close_notify.\n def read(buffer : Pointer(UInt8), count : Int32) : Int32 | Cancelled | SocketError\n # Up to *max_bytes* as a string, what is held first: \"\" once the peer\n # closed with close_notify. `IyiSocket#read`'s form, for a block an\n # upgrade hands either.\n def read(max_bytes : Int32 = 4096) : String | Cancelled | SocketError\n # How long a read or a write of the connection under it may wait.\n def read_timeout_ms=(ms : Int32 | ::Nil) : Int32 | ::Nil\n # Whether the handshake resumed a session (`TlsConnection#resumed?`).\n def resumed? : Bool\n def write(text : String) : Int32 | Cancelled | SocketError\n def write_timeout_ms=(ms : Int32 | ::Nil) : Int32 | ::Nil\nend\n\n# What `HTTP.upgrade` answers: the 101, and the connection, which is the\n# caller's now - the socket, and the bytes the server sent after its head.\npub class Upgraded\n def initialize(response : Response, socket : IyiSocket | TlsStream, leftover : String)\n def leftover : String\n def response : Response\n # The connection: the socket, or for `https://` the `TlsStream` over it.\n def socket : IyiSocket | TlsStream\nend\n\nimpl Error for HTTPError\n def message : String\nend\n"
},
{
"name": "std/http_proxy",
@@ -538,7 +538,7 @@
{
"name": "std/socket",
"path": "src/std/socket.iyi",
- "sourceSha256": "fe78564802a8a020e4fff120d2f676e30b86a1f5b31611d71c23f2f5b1ffed2f",
+ "sourceSha256": "eff1d8cd1e03a482ece90494c957df9f6d962808444e75f56a8b4843b2d5c894",
"surface": "module std/socket\n\n# What a resolver answers and is built from, under the names a program\n# that connects by name already imports from here.\npub alias DnsError = Std::Dns::DnsError\n\n# An encrypted way to a name server, asked by a resolver `Resolver#encrypted`\n# made instead of its plaintext servers: std/secure_dns's `DnsOverTls`\n# (RFC 7858) and `DnsOverHttps` (RFC 8484). The trait is here and they are\n# there because they speak std/tls and std/http, which import this module,\n# and R-1 refuses the cycle - as std/http's `CookieStore` is std/cookie's\n# to implement.\npub trait DnsUpstream\n # Its connections closed; the next exchange opens new ones.\n abstract def close : Nil\n # The answer to each of *queries*, in their order, each one whole\n # message as it came; or why there are none. *timeout_ms* bounds each\n # wait on the server. An answer is matched to its query by the\n # transport - by ID over one connection, by request over HTTP - and the\n # resolver then reads it as it reads a datagram.\n abstract def exchange(queries : Array(Bytes), timeout_ms : Int32) : Array(Bytes) | DnsError | Cancelled\n # How a message names it: `dns.test (127.0.0.1:853) over TLS`.\n abstract def label : String\n # Whether its queries carry the ID 0: DNS over HTTPS's, which RFC 8484\n # 4.1 asks for so that an HTTP cache sees one request for one question.\n # The answer must carry the ID the query did all the same.\n abstract def zero_id? : Bool\nend\n\n# Either family's address: what a resolver answers for a name.\npub alias IPAddress = IPv4Address | IPv6Address\n\npub struct IPv4Address\n def b0 : UInt8\n def b1 : UInt8\n def b2 : UInt8\n def b3 : UInt8\n def initialize(b0 : UInt8, b1 : UInt8, b2 : UInt8, b3 : UInt8)\n def to_s : String\nend\n\n# Sixteen bytes in network order, held as two words: `high` is bytes 0 to\n# 7. Written the RFC 5952 way - lowercase, no leading zeros, the longest\n# run of two or more zero groups as `::`, the first of two equal runs -\n# and an IPv4-mapped address with its dotted tail, `::ffff:192.0.2.1`.\npub struct IPv6Address\n # Byte *index*, 0 to 15.\n def [](index : Int32) : UInt8\n # The 16-bit group *index*, 0 to 7. Not `group`: that name is the task\n # group's in iyi (SPEC.md III.4.9).\n def hextet(index : Int32) : Int32\n def high : UInt64\n def initialize(high : UInt64, low : UInt64)\n def low : UInt64\n # The sixteen bytes at *bytes*, network order.\n def self.new(bytes : Pointer(UInt8)) : IPv6Address\n def to_s : String\nend\n\npub class IyiSocket\n # Room for any family's `sockaddr`, zeroed, so a unix path the kernel\n # did not end with a NUL still ends.\n def self.__address_room : Pointer(UInt8)\n # A `sockaddr` the kernel filled, as text: dotted IPv4, IPv6 written as\n # RFC 5952 has it, or a unix socket's path.\n def self.__address_text(addr : Pointer(UInt8), family : Int32) : String\n # What every connected and accepted socket starts with: TCP_NODELAY, on\n # TCP; a unix socket has no Nagle.\n def self.__send_at_once(fd : Int32, family : Int32) : Nil\n def self.__sys3(number : Int64, a : Int64, b : Int64, c : Int64) : Int64\n def self.__sys5(number : Int64, a : Int64, b : Int64, c : Int64, d : Int64, e : Int64) : Int64\n def self.__sys6(number : Int64, a : Int64, b : Int64, c : Int64, d : Int64, e : Int64, f : Int64) : Int64\n def self.__sys_accept(fd : Int32, addr : Pointer(UInt8), addrlen : Pointer(UInt32), family : Int32) : Int32\n def self.__sys_addr_size(family : Int32) : Int64\n def self.__sys_bind(fd : Int32, addr : Pointer(UInt8), family : Int32) : Int32\n # Every close of a socket in this file goes through here. `__iyi_close` is\n # the descriptor close, and on Windows a socket is not a descriptor:\n # `CloseHandle` on a SOCKET releases the kernel handle and leaves Winsock's\n # half of it standing, and the graceful close that makes a peer's read\n # answer EOF rather than a reset is `closesocket`'s.\n def self.__sys_close(fd : Int32) : Nil\n # A raw syscall's negative errno, as the number.\n def self.__sys_code(result : Int64) : Int32\n # *ms* is `connect_to`'s wait's: the call answers EINPROGRESS at once.\n def self.__sys_connect(fd : Int32, addr : Pointer(UInt8), family : Int32, ms : Int32) : Int32\n def self.__sys_connects_blocking?(family : Int32) : ::Bool\n def self.__sys_dgram_connect(fd : Int32, addr : Pointer(UInt8), family : Int32) : Int32\n def self.__sys_dgram_send(fd : Int32, buf : Pointer(UInt8), len : UInt64) : Int64\n # The resolver's datagram socket: see the darwin arm. The raw call\n # answers the negative errno, which `__sys_code` reads.\n def self.__sys_dgram_socket(family : Int32) : Int32\n def self.__sys_dgram_wait(fd : Int32, ms : Int32) : ::Bool | Cancelled\n def self.__sys_getpeername(fd : Int32, addr : Pointer(UInt8), addrlen : Pointer(UInt32)) : Int32\n def self.__sys_getsockname(fd : Int32, addr : Pointer(UInt8), addrlen : Pointer(UInt32)) : Int32\n def self.__sys_in_progress?(result : Int32) : ::Bool\n def self.__sys_listen(fd : Int32, backlog : Int32) : Int32\n def self.__sys_no_sigpipe(fd : Int32) : Nil\n def self.__sys_nodelay?(fd : Int32) : ::Bool\n # `ppoll` on one descriptor for *ms* milliseconds - a timespec, or none\n # for -1 - and no signal mask. 0 when the time ran out.\n def self.__sys_poll(fd : Int32, events : Int32, ms : Int32) : Int32\n def self.__sys_recv(fd : Int32, buf : Pointer(UInt8), len : UInt64) : Int64\n # MSG_NOSIGNAL (0x4000): a write to a connection the peer closed answers\n # EPIPE instead of killing the process with SIGPIPE.\n def self.__sys_send(fd : Int32, buf : Pointer(UInt8), len : UInt64, ms : Int32) : Int64\n # TCP_NODELAY (1) at IPPROTO_TCP (6): Nagle's algorithm off when *on*.\n def self.__sys_set_nodelay(fd : Int32, on : ::Bool) : Int32\n # F_GETFL (3), then F_SETFL (4) with O_NONBLOCK (0x800) added.\n def self.__sys_set_nonblock(fd : Int32) : Int32\n def self.__sys_set_reuseaddr(fd : Int32) : Int32\n # IPV6_V6ONLY (26) at IPPROTO_IPV6 (41).\n def self.__sys_set_v6only(fd : Int32) : Int32\n def self.__sys_shutdown_write(fd : Int32) : Int64\n # SO_ERROR (4) at SOL_SOCKET (1): what a connect that was in progress\n # ended with, 0 when it went through.\n def self.__sys_sock_error(fd : Int32) : Int32\n def self.__sys_socket(family : Int32) : Int32\n # A raw syscall answers the negative errno: EAGAIN is 11, EINPROGRESS 115.\n def self.__sys_would_block?(result : Int64) : ::Bool\n def accept : IyiSocket | Cancelled | SocketError\n # The order `connect` tries a name's addresses in (RFC 8305 section 4):\n # one family's first address, then the other's, alternating, each family\n # in the resolver's order, and the longer family's rest at the end.\n # IPv6 goes first when this machine has a route to the first IPv6\n # address - RFC 6724's first rule, asked the way glibc's getaddrinfo\n # asks it, by connecting a UDP socket, which sends nothing - and IPv4\n # first when it has none, so a machine without IPv6 does not start every\n # connect with an attempt that cannot leave it. A name of one family is\n # left in the resolver's order, and asks nothing.\n def self.attempt_order(addresses : Array(IPAddress)) : Array(IPAddress)\n def self.await_readable(fd : Int32, ms : Int32 = -1) : ::Bool | Cancelled\n def self.await_writable(fd : Int32, ms : Int32 = -1) : ::Bool | Cancelled\n # A timeout as the waits take it: milliseconds, -1 for none.\n def self.bound(ms : Int32 | ::Nil) : Int32\n protected def bound_path(path : String) : Nil\n # *path* with the NUL a system call reads to.\n def self.c_path(path : String) : Pointer(UInt8)\n def close : Nil\n # The sending half ended, the receiving half kept: the peer reads the\n # end of the stream after what was written, and this end goes on reading\n # what the peer still sends until its own end. `shutdown(SHUT_WR)` on\n # POSIX, `shutdown(SD_SEND)` on Windows. A socket that is not connected\n # answers the platform's refusal as a `SocketError`; `close` is still\n # what gives the socket back.\n def close_write : SocketError | ::Nil\n def closed? : ::Bool\n # A host name is resolved by the machine's resolver (`Resolver.system`);\n # a name that does not resolve is a `SocketError` whose `unresolved?` is\n # true and whose sentence names the host. It panicked, \"cannot resolve\n # address: \\\"example.com\\\"\", for every name but `localhost`, so\n # `HTTP.get(\"http://example.com/\")` could not run.\n #\n # A name with more than one address is raced, Happy Eyeballs v2 (RFC\n # 8305): the first address is tried, the next `ATTEMPT_DELAY_MS` later\n # without waiting for the first to fail, or at once when an attempt\n # fails, and so on down the list (`attempt_order`'s: IPv6 first when\n # this machine has a route to it, the families alternating). The first\n # handshake to finish wins, and every other attempt is cancelled and its\n # socket closed before `connect` answers. The addresses were tried one\n # after another, IPv4 first, each with the whole timeout: a name whose\n # first address dropped SYNs cost all of *timeout_ms* - or the kernel's\n # two minutes of retries, unbounded - before the second was tried. Now\n # it costs the delay: 250 ms to a name whose first address is a full\n # listen queue and whose second listens (`bench/std_dns_exercise.sh`,\n # under a 5,000 ms bound).\n #\n # `localhost` is raced the same way, 127.0.0.1 and then ::1, without\n # asking the routing table: a listener here binds `localhost` at\n # 127.0.0.1, so it goes first, and Windows, which names ::1 first, has\n # dev servers that bind ::1 alone. This replaces the rule that tried\n # ::1 only after 127.0.0.1 refused: Windows takes 2 s to refuse a\n # connect, so a ::1-only listener was reached in 2017 ms and a port\n # nobody holds refused in 4059 ms, where now ::1 starts 250 ms after\n # 127.0.0.1 whatever 127.0.0.1 is still doing.\n #\n # *timeout_ms* bounds the whole connect, every attempt together (-1: as\n # long as the kernel waits, which on Linux is about two minutes of SYN\n # retries for a host that drops them). It bounded each address's\n # handshake, which a race has no use for: an attempt started 500 ms in\n # would have run 500 ms past the caller's bound. A connect that runs out\n # is a `SocketError` whose `timed_out?` is true. The name's lookup is\n # outside it, held by the resolver's own `timeout_ms` and `attempts`.\n #\n # A name none of whose addresses connects is one `SocketError` that\n # names each address in the order tried and how its attempt ended,\n # `tried`: `cannot connect to example.com:443: [2001:db8::1]:443 timed\n # out; 192.0.2.1:443 connection refused`. It answered the first\n # address's failure alone. Its `code` is the first attempt's, or a\n # timeout's when the time ran out first. A name with one address is\n # connected to as an address is, and fails with that address's error.\n def self.connect(host : String, port : Int32, timeout_ms : Int32 = -1) : IyiSocket | Cancelled | SocketError\n # The same, with a host name resolved by *resolver*: name servers, a\n # hosts file and search list of the program's choosing, rather than the\n # machine's. An address and `localhost` are what they are either way.\n def self.connect(host : String, port : Int32, resolver : Resolver, timeout_ms : Int32 = -1) : IyiSocket | Cancelled | SocketError\n # A connect to *addr*, waiting at most *ms* milliseconds for the\n # handshake (-1: as long as the kernel does). The resolver's TCP query\n # and `connect` to an address are its callers with a bound; a race's\n # attempts wait as the kernel does, and the race keeps the bound. The\n # bound reaches `__sys_connect` as well as the wait after it, because\n # on Windows that call is the wait: it parks in `ConnectEx` until the\n # handshake is over.\n def self.connect_to(addr : Pointer(UInt8), family : Int32, peer : String, ms : Int32 = -1) : IyiSocket | Cancelled | SocketError\n # The stream socket listening at *path*.\n def self.connect_unix(path : String) : IyiSocket | Cancelled | SocketError\n # *host* and *port* as a message names them: an IPv6 literal in its\n # brackets, as a URL writes it. Bare, `::1:57456` was an address with\n # no port, and `ff02::1:80` another address altogether.\n def self.endpoint(host : String, port : Int32) : String\n # The address family, the platform's number for it: an accepted\n # connection is its listener's, and on Windows the socket `AcceptEx`\n # fills in has to be made in it. With `fd`, what makes the socket again\n # on another thread (std/http's `Server.serve`, on its pool).\n def family : Int32\n def fd : Int32\n def initialize(fd : Int32, family : Int32 = AF_INET)\n def self.listen(host : String, port : Int32, backlog : Int32 = 128) : IyiSocket\n def self.listen(port : Int32, backlog : Int32 = 128) : IyiSocket\n # A stream socket at *path*. The path must not exist: a socket file an\n # earlier run left behind is refused rather than taken over, since\n # which program owns it is not something a bind can know. `close`\n # removes the file.\n def self.listen_unix(path : String, backlog : Int32 = 128) : IyiSocket\n def local_address : String\n # The port sits at the same two bytes of a `sockaddr_in` and a\n # `sockaddr_in6`.\n def local_port : Int32\n # `localhost` in any case, and with the one trailing dot of a name\n # written fully qualified: a host name's case is not part of it (RFC\n # 4343), and getaddrinfo, Python and curl all answer 127.0.0.1 for\n # `LOCALHOST` and `localhost.`. Matched exactly, these were \"cannot\n # resolve address: \\\"LOCALHOST\\\"\", and so were `Localhost` and\n # `localhost.`.\n def self.localhost?(host : String) : ::Bool\n # What `localhost` stands for, in the order `connect` tries it.\n def self.loopback : Array(IPAddress)\n def self.make_sockaddr_in(ip : IPv4Address, port : Int32) : Pointer(UInt8)\n # A `sockaddr_in6`: family, port, a zero flow label, the address, a zero\n # scope. The family's number and where it sits are the platform's.\n def self.make_sockaddr_in6(ip : IPv6Address, port : Int32) : Pointer(UInt8)\n # A `sockaddr_un`: the family, then the path and its NUL. The room for\n # the path is the platform's - 104 bytes on darwin, 108 elsewhere - and a\n # longer one is refused rather than cut, since a cut path is another\n # path.\n def self.make_sockaddr_un(path : String) : Pointer(UInt8)\n # Whether a small write goes at once (TCP_NODELAY) rather than waiting\n # for an earlier one's acknowledgement, as Nagle's algorithm (RFC 896)\n # has it wait. A connected or accepted TCP socket here sends at once, as\n # Go's net package sets its own; `nodelay = false` turns Nagle back on\n # for a program that writes in pieces it would rather have joined. With\n # Nagle on, a write behind an unacknowledged one waited out the peer's\n # delayed ACK, 40 ms on Linux: measured on loopback, --release, medians\n # of 200, a GET on a new https:// connection took 82.89 ms between\n # std/http's ends and 42.00 to OpenSSL's s_server, and takes 1.07 and\n # 1.10 now; wrk against a server answering a streamed body went from\n # 1,211 to 10,702 requests a second. A unix socket has no Nagle to turn\n # off: it answers false and is left alone.\n def nodelay=(on : ::Bool) : ::Bool\n # Whether TCP_NODELAY is on, as the socket's option reads.\n def nodelay? : ::Bool\n # Dotted-decimal IPv4 only: four octets of 0 to 255, no leading zero\n # (`\"010.1.1.1\"` is refused; glibc's `inet_aton` reads `010` as octal 8,\n # CVE-2021-29921, so it must not quietly be 10 here), plus `localhost`.\n # The refusal inspects the host so a stray space shows.\n def self.parse_ip(host : String) : IPv4Address\n # Eight groups of up to four hex digits, `::` once for a run of zero\n # groups, and a dotted IPv4 tail for the last two (`::ffff:1.2.3.4`). A\n # zone (`fe80::1%eth0`) is refused by name: it names an interface, and\n # this parser has no way to ask the platform which.\n def self.parse_ipv6(host : String) : IPv6Address\n def read(buffer : Pointer(UInt8), count : Int32) : Int32 | Cancelled | SocketError\n def read(max_bytes : Int32 = 4096) : String | Cancelled | SocketError\n # The dotted-decimal reading alone, with the refusal's sentence as the\n # answer instead of a panic: the resolver reads the addresses a hosts\n # file and a resolv.conf write, and a line it cannot read is skipped\n # there, not the end of the program.\n def self.read_ipv4(host : String) : IPv4Address | String\n # The reading alone, the refusal's sentence as the answer: see\n # `read_ipv4`.\n def self.read_ipv6(host : String) : IPv6Address | String\n # The bounds on a read's and a write's wait, in milliseconds; nil waits\n # as long as it takes.\n def read_timeout_ms : Int32 | ::Nil\n def read_timeout_ms=(ms : Int32 | ::Nil) : Int32 | ::Nil\n def recv(buffer : Pointer(UInt8), count : Int32) : Int32 | Cancelled | SocketError\n def recv(max_bytes : Int32 = 4096) : String | Cancelled | SocketError\n # The other end's address and port, as `local_address` and\n # `local_port` give this end's: what a server logs a client by. A unix\n # socket's peer has a path at most, often none (\"\"), and no port. Nil\n # when the other end is gone - the connection was reset or never made -\n # which is the peer's doing and no fault of the program's.\n def remote_address : String | ::Nil\n def remote_port : Int32 | ::Nil\n # Every address *host* stands for, by the machine's resolver, in the\n # resolver's order (see `Resolver#resolve`); `connect` tries them in\n # `attempt_order`'s.\n def self.resolve(host : String) : Array(IPAddress) | DnsError | Cancelled\n def send(buffer : Pointer(UInt8), count : Int32) : Int32 | Cancelled | SocketError\n def send(contents : String) : Int32 | Cancelled | SocketError\n # A host and port as the address a call takes, and its family: an IPv6\n # literal bare or in brackets, anything else IPv4.\n def self.sockaddr(host : String, port : Int32) : Tuple(Pointer(UInt8), Int32)\n # This socket as a stream (`SocketIO`), the same one every time, so\n # what one read took ahead the next reads. It was a stream over the\n # descriptor that read a socket with nothing yet to read as failed.\n def to_io : Std::Socket::SocketIO\n def to_unsafe : Int32\n # Whether this is a unix socket, which has a path where TCP has a port:\n # `remote_port` and `local_port` refuse it. std/tls asks before it keys\n # a session's tickets by the port they came from.\n def unix? : ::Bool\n def write(buffer : Pointer(UInt8), count : Int32) : Int32 | Cancelled | SocketError\n def write(contents : String) : Int32 | Cancelled | SocketError\n def write_timeout_ms : Int32 | ::Nil\n def write_timeout_ms=(ms : Int32 | ::Nil) : Int32 | ::Nil\nend\n\npub alias ResolvConf = Std::Dns::ResolvConf\n\n# A stub resolver: a host name's addresses, from a hosts file and then\n# from name servers, as the C library's resolver finds them - but iyi's,\n# so asking costs no library (see the header of this file).\n#\n# `Resolver.system` is the machine's: `/etc/resolv.conf` and `/etc/hosts`\n# on Linux and darwin, the registry and\n# `%SystemRoot%\\System32\\drivers\\etc\\hosts` on Windows (see\n# `__sys_dns_config` for what the registry can and cannot say). A\n# resolver built with `new` or `from_config` asks exactly what it was\n# given, which is how a test asks a server it runs itself.\n#\n# `resolve` answers every address for a name, IPv4 first and then IPv6,\n# each family in the order the hosts file or the server gave it;\n# `connect` races them in `IyiSocket.attempt_order`'s order:\n#\n# 1. An address, or `localhost` (127.0.0.1 and ::1: RFC 6761 6.3 keeps it\n# off the network), is itself. Anything that is not a host name is\n# `invalid_name?`.\n# 2. The hosts file: every line naming the host, in any case. A name\n# found there is not asked about.\n# 3. The names the search list and `ndots` make of it (`DnsName.candidates`),\n# in order. Each is asked for A and then AAAA records; NXDOMAIN or no\n# records moves on to the next, and any other failure ends the lookup\n# with that failure - a server that timed out on `db.corp.example` must\n# not turn the answer into `db.example`'s (musl's rule; glibc moves on).\n#\n# A and AAAA are asked one after the other, and `connect` starts its race\n# once both have answered. RFC 8305 section 3 asks both at once and\n# starts connecting on the A answer when AAAA is 50 ms behind it; that\n# makes a lookup answer in pieces, and saves the AAAA query's round trip\n# at most - 5 to 65 ms uncached for five names through systemd-resolved\n# on the machine this was written on, 0 to 1 ms cached (dig's query\n# times) - where the race saves what an address that drops SYNs costs,\n# the whole timeout. Asked in turn, an NXDOMAIN for A spares the AAAA\n# query, as it does for most of a search list's candidates.\n#\n# A query is one UDP datagram from a fresh socket (so a fresh source\n# port), with an ID from `Random::Secure`, to each server in turn, each\n# waited for `timeout_ms`, and the round repeated `attempts` times. A\n# datagram that does not carry the ID and the question is ignored and the\n# wait goes on; one from any other address never arrives, because the\n# socket is connected to the server. An answer with TC set is asked again\n# over TCP. SERVFAIL, REFUSED and a server that does not answer move on to\n# the next server; NXDOMAIN is the answer. An answer that cannot be read\n# is ignored like a forgery, and is the failure reported if nothing\n# better arrives before the timeout.\n#\n# Encrypted:\n# `encrypted(upstreams)` is the same resolver - hosts file, search list,\n# `ndots`, timeout and attempts - asking `DnsUpstream`s instead of its\n# plaintext servers: DNS over TLS or over HTTPS, std/secure_dns's\n# `DnsOverTls` and `DnsOverHttps`. Each upstream is asked in turn, the\n# round repeated `attempts` times, as the servers are; A and AAAA go in\n# one exchange. An answer over one is held to more than a datagram is:\n# the ID and the question as ever, and no TC and no byte past the message,\n# since a stream has room for any answer and its frame is the message.\n# The plaintext servers are not asked at all, not even when every\n# upstream is down, unless the caller passes *plaintext_fallback*: RFC\n# 8310 5's strict profile, where a lookup that cannot be private fails,\n# is the default, because falling back hands the very question the\n# program meant to keep private to anyone on the path, and a program\n# that would rather have an answer says so. With *plaintext_fallback*\n# (the opportunistic profile) the plaintext servers are asked only when\n# no upstream could be: one that answered, SERVFAIL included, has spoken.\n# One task at a time per resolver with upstreams: an upstream holds its\n# connection, as an `HTTPClient` does.\npub class Resolver\n protected def ask_upstreams_instead(upstreams : Array(DnsUpstream), plaintext_fallback : Bool) : Nil\n def attempts : Int32\n # *records* by ascending priority, those of one priority shuffled.\n def self.by_priority(records : Array(SvcbRecord)) : Array(SvcbRecord)\n # Every upstream's connections closed.\n def close : Nil\n # This resolver asking *upstreams* instead of its plaintext servers,\n # which are asked only with *plaintext_fallback*, and then only when no\n # upstream could be (see the class).\n def encrypted(upstreams : Array(DnsUpstream), plaintext_fallback : Bool = false) : Resolver\n def encrypted(upstream : DnsUpstream, plaintext_fallback : Bool = false) : Resolver\n # The machine's resolver - its hosts file and search list - asking\n # *upstreams*; its name servers only with *plaintext_fallback*.\n def self.encrypted(upstreams : Array(DnsUpstream), plaintext_fallback : Bool = false) : Resolver\n def self.encrypted(upstream : DnsUpstream, plaintext_fallback : Bool = false) : Resolver\n # A resolver from a parsed resolv.conf and a hosts file's text. A\n # `nameserver` line this resolver cannot use - an IPv6 address with a\n # zone, a typo - is skipped as glibc skips one; with none left, the\n # servers a machine without a configuration is assumed to run.\n def self.from_config(config : ResolvConf, hosts : String = \"\") : Resolver\n # A resolver from a resolv.conf and a hosts file at these paths. A file\n # that is not there reads as empty, as it does to the C library.\n def self.from_files(resolv_conf : String, hosts : String) : Resolver\n # The hosts file's text, read before any server is asked.\n def hosts : String\n # The HTTPS records (RFC 9460 section 9) of the origin `https://host:port`,\n # by SVCB resolution (section 3): the question is *host* for port 443\n # and `_port._https.host` for any other (section 9.1); an AliasMode\n # answer is followed to its target's HTTPS records, at most\n # `SvcbAnswer::MAX_ALIASES` of them, and CNAMEs on the way as `txt`\n # follows them; and the ServiceMode records found are answered by\n # priority, those of one priority in a random order (section 2.4.1).\n # The name is asked as written, as for `txt`, over the upstreams when\n # the resolver has them.\n #\n # Of an AliasMode set one record is followed, at random where there are\n # several (section 2.4.2), and any ServiceMode record beside it is\n # passed over (section 2.4.1). A target that has no HTTPS records ends\n # resolution there, with none: the answer's `name` is the target, which\n # section 3 still has a client connect to. An answer with a record\n # `SvcbRecord.parse` refuses is the whole set refused, `malformed?`, as\n # section 2.2 has it; so is a chain longer than the bound. NXDOMAIN and\n # NODATA for the origin's own name are `not_found?` and `no_address?`,\n # as they are for an address.\n def https_records(host : String, port : Int32 = 443) : SvcbAnswer | DnsError | Cancelled\n # *nameservers* are addresses, each with an optional port:\n # `\"192.0.2.1\"`, `\"192.0.2.1:5353\"`, `\"2001:db8::1\"`,\n # `\"[2001:db8::1]:5353\"`. One that is not is refused here, by name: it is\n # the program's own mistake. None at all is a resolver that reads only\n # *hosts*.\n def initialize(nameservers : Array(String), search : Array(String) = [] of String, ndots : Int32 = 1, timeout_ms : Int32 = 5000, attempts : Int32 = 2, hosts : String = \"\")\n # What NODATA for *type* says the name lacks: its records, by name, for\n # the types asked other than as addresses.\n def self.missing(type : Int32) : String\n def nameservers : Array(String)\n def ndots : Int32\n # The monotonic clock in milliseconds: the runtime's, which its sleep\n # queue keeps deadlines by. A target without the runtime has no socket\n # to wait on either (`__sys_dgram_socket` refuses there), so nothing\n # there reads a time.\n def self.now_ms : Int64\n # The answer to one question over plaintext: each server in turn, the\n # round repeated `attempts` times, until one answers NOERROR or\n # NXDOMAIN. The failure answered otherwise is the last server's.\n protected def plaintext(name : String, type : Int32) : Tuple(DnsMessage, String) | DnsError | Cancelled\n # Whether `nameservers` are asked when no upstream could be (RFC 8310's\n # opportunistic profile); false, the strict one, by default.\n def plaintext_fallback : Bool\n def self.rcode_name(rcode : Int32) : String\n # What an answer's RCODE makes of it: the message for NOERROR, and the\n # failure it says otherwise.\n def self.read_rcode(message : DnsMessage, name : String, label : String) : Tuple(DnsMessage, String) | DnsError\n # An answer that came over a stream, read: see `ask_upstream`.\n def self.read_streamed(bytes : Bytes, id : UInt16, name : String, type : Int32, label : String) : Tuple(DnsMessage, String) | DnsError\n # Every address *host* stands for: see the class's comment for the\n # order things are asked in.\n def resolve(host : String) : Array(IPAddress) | DnsError | Cancelled\n def search : Array(String)\n # A name server written as an address with an optional port, or the\n # sentence saying why it is not one.\n def self.server(text : String) : NameServer | String\n # The machine's resolver, read afresh each time: a configuration the\n # network manager rewrote is the one the next lookup uses.\n def self.system : Resolver\n def timeout_ms : Int32\n # The TXT records of *name*, each one's character-strings joined\n # (`DnsRecord.character_strings`), in the order the answer gave them,\n # CNAMEs followed as `resolve` follows them. The name is asked as\n # written - no hosts file, no search list - since a TXT name\n # (`_acme-challenge.example.com`, a domain's SPF) is written in full.\n def txt(name : String) : Array(String) | DnsError | Cancelled\n # The encrypted upstreams asked instead of `nameservers`; none for a\n # plaintext resolver.\n def upstreams : Array(DnsUpstream)\nend\n\n# A connection's own failure: which operation, and the platform's error\n# number for why - or, for a host name that could not be turned into an\n# address, the resolver's `DnsError`, and no number (`code` 0); or, for a\n# protocol layered on the connection (TLS), that protocol's own sentence,\n# and no number either; or, for a name whose addresses were all tried and\n# none connected, every one of them with how its attempt ended.\npub struct SocketError\n def code : Int32\n # The peer is gone: it reset the connection, or closed it and the write\n # found the pipe broken, or the connection was aborted.\n def disconnected? : ::Bool\n # Why the host's name did not resolve; nil for every other failure.\n def dns_error : DnsError | ::Nil\n # A connect to a name none of whose addresses took the connection:\n # *code* is what `refused?` and `timed_out?` read, and *tried* the\n # sentence naming every address.\n def self.every_address(operation : String, code : Int32, tried : String) : SocketError\n def initialize(operation : String, code : Int32, dns_error : DnsError | ::Nil, layer_reason : String | ::Nil, tried : String | ::Nil)\n def initialize(operation : String, code : Int32, dns_error : DnsError | ::Nil, layer_reason : String | ::Nil)\n def initialize(operation : String, code : Int32, dns_error : DnsError | ::Nil)\n def initialize(operation : String, code : Int32)\n # What the protocol over the connection said went wrong; nil otherwise.\n def layer_reason : String | ::Nil\n # A failure of the protocol over the connection, in its words: `cannot\n # read from example.com: tls: a record that does not decrypt`.\n def self.layered(operation : String, reason : String) : SocketError\n def operation : String\n def reason : String\n def refused? : ::Bool\n # A wait for the peer that ran past its socket's timeout, carrying the\n # platform's own number for it so `reason` and `timed_out?` read it as\n # they read the kernel's.\n def self.timed_out(operation : String) : SocketError\n # The peer did not answer in time: a connect the network timed out, or a\n # read or write past its socket's timeout.\n def timed_out? : ::Bool\n # For a connect to a name that stood for more than one address: each\n # address in the order it was tried and how its attempt ended -\n # `[2001:db8::1]:443 timed out; 192.0.2.1:443 connection refused` - which\n # `reason` answers. Nil for every other failure.\n def tried : String | ::Nil\n def unreachable? : ::Bool\n # The host was a name, and no address came of it: `dns_error` says why\n # (`not_found?` for NXDOMAIN, `server_failure?` for SERVFAIL, ...).\n def unresolved? : ::Bool\nend\n\n# A socket as a stream, for what is written against streams: `read_line`,\n# `puts`, `IyiIO.copy` between it and a file or a pipe. Its reads and\n# writes are the socket's own, which park the task and keep its\n# timeouts. A stream has no error to answer, so a read the socket refuses\n# ends the stream, `read_failed?` true, and a write it refuses panics,\n# naming the reason; a program that answers those reads the socket.\n# Writes are buffered: `flush`, or `IyiIO.copy`, which flushes, sends\n# them. Closing it closes the socket.\npub class SocketIO < ::IyiIO\n def initialize(socket : IyiSocket, buffer_size : Int32 = 4096)\n def socket : IyiSocket\nend\n\nimpl Error for SocketError\n def message : String\nend\n"
},
{
@@ -728,7 +728,7 @@
},
{
"path": "src/iyi/concurrency.iyi",
- "sha256": "0a2057fcd85011a1bdbd098176792092b1e5a5a4e0e9ddee6d23670e4fbf5214"
+ "sha256": "197f8f27f21e424be85d1f8b5d607659771202d51712f5304f7012125d44abd8"
},
{
"path": "src/iyi/enum.iyi",
@@ -764,7 +764,7 @@
},
{
"path": "src/iyi/prelude.iyi",
- "sha256": "92596c2c55ebd108cc41a422f58d9ebd17be67f28639c8ad67aeae7482a99d14"
+ "sha256": "2c724ca28f93ee1260c649727ed33f201960b04ca64108a0e3fb07d7a52f051e"
},
{
"path": "src/iyi/primitives.iyi",
diff --git a/records/wasm/arrays.wasm b/records/wasm/arrays.wasm
index 4349456..d529bc6 100755
Binary files a/records/wasm/arrays.wasm and b/records/wasm/arrays.wasm differ
diff --git a/records/wasm/blocks.wasm b/records/wasm/blocks.wasm
index e119f0a..4eb056f 100755
Binary files a/records/wasm/blocks.wasm and b/records/wasm/blocks.wasm differ
diff --git a/records/wasm/bounds.wasm b/records/wasm/bounds.wasm
index 0d6178a..810f8fd 100755
Binary files a/records/wasm/bounds.wasm and b/records/wasm/bounds.wasm differ
diff --git a/records/wasm/case.wasm b/records/wasm/case.wasm
index 2d2c723..bd2b5c1 100755
Binary files a/records/wasm/case.wasm and b/records/wasm/case.wasm differ
diff --git a/records/wasm/classes.wasm b/records/wasm/classes.wasm
index cababa4..78cf67e 100755
Binary files a/records/wasm/classes.wasm and b/records/wasm/classes.wasm differ
diff --git a/records/wasm/conditions.wasm b/records/wasm/conditions.wasm
index 8b23f3e..5e08ae1 100755
Binary files a/records/wasm/conditions.wasm and b/records/wasm/conditions.wasm differ
diff --git a/records/wasm/constants.wasm b/records/wasm/constants.wasm
index c9b831f..f48e032 100755
Binary files a/records/wasm/constants.wasm and b/records/wasm/constants.wasm differ
diff --git a/records/wasm/conversions.wasm b/records/wasm/conversions.wasm
index d25ef42..c87bdb5 100755
Binary files a/records/wasm/conversions.wasm and b/records/wasm/conversions.wasm differ
diff --git a/records/wasm/defaults.wasm b/records/wasm/defaults.wasm
index 2f65f7d..52b1fc9 100755
Binary files a/records/wasm/defaults.wasm and b/records/wasm/defaults.wasm differ
diff --git a/records/wasm/defer.wasm b/records/wasm/defer.wasm
index 82770bb..d0f65b5 100755
Binary files a/records/wasm/defer.wasm and b/records/wasm/defer.wasm differ
diff --git a/records/wasm/dispatch.wasm b/records/wasm/dispatch.wasm
index 9102983..e94a843 100755
Binary files a/records/wasm/dispatch.wasm and b/records/wasm/dispatch.wasm differ
diff --git a/records/wasm/errors.wasm b/records/wasm/errors.wasm
index 93cda47..d029c38 100755
Binary files a/records/wasm/errors.wasm and b/records/wasm/errors.wasm differ
diff --git a/records/wasm/exports.wasm b/records/wasm/exports.wasm
index b0bbb15..78117be 100755
Binary files a/records/wasm/exports.wasm and b/records/wasm/exports.wasm differ
diff --git a/records/wasm/functions.wasm b/records/wasm/functions.wasm
index 2f99c47..a7b4ab3 100755
Binary files a/records/wasm/functions.wasm and b/records/wasm/functions.wasm differ
diff --git a/records/wasm/generic-functions.wasm b/records/wasm/generic-functions.wasm
index 9941ac5..167e197 100755
Binary files a/records/wasm/generic-functions.wasm and b/records/wasm/generic-functions.wasm differ
diff --git a/records/wasm/generic-types.wasm b/records/wasm/generic-types.wasm
index 21b0886..84e766f 100755
Binary files a/records/wasm/generic-types.wasm and b/records/wasm/generic-types.wasm differ
diff --git a/records/wasm/hashes.wasm b/records/wasm/hashes.wasm
index c1c1da5..22835da 100755
Binary files a/records/wasm/hashes.wasm and b/records/wasm/hashes.wasm differ
diff --git a/records/wasm/hello.wasm b/records/wasm/hello.wasm
index 774e2f3..54cd580 100755
Binary files a/records/wasm/hello.wasm and b/records/wasm/hello.wasm differ
diff --git a/records/wasm/iyi-basics.wasm b/records/wasm/iyi-basics.wasm
index 05b3b4f..173c8df 100755
Binary files a/records/wasm/iyi-basics.wasm and b/records/wasm/iyi-basics.wasm differ
diff --git a/records/wasm/iyi-calc.wasm b/records/wasm/iyi-calc.wasm
index cc49223..7751043 100755
Binary files a/records/wasm/iyi-calc.wasm and b/records/wasm/iyi-calc.wasm differ
diff --git a/records/wasm/iyi-collections.wasm b/records/wasm/iyi-collections.wasm
index 9c4b282..b3abadb 100755
Binary files a/records/wasm/iyi-collections.wasm and b/records/wasm/iyi-collections.wasm differ
diff --git a/records/wasm/iyi-config.wasm b/records/wasm/iyi-config.wasm
index 953769c..e72eb5d 100755
Binary files a/records/wasm/iyi-config.wasm and b/records/wasm/iyi-config.wasm differ
diff --git a/records/wasm/iyi-derive.wasm b/records/wasm/iyi-derive.wasm
index d3ee91b..68d67e2 100755
Binary files a/records/wasm/iyi-derive.wasm and b/records/wasm/iyi-derive.wasm differ
diff --git a/records/wasm/iyi-enums.wasm b/records/wasm/iyi-enums.wasm
index 01d1844..a0b018e 100755
Binary files a/records/wasm/iyi-enums.wasm and b/records/wasm/iyi-enums.wasm differ
diff --git a/records/wasm/iyi-errors.wasm b/records/wasm/iyi-errors.wasm
index 659d176..5dba5cb 100755
Binary files a/records/wasm/iyi-errors.wasm and b/records/wasm/iyi-errors.wasm differ
diff --git a/records/wasm/iyi-files.wasm b/records/wasm/iyi-files.wasm
index 424402a..fe94aca 100755
Binary files a/records/wasm/iyi-files.wasm and b/records/wasm/iyi-files.wasm differ
diff --git a/records/wasm/iyi-format.wasm b/records/wasm/iyi-format.wasm
index 305b60a..200f399 100755
Binary files a/records/wasm/iyi-format.wasm and b/records/wasm/iyi-format.wasm differ
diff --git a/records/wasm/iyi-formatting.wasm b/records/wasm/iyi-formatting.wasm
index 6fe2a74..05cb252 100755
Binary files a/records/wasm/iyi-formatting.wasm and b/records/wasm/iyi-formatting.wasm differ
diff --git a/records/wasm/iyi-generics.wasm b/records/wasm/iyi-generics.wasm
index 24e0744..9eb08cd 100755
Binary files a/records/wasm/iyi-generics.wasm and b/records/wasm/iyi-generics.wasm differ
diff --git a/records/wasm/iyi-grid.wasm b/records/wasm/iyi-grid.wasm
index b0670e1..3de0a78 100755
Binary files a/records/wasm/iyi-grid.wasm and b/records/wasm/iyi-grid.wasm differ
diff --git a/records/wasm/iyi-hello.wasm b/records/wasm/iyi-hello.wasm
index 7785a65..ea05c6e 100755
Binary files a/records/wasm/iyi-hello.wasm and b/records/wasm/iyi-hello.wasm differ
diff --git a/records/wasm/iyi-immutable.wasm b/records/wasm/iyi-immutable.wasm
index 270b0d8..bd09e5f 100755
Binary files a/records/wasm/iyi-immutable.wasm and b/records/wasm/iyi-immutable.wasm differ
diff --git a/records/wasm/iyi-init_order.wasm b/records/wasm/iyi-init_order.wasm
index 187607c..7bdc2de 100755
Binary files a/records/wasm/iyi-init_order.wasm and b/records/wasm/iyi-init_order.wasm differ
diff --git a/records/wasm/iyi-inventory.wasm b/records/wasm/iyi-inventory.wasm
index 1c87b21..dbef64e 100755
Binary files a/records/wasm/iyi-inventory.wasm and b/records/wasm/iyi-inventory.wasm differ
diff --git a/records/wasm/iyi-io.wasm b/records/wasm/iyi-io.wasm
index ee26d9f..9cae9e8 100755
Binary files a/records/wasm/iyi-io.wasm and b/records/wasm/iyi-io.wasm differ
diff --git a/records/wasm/iyi-modules.wasm b/records/wasm/iyi-modules.wasm
index 8097637..88253a2 100755
Binary files a/records/wasm/iyi-modules.wasm and b/records/wasm/iyi-modules.wasm differ
diff --git a/records/wasm/iyi-sessions.wasm b/records/wasm/iyi-sessions.wasm
index ef25f00..11f43ab 100755
Binary files a/records/wasm/iyi-sessions.wasm and b/records/wasm/iyi-sessions.wasm differ
diff --git a/records/wasm/iyi-shapes.wasm b/records/wasm/iyi-shapes.wasm
index a2b20c5..aeb71e4 100755
Binary files a/records/wasm/iyi-shapes.wasm and b/records/wasm/iyi-shapes.wasm differ
diff --git a/records/wasm/iyi-socket.wasm b/records/wasm/iyi-socket.wasm
index 15e5564..38d9b99 100755
Binary files a/records/wasm/iyi-socket.wasm and b/records/wasm/iyi-socket.wasm differ
diff --git a/records/wasm/iyi-std_iterator.wasm b/records/wasm/iyi-std_iterator.wasm
index 14a1765..0c74f5c 100755
Binary files a/records/wasm/iyi-std_iterator.wasm and b/records/wasm/iyi-std_iterator.wasm differ
diff --git a/records/wasm/iyi-std_text.wasm b/records/wasm/iyi-std_text.wasm
index 173bafb..358b441 100755
Binary files a/records/wasm/iyi-std_text.wasm and b/records/wasm/iyi-std_text.wasm differ
diff --git a/records/wasm/iyi-std_time.wasm b/records/wasm/iyi-std_time.wasm
index 45b2667..2d03d13 100755
Binary files a/records/wasm/iyi-std_time.wasm and b/records/wasm/iyi-std_time.wasm differ
diff --git a/records/wasm/iyi-visited.wasm b/records/wasm/iyi-visited.wasm
index 1497659..6841e37 100755
Binary files a/records/wasm/iyi-visited.wasm and b/records/wasm/iyi-visited.wasm differ
diff --git a/records/wasm/iyi-webapp.wasm b/records/wasm/iyi-webapp.wasm
index d1b1f8e..b22269f 100755
Binary files a/records/wasm/iyi-webapp.wasm and b/records/wasm/iyi-webapp.wasm differ
diff --git a/records/wasm/loops.wasm b/records/wasm/loops.wasm
index 9cb86d0..f1bddb6 100755
Binary files a/records/wasm/loops.wasm and b/records/wasm/loops.wasm differ
diff --git a/records/wasm/manifest.json b/records/wasm/manifest.json
index cb43131..a7a42c4 100644
--- a/records/wasm/manifest.json
+++ b/records/wasm/manifest.json
@@ -1,10 +1,10 @@
{
"recorded": {
- "compiler": "iyi 0.20.0 (built on Crystal 1.22.0-dev [224ed74fc] (2026-11-02))",
- "commit": "224ed74fc7ba9d78345dfd10f21ece4b499702fa",
+ "compiler": "iyi 0.20.1 (built on Crystal 1.22.0-dev [91d012b1e] (2026-11-04))",
+ "commit": "91d012b1ed978c7df5cc033624ec161a2daf01b6",
"machine": "Linux 7.0.0-38-generic x86_64, QEMU Virtual CPU version 2.5+",
"command": "npm run record:wasm",
- "when": "2026-11-02T07:18:22.704Z"
+ "when": "2026-11-04T04:32:07.924Z"
},
"samples": [
{
@@ -14,7 +14,7 @@
"sourceSha256": "676f8372f2cd5f8941b1dcd0ccb167ee6f6f27c1de4a77cef3af0372c323134d",
"wasm": "hello.wasm",
"bytes": 123087,
- "sha256": "b2f99b569507b06c72ac45d9afb48ddf6342bd7fcb333a857392aa6318f5c65b",
+ "sha256": "38d89c17020feacd80582cc0d9b154fa22bd2568d5683b5e4a6294d45bd0ad19",
"exitCode": 0,
"nativeStdout": "Hello Türkiye!\n",
"wasmStdout": "Hello Türkiye!\n",
@@ -28,7 +28,7 @@
"sourceSha256": "c908a80d8c5a2c3f42bc6c90cd9d9a822a4c0b077cbd4713cc627f2d9f4912c0",
"wasm": "modules.wasm",
"bytes": 124842,
- "sha256": "3148c581ed9ffba3999fec179835b1583eeb72ad033dd7ef3c5d7bd3fb110eb3",
+ "sha256": "421a51e54316bf4e162322ab988e25d4e8a18501fcabaf74b2e9a11f61e90e85",
"exitCode": 0,
"nativeStdout": "7\n",
"wasmStdout": "7\n",
@@ -42,7 +42,7 @@
"sourceSha256": "1d341c749781b957ea8e5729a226f84852f816c914224de63771b5a03bfb6153",
"wasm": "exports.wasm",
"bytes": 124838,
- "sha256": "c81410a6f19ab5fd8abdb5888a2eebb027f205f501ac9e1acc1a0211dbc0e612",
+ "sha256": "165e1dc29c8306b68dc489f1be1c4e7cb49d94e2a0dfe37701a60b4ed42a9815",
"exitCode": 0,
"nativeStdout": "7\n",
"wasmStdout": "7\n",
@@ -56,7 +56,7 @@
"sourceSha256": "672aec63ec81d0a8dc9cc773688d853a6b9a246618705a4cfa782191efe3fea5",
"wasm": "variables.wasm",
"bytes": 156061,
- "sha256": "e923e39c408a3c153216eff5469350c49d73ac7dd3e6074a739f4355d296e989",
+ "sha256": "b0a5a9e9e814b7ba37bd613d9c0a7e266a17ebe0a4754bdb4a06ad638dfe78af",
"exitCode": 0,
"nativeStdout": "iyi\n2026\n0.75\ntrue\n2027\n10\n",
"wasmStdout": "iyi\n2026\n0.75\ntrue\n2027\n10\n",
@@ -70,7 +70,7 @@
"sourceSha256": "bbd35199bdfae3aa05673d869e512e1713213fa1aeef929e6477fec7ff6e4464",
"wasm": "types.wasm",
"bytes": 158793,
- "sha256": "02ce0e08a24e6218fc6820eff9835b0fefdf204fa54e330889fdc94c5185045a",
+ "sha256": "f8e6e6e6ffb943a0b51f6f847d1f74d0cd4cfa05d0b285bcb1eb9ee0c65b74c9",
"exitCode": 0,
"nativeStdout": "Int32\nInt64\nFloat64\nBool\nChar\nString\n3\n3.5\n1\n1024\n",
"wasmStdout": "Int32\nInt64\nFloat64\nBool\nChar\nString\n3\n3.5\n1\n1024\n",
@@ -84,7 +84,7 @@
"sourceSha256": "1b8cf5e80ec2dbe3ce9b207d12491e7b24761caf1eb2797a098f68f36e933b81",
"wasm": "conversions.wasm",
"bytes": 187416,
- "sha256": "2c054e3c89a1a116d5885db0d3e6f8da9d20e82bba44d23469f58802c84580b8",
+ "sha256": "ce2964d3ac0e39d59624288868c4e11456da234312570b70a02dd816addca683",
"exitCode": 0,
"nativeStdout": "42.0\n42!\n9\n10.0\n18\n17\n\n65\nB\n",
"wasmStdout": "42.0\n42!\n9\n10.0\n18\n17\n\n65\nB\n",
@@ -98,7 +98,7 @@
"sourceSha256": "e5e15868dc555999be7b1c15e2349147be0e335a5b45792b893aa9c9d3e139ba",
"wasm": "constants.wasm",
"bytes": 160443,
- "sha256": "899cc10df244def0c0b41d12d716abcc61c467b2351c736900923f81ec6784d9",
+ "sha256": "7a31ad7c728a18fb7ad4629d223eb6b3f1040b55ed1515d909e6058ee8e82435",
"exitCode": 0,
"nativeStdout": "12.57\nMerhaba, dünya\ntrue\n",
"wasmStdout": "12.57\nMerhaba, dünya\ntrue\n",
@@ -112,7 +112,7 @@
"sourceSha256": "1261f0c7c47c4f9469358362a6c52a84cd6450a6e4347e1d7ff9eba09a88f89a",
"wasm": "functions.wasm",
"bytes": 125184,
- "sha256": "0d6e13b48be440c0b4158538681d8319bfd05bd8c9f843a6be119703e26b4470",
+ "sha256": "a6e3f66932f7ed2580d456041b5553d336b87a0472847092f7dce07d66f592ca",
"exitCode": 0,
"nativeStdout": "5\nnegative\nHello, Ayşe!\nSelam, Ayşe!\n",
"wasmStdout": "5\nnegative\nHello, Ayşe!\nSelam, Ayşe!\n",
@@ -126,7 +126,7 @@
"sourceSha256": "6b655fcd036d722550a028424fe4719cdfb89383fed7607e260c46fbf0fff932",
"wasm": "returns.wasm",
"bytes": 127402,
- "sha256": "a562301ac6ac68f527583c80c033aa83dc650e65fb4c2d8eaa80ef74cc359226",
+ "sha256": "6f4b35be779b8d64e92c5d6b95fa959eccd30a9d9574f5d592e16e9cc2132dca",
"exitCode": 0,
"nativeStdout": "3\n2\n2\n1\n6\n\n",
"wasmStdout": "3\n2\n2\n1\n6\n\n",
@@ -140,7 +140,7 @@
"sourceSha256": "a559c045cfef442a9927954673d8ad5fc61f610a0908950bcdd79182853a4e04",
"wasm": "nil.wasm",
"bytes": 125476,
- "sha256": "b910ce39a42b38d673f9888e725efd5c4fe263916392802f487c9a8702e54b6a",
+ "sha256": "68356136db87b27634e5c328e02d0386f3e2dd777ce9b463ec1428ab06c25a59",
"exitCode": 0,
"nativeStdout": "Ada\n\nADA\nnobody\n",
"wasmStdout": "Ada\n\nADA\nnobody\n",
@@ -154,7 +154,7 @@
"sourceSha256": "18f56f89ce4e1bcf3f1c2abff8e3539b5bde85135ec978931583dddd7882992c",
"wasm": "loops.wasm",
"bytes": 126424,
- "sha256": "70b84276aadfc16114677bbf4beba2def27106595e68c4c125f60256869979bf",
+ "sha256": "e0a0758c488943d7bfe214a973aae9a620649dea031ad74262d68c92865f5173",
"exitCode": 0,
"nativeStdout": "1\n2\n3\nround 0\nround 1\nround 2\n64\ncount 1\ncount 3\ncount 4\n",
"wasmStdout": "1\n2\n3\nround 0\nround 1\nround 2\n64\ncount 1\ncount 3\ncount 4\n",
@@ -168,7 +168,7 @@
"sourceSha256": "418036dc21744938451d1210eeceaca1931d0edb5329f3f8e801f626fa234fd6",
"wasm": "ranges.wasm",
"bytes": 141760,
- "sha256": "4765a3a9662c3d9d9bf6ffe04e86ec95481c9da2792ea4cf70e06bdd4886f750",
+ "sha256": "ce1431275be2f17eb1e7127f660eb3f9466b8639cb67eb14d772ec32da62f1b5",
"exitCode": 0,
"nativeStdout": "1\n2\n3\n4\n5\n1\n2\n3\n4\n[1, 4, 9, 16, 25]\n5050\n[3, 6, 9]\nadult\n",
"wasmStdout": "1\n2\n3\n4\n5\n1\n2\n3\n4\n[1, 4, 9, 16, 25]\n5050\n[3, 6, 9]\nadult\n",
@@ -182,7 +182,7 @@
"sourceSha256": "8ed0a951d7e760d2bb95c784b20c3e59771080e0578a2cadf0b1bdf41fd2eed2",
"wasm": "conditions.wasm",
"bytes": 124828,
- "sha256": "0e3ae9ecb6b7748bf36c6736dd7fabe8f95e8f86c67870ca7d14b68695acb3fe",
+ "sha256": "0ff133e1140610da7c74ba371bc7385c1763c466778c10a92607d4a2c14e7807",
"exitCode": 0,
"nativeStdout": "warm\nabove freezing\nnice day\nwarm\nodd\ntrue\nfalse\n",
"wasmStdout": "warm\nabove freezing\nnice day\nwarm\nodd\ntrue\nfalse\n",
@@ -196,7 +196,7 @@
"sourceSha256": "71de25c31bbd4ad83f406525df3f263bbb18c8dcab00eee4dba6baa166409ffb",
"wasm": "case.wasm",
"bytes": 127806,
- "sha256": "33112c07e0f4db76abd152f20e8e9d857b8b80397a64fe77035dc7749dbd0bf8",
+ "sha256": "eafde9ad418f89622e33a02edfb09460b720f0570fe05d83a5629a3de80b503d",
"exitCode": 0,
"nativeStdout": "weekend\nalmost\nweekday\nB\na number: 2\n",
"wasmStdout": "weekend\nalmost\nweekday\nB\na number: 2\n",
@@ -210,7 +210,7 @@
"sourceSha256": "c39edacef8695b8aa0170cd169373a19ece2ec4fcf204439ef48e22aadcf06f2",
"wasm": "defer.wasm",
"bytes": 128138,
- "sha256": "edbb73210e8210678d0e93205a1f4cbb77e54b466b842a07289403329871a2a1",
+ "sha256": "8c66f8e24e1a3fb27ef3b6964f7d15278c7e867b0d84a9fc1817f6e00b1ac523",
"exitCode": 0,
"nativeStdout": "open\nworking\nclose\n42\nfirst\nsecond\nthird\nturn 0\ndone with 0\nturn 1\ndone with 1\n",
"wasmStdout": "open\nworking\nclose\n42\nfirst\nsecond\nthird\nturn 0\ndone with 0\nturn 1\ndone with 1\n",
@@ -224,7 +224,7 @@
"sourceSha256": "2354525f27753b7a4bdef384dc4c0c6507c12fe845e9d2927e7aec0f62c2814c",
"wasm": "structs.wasm",
"bytes": 125572,
- "sha256": "ab6c2b2e09b57aeac3f2ac3aa4384b8c265054c056ea511f5672e479e7fe46f6",
+ "sha256": "653ee06a14e11ed4ba2853ac12e2cf95512e7fa1c534e25d8a37f9d73846c8c1",
"exitCode": 0,
"nativeStdout": "1\n(4, 6)\n(1, 2)\n",
"wasmStdout": "1\n(4, 6)\n(1, 2)\n",
@@ -238,7 +238,7 @@
"sourceSha256": "cfc6709102991d8da3357f3f6230408b0c79e8bf7199e63f8b40a272c65a2f6a",
"wasm": "classes.wasm",
"bytes": 124398,
- "sha256": "f09ca5018601c91f292d49427f0712b88af0b418a0072d7b6ef67da83d613152",
+ "sha256": "51e10f413ca4fab84bbe5a3e08f03b0eb24d9b5dbc95a2f867f693f411a866f9",
"exitCode": 0,
"nativeStdout": "2\n0\n",
"wasmStdout": "2\n0\n",
@@ -252,7 +252,7 @@
"sourceSha256": "7f4841af80023b70c3e6f897ec7d5ba82001acd91a905e4772cc6f6f0dec0aa9",
"wasm": "arrays.wasm",
"bytes": 145881,
- "sha256": "a936176105b1cf1b823a04753d93cb4cd86cb11a3dd7456c13830461b2a01f5e",
+ "sha256": "b6f59b198213a93a2aafcdd3c786e0a8d0d57c1ecd140163d3fc4ced243d2272",
"exitCode": 0,
"nativeStdout": "5\n3\n3\n5\n[3, 1, 4, 1, 5, 9]\ntrue\n23\n[1, 1, 3, 4, 5, 9]\n[3, 4, 5, 9]\n[30, 10, 40, 10, 50, 90]\n23\nAyşe, Mehmet\n1. Ayşe\n2. Mehmet\n",
"wasmStdout": "5\n3\n3\n5\n[3, 1, 4, 1, 5, 9]\ntrue\n23\n[1, 1, 3, 4, 5, 9]\n[3, 4, 5, 9]\n[30, 10, 40, 10, 50, 90]\n23\nAyşe, Mehmet\n1. Ayşe\n2. Mehmet\n",
@@ -266,7 +266,7 @@
"sourceSha256": "1b5fb76d25d7f5979248800422d871da36d4c2d0b829ec9c311c7b200beb167b",
"wasm": "hashes.wasm",
"bytes": 165179,
- "sha256": "c57e6e01912536496dc25cf385feafc8fba8317663dc78b5f84e12b99cf3da27",
+ "sha256": "edf321c2fe8ad10a22a36bd2d1693c8e3a68023acd16c9c436877b596acb12ee",
"exitCode": 0,
"nativeStdout": "31\n2\n[\"Ayşe\", \"Mehmet\", \"Zeynep\"]\n\nfalse\nAyşe is 31\nMehmet is 27\nZeynep is 40\n{\"a\" => 2, \"b\" => 1}\n",
"wasmStdout": "31\n2\n[\"Ayşe\", \"Mehmet\", \"Zeynep\"]\n\nfalse\nAyşe is 31\nMehmet is 27\nZeynep is 40\n{\"a\" => 2, \"b\" => 1}\n",
@@ -280,7 +280,7 @@
"sourceSha256": "75f2414a08f54a9d8ecc53d7b0621d8c4906a3f39d9a673dd1b8d2f219b4856b",
"wasm": "tuples.wasm",
"bytes": 127759,
- "sha256": "789f6da9fbc0ce1ce352cade9019cb9b17a85cf3d360dc0aca95cbeaf721eb19",
+ "sha256": "6056cbfa88e63e5d611ac94fa18b401da1f24eaffbe01f1a4ddb644d73cf43f3",
"exitCode": 0,
"nativeStdout": "1\none\n1 is one\nAyşe\n31\n2..9\n",
"wasmStdout": "1\none\n1 is one\nAyşe\n31\n2..9\n",
@@ -294,7 +294,7 @@
"sourceSha256": "f2ae3fc23344a9f94f896942b8609956979599387e313e98d49cb4977ba9dd17",
"wasm": "blocks.wasm",
"bytes": 127866,
- "sha256": "b024efb9529caf394a9b41c630abb009f69a35dbc5d019ecbf8f51dc9df306d7",
+ "sha256": "e01d093cb97f30a1facc40fba0d44e3dd6898d0656431943b26f953fc7efa4fd",
"exitCode": 0,
"nativeStdout": "step 0\nstep 1\nstep 2\n6\n49\n2\n105\n",
"wasmStdout": "step 0\nstep 1\nstep 2\n6\n49\n2\n105\n",
@@ -308,7 +308,7 @@
"sourceSha256": "f4fb9b8be821d24f79148d110edb8f6bd36239bab164d7be45bc4d3da29eba7a",
"wasm": "methods.wasm",
"bytes": 161051,
- "sha256": "900fd19677f0e65142dd426d78cfe686ca7c4f60767cc3d9854549ccded70a0e",
+ "sha256": "3d85a2d848d5011a19920b4762269a04aa7bac800a563275d46ff1264cb055bd",
"exitCode": 0,
"nativeStdout": "12.0\nfalse\n48.0\nIYI\n4.0\n6\n",
"wasmStdout": "12.0\nfalse\n48.0\nIYI\n4.0\n6\n",
@@ -322,7 +322,7 @@
"sourceSha256": "98aaeb457da3a65d50c3ce5402f3378e431010d50122396f9a353b4d958fc8a8",
"wasm": "traits.wasm",
"bytes": 160535,
- "sha256": "d414044f7f0e6ff94bc7b5ade6b095c32c968333c5c7e49acd2c8ea710ccd92e",
+ "sha256": "1dccca26c8177bf515393a4db94b2f7fd31507fe822479a1d9770f0315ad9497",
"exitCode": 0,
"nativeStdout": "3.14\n4.0\n",
"wasmStdout": "3.14\n4.0\n",
@@ -336,7 +336,7 @@
"sourceSha256": "e9a5985790ff7c52c42846b394872bb18ac9040abfd59e9453d4ac71747fcfb9",
"wasm": "dispatch.wasm",
"bytes": 136069,
- "sha256": "c619d9328cf1a563769a23b1fa9b75fdcd3e3432c5a78aebbd6590f77a587927",
+ "sha256": "00325eb2fcd4ec3a1af9143c6ff51df4bd1ac9c171a583b5d1b8ccb0ec9ff530",
"exitCode": 0,
"nativeStdout": "hav\nmiyav\nhav miyav\n",
"wasmStdout": "hav\nmiyav\nhav miyav\n",
@@ -350,7 +350,7 @@
"sourceSha256": "7e438eff3837000a1551861b7107a2a4f7f6f78988a283fec3174429a1f20844",
"wasm": "defaults.wasm",
"bytes": 127100,
- "sha256": "c874918891eb024609cbfe95b478018e14ae1f53491ab22d3675e8bf8cfbf01e",
+ "sha256": "11fd75705a649460a5e68bcc738382a79b12cb33783069f7748d6aca563d3513",
"exitCode": 0,
"nativeStdout": "Merhaba, Ada!\nMERHABA, ADA!\nBEEP unit 7\nBEEP UNIT 7\n",
"wasmStdout": "Merhaba, Ada!\nMERHABA, ADA!\nBEEP unit 7\nBEEP UNIT 7\n",
@@ -364,7 +364,7 @@
"sourceSha256": "61cb534c8687c14b7392657f859b165eb063dc892ae64429ccc59dd241441466",
"wasm": "unions.wasm",
"bytes": 135190,
- "sha256": "9b658342abd216814b620939a4d3506adf316add17b205b0d7725ed2c3e5b7e4",
+ "sha256": "1b1d08ebe15d3c2adec4e4efd232cb05f7fa75c3c9cdfd3b6b9362ff973e4627",
"exitCode": 0,
"nativeStdout": "number 24\nproblem, not a number: x\nnumber 14\n6\n",
"wasmStdout": "number 24\nproblem, not a number: x\nnumber 14\n6\n",
@@ -378,7 +378,7 @@
"sourceSha256": "af4c28f720136cd6c8b935273fa769fe234b55716da4abed70196e56d8b9faf7",
"wasm": "errors.wasm",
"bytes": 160371,
- "sha256": "bf4685973716b806c46817d9091b6ec0861e06f247781223e6991ded1e41c979",
+ "sha256": "28f5753b1b2208e441777f7725c79c5edabcf1e2fca60a5bee643785aa926dd8",
"exitCode": 0,
"nativeStdout": "2027\nno such key: missing\ndefault\niyi\n",
"wasmStdout": "2027\nno such key: missing\ndefault\niyi\n",
@@ -392,7 +392,7 @@
"sourceSha256": "704f192ecd578a9847d911563129b559b5b04cc276327f8de24fd1502565f542",
"wasm": "strings.wasm",
"bytes": 160621,
- "sha256": "6af479f7f4dd2febc2a18f5aed837b172b468b80f22e70f01b29d736bf5c54bc",
+ "sha256": "c48fff3f2df3aa6fd46e301aa0944db7dd8b51b5255cfccfcb08cc63125e875d",
"exitCode": 0,
"nativeStdout": "10\nHELLO, IYI\nhello, iyi\ntrue\nHello, iyi!\n[\"Hello,\", \"iyi\"]\n2 and true and [1, 2]\n12,05 TL\ntotal: 12,05 TL\nitem 12\ntotal 99\n",
"wasmStdout": "10\nHELLO, IYI\nhello, iyi\ntrue\nHello, iyi!\n[\"Hello,\", \"iyi\"]\n2 and true and [1, 2]\n12,05 TL\ntotal: 12,05 TL\nitem 12\ntotal 99\n",
@@ -406,7 +406,7 @@
"sourceSha256": "2b0ace59365d9bb35c904102d1131a95f5787113ba585975a819c6a354666e7f",
"wasm": "generic-functions.wasm",
"bytes": 151699,
- "sha256": "46473ac6701e53a583ce021c8b0490161d086e01334c3b26f3088e92c98655be",
+ "sha256": "858cbbd01e1e6f1a5b490b2f4b83c20cbc02649b8c5618eda024760c5f846ca2",
"exitCode": 0,
"nativeStdout": "10\na\n{\"one\", 1}\n101\n",
"wasmStdout": "10\na\n{\"one\", 1}\n101\n",
@@ -420,7 +420,7 @@
"sourceSha256": "fe5f0b5626ef062f7af44196125d3b50d1914788fb1416d980d10e22f872aa8f",
"wasm": "generic-types.wasm",
"bytes": 135597,
- "sha256": "5c3cfeed954628eb61366e149ad8b96558faaa70feb6b7ce63588796936c9bcc",
+ "sha256": "0a11fc1ef4fbbd9c159cee034d0086ee874a84e6a4f19e83ed62e1c95c185f9b",
"exitCode": 0,
"nativeStdout": "2\n2\n1\n\niyi\na stack of 1\na stack of 0\n",
"wasmStdout": "2\n2\n1\n\niyi\na stack of 1\na stack of 0\n",
@@ -434,7 +434,7 @@
"sourceSha256": "cd5324bcc2756eef8d4ebeca406c61ecf12cfd5898b26a01ec8fa4fd1ef64d26",
"wasm": "bounds.wasm",
"bytes": 129541,
- "sha256": "fdc11a3ae994b9bd087d03c63a8b98c391ff7a326cc8124b5d627a553d9211fa",
+ "sha256": "073f84a1a4f1aa838ad40a6df38051c8f30fce51553a3f31a977d2d976457853",
"exitCode": 0,
"nativeStdout": "200\n45\n30\n",
"wasmStdout": "200\n45\n30\n",
@@ -448,7 +448,7 @@
"sourceSha256": "2fab386bd172c15319db0a4e553008d7ae7114fd8cdef72e7a48a850c301c72c",
"wasm": "iyi-basics.wasm",
"bytes": 213443,
- "sha256": "1ceed0e4827fa6b8c8445d7c20558d0b0c304ceb052ecfc4dd3ce48ba6e3e625",
+ "sha256": "024d922379b0d19585337915d3847c2d8ab5b689f63a090f458cc09e8e23ff16",
"exitCode": 0,
"nativeStdout": "55\n1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n3\n4,5,9,6\ntrue\n1,1,2,3,4,5,6,9\n3,1,4,1,5,9,2,6\n1,1,2,3,4,5,6,9\n31\n31\nhello, world\ntrue\n12\nKhoor, Zruog\n3\n3\n{\"a\" => 3, \"b\" => 1, \"c\" => 1}\n14\nthe 5\ncat 2\nsat 2\n[\"cat\", \"end\"]\n",
"wasmStdout": "55\n1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n3\n4,5,9,6\ntrue\n1,1,2,3,4,5,6,9\n3,1,4,1,5,9,2,6\n1,1,2,3,4,5,6,9\n31\n31\nhello, world\ntrue\n12\nKhoor, Zruog\n3\n3\n{\"a\" => 3, \"b\" => 1, \"c\" => 1}\n14\nthe 5\ncat 2\nsat 2\n[\"cat\", \"end\"]\n",
@@ -459,10 +459,10 @@
"id": "iyi/calc",
"set": "iyi",
"path": "samples/iyi/calc.iyi",
- "sourceSha256": "7310f0e547c6ec9632bcf318538cd8778d8c32409822dafd3b98c43fcbf4e614",
+ "sourceSha256": "9844743a83b1d7bd80eceef04339216ef04e8a446c800f9c50da265442f8dc8e",
"wasm": "iyi-calc.wasm",
"bytes": 200618,
- "sha256": "aa2cfc22916be0e807cad21a8d95de21cb0c097b45add05dba340ceafc629503",
+ "sha256": "9eba1a8c39a31abf400ba374a183b7cb44cf4f502409883caba1d175943e99b0",
"exitCode": 0,
"nativeStdout": "64\n",
"wasmStdout": "64\n",
@@ -476,7 +476,7 @@
"sourceSha256": "9b8bb4c0bf703238a08afe44995207bb1cb6292578a7d255b5f3ece0c2a80213",
"wasm": "iyi-collections.wasm",
"bytes": 228588,
- "sha256": "36964f3cc2d2ea02d38a0564a8ef13256bfc6ca1af838aa79dbb2b937b95d6e9",
+ "sha256": "c27bcfda04e61f7b14dee7b8f846e2a323865e72df156a646f1b400819af1f27",
"exitCode": 0,
"nativeStdout": "to_a 3,1,4,1,5\nsize 5\nempty? false true\nall? true\nany? true\nnone? true\none? true\ncount 2\nfind 4 nil\nindex 2 nil\nfirst?/first 3 3\nmap 6,2,8,2,10\nflat_map 3,3,1,1,4,4,1,1,5,5\nmap_with_index 0,1,8,3,20\nselect 3,4,5\nreject 1,1\npartition 3+4+5 | 1+1\ntake/skip 3,1 1,5\ntake_while 3,1\nskip_while 4,1,5\nreduce 14\neach_with_index 0,1,2,3,4\neach_with_object 30,10,40,10,50\njoin 3-1-4-1-5\nincludes? true false\nmax?/max 5 5\nmin?/min 1 1\nsum/product 14 60\nsum of nothing 0\ntally 2\nsorted 1,1,3,4,5\nmax_by pear\ngroup_by pear,plum,kiwi\nindex_by fig\neach_slice 3+1 4+1 5\neach_cons 3+1 1+4 4+1 1+5\nchunk_while 3 1+4 1+5\nfirst(n) 3,1,4\ncompact_map 30,40,50\nfind_value big\naccumulate 0,3,4,8,9,14\nreduce? 14\nin_groups_of 3+1 4+1 5+0\nmin_by fig\nmax_of/min_of 4 3\nminmax 1,5\ntally_by 3\nsum_by 15\nzip 3pear 1fig 4plum 1kiwi\ncycle 1,2,1,2\nas a type 5 items / 4 items\n",
"wasmStdout": "to_a 3,1,4,1,5\nsize 5\nempty? false true\nall? true\nany? true\nnone? true\none? true\ncount 2\nfind 4 nil\nindex 2 nil\nfirst?/first 3 3\nmap 6,2,8,2,10\nflat_map 3,3,1,1,4,4,1,1,5,5\nmap_with_index 0,1,8,3,20\nselect 3,4,5\nreject 1,1\npartition 3+4+5 | 1+1\ntake/skip 3,1 1,5\ntake_while 3,1\nskip_while 4,1,5\nreduce 14\neach_with_index 0,1,2,3,4\neach_with_object 30,10,40,10,50\njoin 3-1-4-1-5\nincludes? true false\nmax?/max 5 5\nmin?/min 1 1\nsum/product 14 60\nsum of nothing 0\ntally 2\nsorted 1,1,3,4,5\nmax_by pear\ngroup_by pear,plum,kiwi\nindex_by fig\neach_slice 3+1 4+1 5\neach_cons 3+1 1+4 4+1 1+5\nchunk_while 3 1+4 1+5\nfirst(n) 3,1,4\ncompact_map 30,40,50\nfind_value big\naccumulate 0,3,4,8,9,14\nreduce? 14\nin_groups_of 3+1 4+1 5+0\nmin_by fig\nmax_of/min_of 4 3\nminmax 1,5\ntally_by 3\nsum_by 15\nzip 3pear 1fig 4plum 1kiwi\ncycle 1,2,1,2\nas a type 5 items / 4 items\n",
@@ -490,7 +490,7 @@
"sourceSha256": "e1c60e92bb4150097bed4d7986dc678380776ef09fc95ff03b278746ff121c10",
"wasm": "iyi-config.wasm",
"bytes": 177376,
- "sha256": "05857dcac41717b838f6759d649649fd76b56b6ca844daba07e794814b1b39c7",
+ "sha256": "a64c12afa8154579a053691b75b5aab9c38be7423a69ebf6e72669b29edb56e3",
"exitCode": 0,
"nativeStdout": "{\"name\" => \"iyi\", \"port\" => \"8080\", \"mode\" => \"fast\"}\n8081\nname, port, mode\nerror: line 2: no '=' in \"broken line\"\n[\"a\", \"\", \"b\"]\n[\"one\", \"two\"]\n\"padded\"\ntrue\n",
"wasmStdout": "{\"name\" => \"iyi\", \"port\" => \"8080\", \"mode\" => \"fast\"}\n8081\nname, port, mode\nerror: line 2: no '=' in \"broken line\"\n[\"a\", \"\", \"b\"]\n[\"one\", \"two\"]\n\"padded\"\ntrue\n",
@@ -504,7 +504,7 @@
"sourceSha256": "ab33030ac91db9cddf89e68f39e93fe96296f028f313e6e3c25afa76851aa32a",
"wasm": "iyi-derive.wasm",
"bytes": 128268,
- "sha256": "158451a926a6df41b90c175e6f8a6864386a9f26001a5fab8d1e88ee404dfedd",
+ "sha256": "b5e0d05aaaa29c0218472ee8d118c58325694f2f9fb1d62c44bd54a106e180bf",
"exitCode": 0,
"nativeStdout": "Point(@x=3 @y=4 )\nSpan(@from=1 @to=9 @label=range )\nTag(@label=Std::Derives::Name @count=2 )\n@label= \n",
"wasmStdout": "Point(@x=3 @y=4 )\nSpan(@from=1 @to=9 @label=range )\nTag(@label=Std::Derives::Name @count=2 )\n@label= \n",
@@ -518,7 +518,7 @@
"sourceSha256": "98b275c766e680a4adc21a3fc80c18b94e954317dd4668a4b303fcbeea8c0c5d",
"wasm": "iyi-enums.wasm",
"bytes": 151146,
- "sha256": "61a95872438a7e5b64b38c0d2d03bec8dadea2d309c927af0ea5836a6b533418",
+ "sha256": "0c4a1549d7642c41f006cb74806180daf689263e36015a665e3060c1406bcb0f",
"exitCode": 0,
"nativeStdout": "level: Warn\nspelled: Error\nsame: true\nother: false\nverdict: loud\nwarn? true\nerror? false\nat least info: true\nbelow error: true\nDebug Info Warn Error \nnames: 4\nparsed: Error\nlenient: Warn\nunknown: nil\nvalue: 3\nfrom 1: Info\nfrom 9: nil\nmode: Read | Write\nread: true\nappend: false\nnone: None\nmasked: Write\nwarns: 2\nerrors: 1\n",
"wasmStdout": "level: Warn\nspelled: Error\nsame: true\nother: false\nverdict: loud\nwarn? true\nerror? false\nat least info: true\nbelow error: true\nDebug Info Warn Error \nnames: 4\nparsed: Error\nlenient: Warn\nunknown: nil\nvalue: 3\nfrom 1: Info\nfrom 9: nil\nmode: Read | Write\nread: true\nappend: false\nnone: None\nmasked: Write\nwarns: 2\nerrors: 1\n",
@@ -532,7 +532,7 @@
"sourceSha256": "89e937ddfc973d79b5f00c8ea3b81315a68b5c2c6cf4d1bac4172a3da1b79bd0",
"wasm": "iyi-errors.wasm",
"bytes": 138685,
- "sha256": "568f8fd704e78223ce04ae518ea433b633c34de8620851cc9bd85012d8c8a353",
+ "sha256": "edf9cb5dd070d1099e51ea5297f54cb1260135ed6874c553029c63593777412d",
"exitCode": 0,
"nativeStdout": "read 12\nio: no such file: missing\nparse: not a number: twelve\nor: 12\nor: 0\nor: 0\nor: -1\npanic: 12\nsuccess:\n open outer\n open inner\n close inner\n close outer\n12\npropagating:\n open outer\n close outer\n-1\n iteration 0\n iteration 0 closed\n iteration 1\n iteration 1 closed\n",
"wasmStdout": "read 12\nio: no such file: missing\nparse: not a number: twelve\nor: 12\nor: 0\nor: 0\nor: -1\npanic: 12\nsuccess:\n open outer\n open inner\n close inner\n close outer\n12\npropagating:\n open outer\n close outer\n-1\n iteration 0\n iteration 0 closed\n iteration 1\n iteration 1 closed\n",
@@ -546,7 +546,7 @@
"sourceSha256": "aaa45bbe135b26d2c882122da7a4fe69df4bcba517a6f8ed57ba0101eda8ba69",
"wasm": "iyi-files.wasm",
"bytes": 125156,
- "sha256": "2b85382b6a4182d4bade780067738aca23596bea2e6d9a1646a3f66e0158461a",
+ "sha256": "938f2c61da810eafcf644405d304bfbb860920f9929fe22c1fa8ca2a6a0655bf",
"exitCode": 1,
"nativeStdout": "hello from iyi\ntrue\nfalse\nfalse\n",
"wasmStdout": "",
@@ -560,7 +560,7 @@
"sourceSha256": "54f6992a74d0bff957fed60b05e8bd9d7bb16b091ea1b79e8277acd7454128bb",
"wasm": "iyi-format.wasm",
"bytes": 308041,
- "sha256": "147d0c42ecc035df2e8e50b0fd0452eb7a81478c36091d89bbc1da13361607ec",
+ "sha256": "d9342a14e3491780072b1038e479579ef3724e056e12fe7f56fccf1eb9080622",
"exitCode": 0,
"nativeStdout": "hello world\nchar 'A' (ascii 65)\nright ' hi'\nleft 'hi '\ndec: 255\nhex: ff\nHEX: 0XFF\noct: 0o377\nbin: 0b101\nplus: +42\nzero: 000042\nsign zero: +00042\nnegative zero: -00042\nint precision: 0007\nfloat precision: 3.14\nfloat precision: 3.1416\nstring precision: cin\nwider than field: 12345\nzero precision float: 4\nzero precision zero int: ' '\nmin int64: -9223372036854775808\nmax int64: 9223372036854775807\nString.format: answer = 42\npercent tuple: score = 0099\nprintf: run completed\n",
"wasmStdout": "hello world\nchar 'A' (ascii 65)\nright ' hi'\nleft 'hi '\ndec: 255\nhex: ff\nHEX: 0XFF\noct: 0o377\nbin: 0b101\nplus: +42\nzero: 000042\nsign zero: +00042\nnegative zero: -00042\nint precision: 0007\nfloat precision: 3.14\nfloat precision: 3.1416\nstring precision: cin\nwider than field: 12345\nzero precision float: 4\nzero precision zero int: ' '\nmin int64: -9223372036854775808\nmax int64: 9223372036854775807\nString.format: answer = 42\npercent tuple: score = 0099\nprintf: run completed\n",
@@ -574,7 +574,7 @@
"sourceSha256": "ad90bbbd17da587770609673949d55c990aee356dda1d437ce23394231ec75ee",
"wasm": "iyi-formatting.wasm",
"bytes": 143143,
- "sha256": "bd023ab26162567aac8c0e688897d08812255bfd9e71d50e4b16a658389972e3",
+ "sha256": "455c2338487d1440594743329e00b74f1ea682bf18e35aac8a526ad4144e9bfd",
"exitCode": 0,
"nativeStdout": "last 30\nfirst 10\nwritten 99\nchar o\nslice lo\nhex ff\nbin 101\nrjust ' ab'\nljust 'ab***'\nrep ababab\nnil nil\nmaybe 99\n",
"wasmStdout": "last 30\nfirst 10\nwritten 99\nchar o\nslice lo\nhex ff\nbin 101\nrjust ' ab'\nljust 'ab***'\nrep ababab\nnil nil\nmaybe 99\n",
@@ -588,7 +588,7 @@
"sourceSha256": "b3ec6ebe033571c8bedecdb4d00a5df97eeca44b709d94352066c8f215f4bbe1",
"wasm": "iyi-generics.wasm",
"bytes": 143824,
- "sha256": "f50fbc70667358f6bd3117167fd7f901fe0f116b2b6fc21a9defdab12043ff01",
+ "sha256": "6a8727c576c6679e07701cfd869abd86fee9b5c054dfd59b1a5f054b472d1923",
"exitCode": 0,
"nativeStdout": "Box(42)\nBox(hello)\nfirst=1, second=two\nBox(1)\nBox(two)\nfirst=3, second=four\nada, ada\n84\ntrue\n7\n",
"wasmStdout": "Box(42)\nBox(hello)\nfirst=1, second=two\nBox(1)\nBox(two)\nfirst=3, second=four\nada, ada\n84\ntrue\n7\n",
@@ -602,7 +602,7 @@
"sourceSha256": "2f28278d0111d0ecc5c45afa5c2de9841abb5ad5f0041417dc97caa4fbc5338f",
"wasm": "iyi-grid.wasm",
"bytes": 187747,
- "sha256": "ad676e5b372c5034175d09f58220bc40833acb96676b153cb95b3cc60fd7737f",
+ "sha256": "fc5548ba269a5eb3ea6ad618af6fcf49f263afe863eb1225e63e50b71f66cf69",
"exitCode": 0,
"nativeStdout": " 0 1 2 3\n 4 5 6 7\n 8 9 10 11\n 12 13 14 15\n120\n[0, 5, 10, 15]\n[0, 4, 8, 12]\n[5, 4, 3, 2, 1]\n[2, 4, 6, 8, 10]\n55\n[3, 1, 2]\n[{1, \"a\"}, {2, \"b\"}, {3, \"c\"}]\n2\na\n[\"b\", \"c\"]\n01234\n",
"wasmStdout": " 0 1 2 3\n 4 5 6 7\n 8 9 10 11\n 12 13 14 15\n120\n[0, 5, 10, 15]\n[0, 4, 8, 12]\n[5, 4, 3, 2, 1]\n[2, 4, 6, 8, 10]\n55\n[3, 1, 2]\n[{1, \"a\"}, {2, \"b\"}, {3, \"c\"}]\n2\na\n[\"b\", \"c\"]\n01234\n",
@@ -616,7 +616,7 @@
"sourceSha256": "27cdcc173bc626bf84c26f468628026979b15bc6127e67594c5adf2930483a83",
"wasm": "iyi-hello.wasm",
"bytes": 130696,
- "sha256": "20965268d4cb996ccc46053610de0fc1ff5c2b0a8e616f234d50dac815137557",
+ "sha256": "bfed0df44969235bb0212ac02af2c40bd3e58ecb9588c82fcd8ee03b0d9e5bf1",
"exitCode": 0,
"nativeStdout": "Hello, iyi!\nHELLO, IYI!\nBEEP 42\nHello, crystal!\nBEEP 7\n-> BEEP 9\n",
"wasmStdout": "Hello, iyi!\nHELLO, IYI!\nBEEP 42\nHello, crystal!\nBEEP 7\n-> BEEP 9\n",
@@ -630,7 +630,7 @@
"sourceSha256": "ffdb52f45c0426a876e109c9e26862d43d4bbc7bca547612e8b31dc5a10e7b9a",
"wasm": "iyi-immutable.wasm",
"bytes": 197267,
- "sha256": "6b58efb5ee61baef3e766191b9e1705696c9f6d278b37546bf0d06df928af0f5",
+ "sha256": "11d4ebd1d2dcce4bad066884d950579d96256ecdb018ed985c61ceab8258e7d6",
"exitCode": 0,
"nativeStdout": "size 5\nempty? false true\nat 3 5\nappended 6 from 5, original 3,1,4,1,5\nconcatenated 3,1,4,1,5,7 / 5\ncopied list 2, source 3\nto_a 3,1,4,1,5\nall?/any? true true\nnone?/one? true true\ncount 2\nfind 4 nil\nindex 2 nil\nfirst?/first 3 3\nmap 6,2,8,2,10\nflat_map 3,3,1,1,4,4,1,1,5,5\nmap_with_index 0,1,8,3,20\nselect/reject 3,4,5 1,1\npartition 3+4+5 | 1+1\ntake/skip 3,1 1,5\ntake_while 3,1\nskip_while 4,1,5\nreduce 14\neach_with_object 30,10,40,10,50\njoin 3-1-4-1-5\nincludes? true false\nmax/min 5 1\nsum/product 14 60\ntally 2\nsorted 1,1,3,4,5\nmax_by pear\ngroup_by pear,plum,kiwi\ntwo elem types 4,2,5,2,6 / 4,3,4,4\n",
"wasmStdout": "size 5\nempty? false true\nat 3 5\nappended 6 from 5, original 3,1,4,1,5\nconcatenated 3,1,4,1,5,7 / 5\ncopied list 2, source 3\nto_a 3,1,4,1,5\nall?/any? true true\nnone?/one? true true\ncount 2\nfind 4 nil\nindex 2 nil\nfirst?/first 3 3\nmap 6,2,8,2,10\nflat_map 3,3,1,1,4,4,1,1,5,5\nmap_with_index 0,1,8,3,20\nselect/reject 3,4,5 1,1\npartition 3+4+5 | 1+1\ntake/skip 3,1 1,5\ntake_while 3,1\nskip_while 4,1,5\nreduce 14\neach_with_object 30,10,40,10,50\njoin 3-1-4-1-5\nincludes? true false\nmax/min 5 1\nsum/product 14 60\ntally 2\nsorted 1,1,3,4,5\nmax_by pear\ngroup_by pear,plum,kiwi\ntwo elem types 4,2,5,2,6 / 4,3,4,4\n",
@@ -644,7 +644,7 @@
"sourceSha256": "0eee830465aeffb6ce0cabe6230e1c8dd87b37458bb42f9ba9b567721dcdc340",
"wasm": "iyi-init_order.wasm",
"bytes": 124809,
- "sha256": "c71d26e6e1579dd29cd5b44976cf9a6015ab83e04dd164075fa1fbc294707842",
+ "sha256": "b0f607519ec0dd50324fcb841e32ede39fe8f203908d6476de67ee438bf76416",
"exitCode": 0,
"nativeStdout": "1. boot/config\n2. boot/registry, above its own `import` line\n3. boot/registry, below it\n4. samples/init_order, above its own `import` lines\n5. Hello, iyi.\n",
"wasmStdout": "1. boot/config\n2. boot/registry, above its own `import` line\n3. boot/registry, below it\n4. samples/init_order, above its own `import` lines\n5. Hello, iyi.\n",
@@ -658,7 +658,7 @@
"sourceSha256": "8d17a1ab3590cbe103b0eedb8487c9df8cc2837e0b84b26a3c8abbe353af80d6",
"wasm": "iyi-inventory.wasm",
"bytes": 218027,
- "sha256": "539236d4dd652a76671806850aec414c540083d9f1b21d90037981853f0501c1",
+ "sha256": "5b3186682657003e9a8ab9fda0c3d04b4b6e563f1a35e16d0576681edd83a52e",
"exitCode": 0,
"nativeStdout": "total: 11.0\ncheap: apple, pear\n2\ntrue\ntrue\nmelon\npear\n3.0\nnil\n1. apple 10 5.0\n2. pear 4 3.0\n3. melon 1 3.0\n[\"apple\", \"pear\", \"melon\"]\n15\npear\n",
"wasmStdout": "total: 11.0\ncheap: apple, pear\n2\ntrue\ntrue\nmelon\npear\n3.0\nnil\n1. apple 10 5.0\n2. pear 4 3.0\n3. melon 1 3.0\n[\"apple\", \"pear\", \"melon\"]\n15\npear\n",
@@ -672,7 +672,7 @@
"sourceSha256": "8878852be923dc681f060baf0ae863e04a30e87f123498813c37738c2c97d41f",
"wasm": "iyi-io.wasm",
"bytes": 131609,
- "sha256": "5836629d0d408e7d5e0763d25608c69c95fd72fa14499f8bd2f9246ffbc2a75b",
+ "sha256": "6b01baf6530675f1c0de0ffa3b59514e636ae74e6612b50d93ec7b82d1755cbd",
"exitCode": 0,
"nativeStdout": "no lines read from standard input\n",
"wasmStdout": "no lines read from standard input\n",
@@ -686,7 +686,7 @@
"sourceSha256": "f4ea9f6e29e18cf9452eccc75ba8324c25d108f825b64765f44335c22c57e0be",
"wasm": "iyi-modules.wasm",
"bytes": 126751,
- "sha256": "3f62b81809aab9324b80610bef0d02d4deeeb9aeea7b33b66679e94d7b952d37",
+ "sha256": "aefa7c4a462776d1d49bb86f7b8d17a2c6eee4855a055e064a3f17aebade4bf6",
"exitCode": 0,
"nativeStdout": "HELLO, IYI. Good day, iyi.\nfriend\nesteemed colleague\n",
"wasmStdout": "HELLO, IYI. Good day, iyi.\nfriend\nesteemed colleague\n",
@@ -700,7 +700,7 @@
"sourceSha256": "d33fc3528bffdd710b785425e5b0559e606c53184c2234774d56fc7d9f2f55c9",
"wasm": "iyi-sessions.wasm",
"bytes": 139540,
- "sha256": "a727d57d37d0fef94b9250d39c4d9d417292225cc27704badf6fb3f74ccf49f6",
+ "sha256": "4485718365ed0640af3e3bddbbc19174f9524a49bff26f6619a3cfb07ca326c9",
"exitCode": 0,
"nativeStdout": "three arrivals:\n ada since tick 1\n linus since tick 2\n grace since tick 3\n (3 signed in)\nlinus left, had been in since tick 2\n ada since tick 1\n grace since tick 3\n (2 signed in)\nnobody named bob: true\nlinus is back:\n ada since tick 1\n grace since tick 3\n linus since tick 4\n (3 signed in)\n ada since tick 1\n grace since tick 3\n linus since tick 4\n (3 signed in)\n",
"wasmStdout": "three arrivals:\n ada since tick 1\n linus since tick 2\n grace since tick 3\n (3 signed in)\nlinus left, had been in since tick 2\n ada since tick 1\n grace since tick 3\n (2 signed in)\nnobody named bob: true\nlinus is back:\n ada since tick 1\n grace since tick 3\n linus since tick 4\n (3 signed in)\n ada since tick 1\n grace since tick 3\n linus since tick 4\n (3 signed in)\n",
@@ -714,7 +714,7 @@
"sourceSha256": "e3e4ca261b7f764ce2a168163c6da93909efc63d663fb1828f7b6d5a79b9f805",
"wasm": "iyi-shapes.wasm",
"bytes": 197147,
- "sha256": "83afd9d2684181460bb4598b5fadd232bb9dfcdd995ea80fecc996544a847725",
+ "sha256": "1cd492ea85bbe9e4a9564b00c2737250ab7bea9fd339735e1484882e3d6ace71",
"exitCode": 0,
"nativeStdout": "circle: 3.14\nrect: 6.0\n9.1\nrect\n[12, nil, 7, -3]\n16\n[12, 7, 3]\n1024\n0.25\n1099511627776\n128\n8\n",
"wasmStdout": "circle: 3.14\nrect: 6.0\n9.1\nrect\n[12, nil, 7, -3]\n16\n[12, 7, 3]\n1024\n0.25\n1099511627776\n128\n8\n",
@@ -728,7 +728,7 @@
"sourceSha256": "afb4e7913d7b5c01a6e3ba5f42b3945e6ede3a374552325d07fa51ac25fa43e3",
"wasm": "iyi-socket.wasm",
"bytes": 413767,
- "sha256": "f73737f8d2b26b4f39b4eedd2958fb3b16648bd979191fc517b8075b7777d26d",
+ "sha256": "6f04c78fd4d8e50b90c8d359d7421fc35a208207b664e2d20726e2278935dfd1",
"exitCode": 1,
"nativeStdout": "server listening on an ephemeral port\nserver received: hello from client\nclient received: hello from server\nclosed\n",
"wasmStdout": "",
@@ -742,7 +742,7 @@
"sourceSha256": "fdfedfc80d758e3490725c1fc6ebaa5648f5066854a28ccf1eb8cdb90b872fc8",
"wasm": "iyi-std_iterator.wasm",
"bytes": 208688,
- "sha256": "92e59ce82a781f1fab12b70d2c721393e9aa4efaebf613fd9aa5a713722799c8",
+ "sha256": "4b03bfefec927743879d96208e917c1b76c74ed48789a16f29bb33c3bdbcb049",
"exitCode": 0,
"nativeStdout": "natural numbers first 5: [1, 2, 3, 4, 5]\npulls from generator: 5\neven squares first 4: [4, 16, 36, 64]\npulls for 4 matches: 8\nchained odds and evens: [1, 3, 5, 2, 4, 6]\nzipped pairs: [{\"apple\", 100}, {\"banana\", 95}, {\"cherry\", 88}]\neach_slice(3): [[1, 2, 3], [4, 5, 6], [7]]\neach_cons(3): [[10, 20, 30], [20, 30, 40], [30, 40, 50]]\nstepped by 2: [0, 20, 40, 60]\ncycled 2 times: [\"tick\", \"tock\", \"tick\", \"tock\"]\nwith_index(1): [{\"alpha\", 1}, {\"beta\", 2}, {\"gamma\", 3}]\nreduce sum: 100\nfirst > 10: 12\n",
"wasmStdout": "natural numbers first 5: [1, 2, 3, 4, 5]\npulls from generator: 5\neven squares first 4: [4, 16, 36, 64]\npulls for 4 matches: 8\nchained odds and evens: [1, 3, 5, 2, 4, 6]\nzipped pairs: [{\"apple\", 100}, {\"banana\", 95}, {\"cherry\", 88}]\neach_slice(3): [[1, 2, 3], [4, 5, 6], [7]]\neach_cons(3): [[10, 20, 30], [20, 30, 40], [30, 40, 50]]\nstepped by 2: [0, 20, 40, 60]\ncycled 2 times: [\"tick\", \"tock\", \"tick\", \"tock\"]\nwith_index(1): [{\"alpha\", 1}, {\"beta\", 2}, {\"gamma\", 3}]\nreduce sum: 100\nfirst > 10: 12\n",
@@ -756,7 +756,7 @@
"sourceSha256": "c891f86fe35a876e7f8279c549a0d16a39f38715593fae69a4ca387cb6468276",
"wasm": "iyi-std_text.wasm",
"bytes": 195805,
- "sha256": "04241460a6d66a0766649a2f142d813f82856387e40cf1ae9035bf3972dd9c4d",
+ "sha256": "58b65f1dc5af2b855f2cbafa2d65b28915a0664a28b44b69109f783e4ff3858d",
"exitCode": 0,
"nativeStdout": "cleaned line: 'GET /api/v1/users HTTP/1.1'\nHTTP method: GET, path: /api/v1/users, version: HTTP/1.1\nHeader 'Authorization' = 'Bearer token123'\nslug: hello-world-this-is-iyi-std-text-\ngreeting size: 11 characters, 12 bytes\nreversed greeting: dlrow olléh\nhex 'ff' to decimal: 255\n",
"wasmStdout": "cleaned line: 'GET /api/v1/users HTTP/1.1'\nHTTP method: GET, path: /api/v1/users, version: HTTP/1.1\nHeader 'Authorization' = 'Bearer token123'\nslug: hello-world-this-is-iyi-std-text-\ngreeting size: 11 characters, 12 bytes\nreversed greeting: dlrow olléh\nhex 'ff' to decimal: 255\n",
@@ -770,7 +770,7 @@
"sourceSha256": "82a17d942f25fe20e124340ce9245293d280594fc4e07ee5a4e82dee62dd3620",
"wasm": "iyi-std_time.wasm",
"bytes": 275376,
- "sha256": "0a094095f5727d4557ada435e75dd3bf9cc041500621f24f85ad21b678d4986f",
+ "sha256": "d494d5133d6a7daee036be9fe3610dc070da42ca509794c468f848959b6dcf7e",
"exitCode": 0,
"nativeStdout": "Reference time (UTC): 2024-02-29 12:30:45 UTC\nRFC 3339: 2024-02-29T12:30:45.123Z\nUnix timestamp: 1709209845 s (1709209845123 ms)\nYear: 2024\nMonth: 2\nDay: 29\nDay of week: Thursday\nDay of year: 60\nTomorrow: 2024-03-01T12:30:45Z\nYesterday: 2024-02-28T12:30:45Z\nLive wall clock valid: true\nMonotonic duration >= 0: true\nLeap day event: 2024-02-29 12:00:00 UTC\nIs leap year: true\n",
"wasmStdout": "Reference time (UTC): 2024-02-29 12:30:45 UTC\nRFC 3339: 2024-02-29T12:30:45.123Z\nUnix timestamp: 1709209845 s (1709209845123 ms)\nYear: 2024\nMonth: 2\nDay: 29\nDay of week: Thursday\nDay of year: 60\nTomorrow: 2024-03-01T12:30:45Z\nYesterday: 2024-02-28T12:30:45Z\nLive wall clock valid: true\nMonotonic duration >= 0: true\nLeap day event: 2024-02-29 12:00:00 UTC\nIs leap year: true\n",
@@ -784,7 +784,7 @@
"sourceSha256": "987608a702df760c9cb5f3c0c85a5f621553fda885504892acb2fbb67d57d744",
"wasm": "iyi-visited.wasm",
"bytes": 176116,
- "sha256": "096c6ee3b3198ec711291e770e4eb6c44dcdf808b9b095c067e6d58a6e1145ba",
+ "sha256": "ed4b948df48e36d0f8dd7b4d73e31e9b937ee1aaae31a282027a047b49336e35",
"exitCode": 0,
"nativeStdout": "Set{\"home\", \"docs\", \"blog\", \"install\", \"about\"}\n5\nfalse\nSet{\"install\"}\nSet{\"orphan\"}\ntrue\nfalse\ntrue\ntrue\n[\"the\", \"cat\", \"and\", \"dog\", \"bird\"]\n",
"wasmStdout": "Set{\"home\", \"docs\", \"blog\", \"install\", \"about\"}\n5\nfalse\nSet{\"install\"}\nSet{\"orphan\"}\ntrue\nfalse\ntrue\ntrue\n[\"the\", \"cat\", \"and\", \"dog\", \"bird\"]\n",
@@ -798,7 +798,7 @@
"sourceSha256": "5bba5cdefa6407b2fe87e8c84bfdcf195bf02177193fbaea3e646c143018f296",
"wasm": "iyi-webapp.wasm",
"bytes": 171353,
- "sha256": "f02d415451f16bed548312611374d917535ea7414bb670ce250c3d75f8827592",
+ "sha256": "c8989407961055e536ceb461549618dcd6fa29740e2635bfeb43c956bc08ddc2",
"exitCode": 0,
"nativeStdout": "GET /\nGET /users\nPOST /users\nGET /count\nGET /v1\nGET /v1/admin/dashboard\n1|42|0\n",
"wasmStdout": "GET /\nGET /users\nPOST /users\nGET /count\nGET /v1\nGET /v1/admin/dashboard\n1|42|0\n",
diff --git a/records/wasm/methods.wasm b/records/wasm/methods.wasm
index f16fbf4..29a1df8 100755
Binary files a/records/wasm/methods.wasm and b/records/wasm/methods.wasm differ
diff --git a/records/wasm/modules.wasm b/records/wasm/modules.wasm
index 0cf747a..0c3aa49 100755
Binary files a/records/wasm/modules.wasm and b/records/wasm/modules.wasm differ
diff --git a/records/wasm/nil.wasm b/records/wasm/nil.wasm
index e60c633..3251665 100755
Binary files a/records/wasm/nil.wasm and b/records/wasm/nil.wasm differ
diff --git a/records/wasm/ranges.wasm b/records/wasm/ranges.wasm
index 8cc93c2..56f19cf 100755
Binary files a/records/wasm/ranges.wasm and b/records/wasm/ranges.wasm differ
diff --git a/records/wasm/returns.wasm b/records/wasm/returns.wasm
index 7dff27f..0a9f90e 100755
Binary files a/records/wasm/returns.wasm and b/records/wasm/returns.wasm differ
diff --git a/records/wasm/strings.wasm b/records/wasm/strings.wasm
index 8b40082..3c52403 100755
Binary files a/records/wasm/strings.wasm and b/records/wasm/strings.wasm differ
diff --git a/records/wasm/structs.wasm b/records/wasm/structs.wasm
index 722ece1..60b8ca7 100755
Binary files a/records/wasm/structs.wasm and b/records/wasm/structs.wasm differ
diff --git a/records/wasm/traits.wasm b/records/wasm/traits.wasm
index 73f4657..5fead95 100755
Binary files a/records/wasm/traits.wasm and b/records/wasm/traits.wasm differ
diff --git a/records/wasm/tuples.wasm b/records/wasm/tuples.wasm
index a79acc2..ec0ae85 100755
Binary files a/records/wasm/tuples.wasm and b/records/wasm/tuples.wasm differ
diff --git a/records/wasm/types.wasm b/records/wasm/types.wasm
index 497c8d3..97f6631 100755
Binary files a/records/wasm/types.wasm and b/records/wasm/types.wasm differ
diff --git a/records/wasm/unions.wasm b/records/wasm/unions.wasm
index eecb8cb..f130dec 100755
Binary files a/records/wasm/unions.wasm and b/records/wasm/unions.wasm differ
diff --git a/records/wasm/variables.wasm b/records/wasm/variables.wasm
index 1db3ace..a2cb4a7 100755
Binary files a/records/wasm/variables.wasm and b/records/wasm/variables.wasm differ
diff --git a/scripts/install-targets.mjs b/scripts/install-targets.mjs
index 4248245..f74ab6f 100755
--- a/scripts/install-targets.mjs
+++ b/scripts/install-targets.mjs
@@ -24,28 +24,33 @@
// * the refusal the `*)` arm prints, which names the same targets in prose
// for a person whose machine is neither. The arms and the message drifting
// apart is the ordinary way this file goes wrong, and it is caught here;
-// * README.md's "Getting it" section, which states the pair a reader is
-// shown, so the site restates the repository rather than one file of it;
-// * README.md's own platform words under the one-liner ("Linux x86-64 and
-// macOS arm64"), which is where the prose name beside each target comes
-// from. They are found by the fenced block that holds the `install.sh`
-// one-liner rather than by the clause that used to follow them, because
-// what follows is prose about other platforms and moves: 48bc8d0b5 put a
-// PowerShell block between the sentence and the clause this once matched.
-// What does not move is that the sentence sits directly under the command
-// it qualifies. Each phrase is matched back to a target by scoring the
-// target's own `uname` components against the phrase's words, so the only
-// spellings this file knows are the two aliases below. An unmatched or
-// ambiguous phrase fails and names itself; it is never guessed;
+// * INSTALL.md's "Linux and macOS" section, which states the pair a reader
+// is shown, so the site restates the repository rather than one file of
+// it. It was README.md's "Getting it" until 0.20.1 moved installing to
+// INSTALL.md;
+// * README.md's own platform words above the one-liner ("Linux x86-64, or
+// macOS 14.4 or later on arm64, into `~/.local`:"). They are found by the
+// fenced block that holds the `install.sh` one-liner rather than by the
+// clause that used to follow them, because what follows is prose about
+// other platforms and moves: 48bc8d0b5 put a PowerShell block between the
+// sentence and the clause this once matched. What does not move is that
+// the sentence sits directly above the command it qualifies. Each phrase
+// is matched back to a target by scoring the target's own `uname`
+// components against the phrase's words, so the only spellings this file
+// knows are the two aliases below. A target no phrase names, or a phrase
+// naming a platform no target is, fails and names itself; it is never
+// guessed. The prose name beside each target is README.md's own
+// ("macOS arm64"), found by the target's own components rather than
+// assembled from them, because the sentence now carries a floor inside
+// the phrase;
// * install.ps1, which is the same three questions for the one platform
// install.sh refuses. 0.14.0 is the first release to publish a Windows
// build, and it is a zip rather than a tarball with an installer of its
// own, so it is recorded beside the tarballs instead of among them: one
// target, read out of the `$Asset` line that names it, cross-checked
// against the refusal that script prints for a 32-bit Windows and against
-// the file name README.md spells out under "Getting it". Its prose name
-// is README.md's own ("Windows x86-64"), found by the target's own
-// components rather than assembled from them.
+// the file name INSTALL.md spells out under "Windows". Its prose name is
+// README.md's own ("Windows x86-64"), found the same way.
//
// WHAT IT DOES NOT DO. It does not check that a release actually published the
// tarballs, or that a URL resolves. It checks that the site offers exactly what
@@ -66,6 +71,7 @@ const out = resolve(site, "src", "generated");
const SOURCE = "install.sh";
const WINDOWS = "install.ps1";
const CLAIM = "README.md";
+const INSTALL = "INSTALL.md";
/**
* Spellings the repository uses for one platform. `uname -s` says `Darwin` and
@@ -93,9 +99,12 @@ const read = (relative) => {
};
const script = read(SOURCE);
-/* README.md wraps at about 80 columns, so its sentences are matched against
- * one long line, the way bench/site_facts.py reads it. */
-const readme = read(CLAIM).replace(/\s+/g, " ");
+/* README.md and INSTALL.md wrap at about 80 columns, so their sentences are
+ * matched against one long line, the way bench/site_facts.py reads them. */
+const flat = (text) => text.replace(/\s+/g, " ");
+const readmeText = read(CLAIM);
+const readme = flat(readmeText);
+const install = flat(read(INSTALL));
/* The `case` block on `$os-$arch`, and nothing else in the script: a `case` on
* something else must not be mistaken for this one. */
@@ -149,12 +158,12 @@ if (disagree.length > 0) {
);
}
-/* README's "Getting it" section, which is where a reader of the repository is
- * told which tarball the installer picks. */
-const documented = /picks the\s+tarball for your machine,\s+([^,]*?),\s+and refuses/.exec(readme);
+/* INSTALL.md's "Linux and macOS" section, which is where a reader of the
+ * repository is told which tarball the installer picks. */
+const documented = /picks the\s+tarball for your machine,\s+([^,]*?),\s+and refuses/.exec(install);
if (!documented) {
throw new Error(
- `install-targets: ${CLAIM} no longer says which tarball install.sh picks ` +
+ `install-targets: ${INSTALL} no longer says which tarball install.sh picks ` +
`for \`uname\`, so the target list has only install.sh stating it and ` +
`this script has nothing to cross-check.`,
);
@@ -166,29 +175,38 @@ const undocumented = [...accepted, ...listed].filter(
if (undocumented.length > 0) {
throw new Error(
`install-targets: ${SOURCE} accepts [${[...accepted].join(", ")}] and ` +
- `${CLAIM} says it picks [${[...listed].join(", ")}]. The repository ` +
+ `${INSTALL} says it picks [${[...listed].join(", ")}]. The repository ` +
`states the set twice and the two have drifted, which is the drift this ` +
`whole publication is arranged to refuse.`,
);
}
-/* The platform words, out of the parenthesis that introduces README's
- * one-liner: "Install the latest release into `~/.local` (Linux x86-64, macOS
- * arm64; a program built for macOS runs on macOS 14.4 or later, ...):",
- * directly above the fenced command. The platforms are the list before the
- * first semicolon; what follows it is a note about one of them, as 0.20.0
- * added. Anchored on that fence, so a paragraph about a platform this record
- * does not carry - the Windows one-liner's, under it - cannot be read
- * instead. */
-const prose = /\(([^()]*)\):\s*```sh\s+curl [^`]*?install\.sh \| sh/.exec(readme);
-if (!prose) {
+/* The platform words, out of the paragraph that introduces README's
+ * one-liner: "Linux x86-64, or macOS 14.4 or later on arm64, into
+ * `~/.local`:", directly above the fenced command. Anchored on that fence, so
+ * a paragraph about a platform this record does not carry - the Windows
+ * one-liner's, under it - cannot be read instead. */
+const lines = readmeText.replace(/\r\n/g, "\n").split("\n");
+const fence = lines.findIndex(
+ (line, i) => /^curl [^`]*install\.sh \| sh/.test(line) && /^```sh\s*$/.test(lines[i - 1] ?? ""),
+);
+let last = fence - 2;
+while (last >= 0 && lines[last].trim() === "") last--;
+let first = last;
+while (first > 0 && lines[first - 1].trim() !== "" && !lines[first - 1].startsWith("#")) first--;
+const sentence = fence < 0 || last < 0 ? "" : flat(lines.slice(first, last + 1).join(" ")).trim();
+if (!sentence.endsWith(":")) {
throw new Error(
- `install-targets: ${CLAIM} no longer states the platforms in the ` +
- `parenthesis above the fenced ${SOURCE} one-liner, so there is no phrase in the ` +
- `repository to take the prose name of each target from.`,
+ `install-targets: ${CLAIM} no longer introduces the fenced ${SOURCE} ` +
+ `one-liner with a sentence ending in a colon, so there is no statement ` +
+ `in the repository of which machines that command is for.`,
);
}
-const phrases = prose[1].split(";")[0].split(/\s*(?:,|and)\s*/).filter(Boolean);
+const phrases = sentence
+ .replace(/:$/, "")
+ .split(/,\s*(?:(?:or|and)\s+)?/)
+ .map((phrase) => phrase.trim())
+ .filter(Boolean);
/** The words of a phrase, and the parts of any word that is itself hyphenated,
* so `x86-64` is matched whole as well as by its halves. */
@@ -211,37 +229,62 @@ const score = (target, phrase) => {
}).length;
};
-/* One phrase per target, each phrase used once. A target with no single best
- * phrase left is a failure naming the target, never a guess: a prose name
- * attached to the wrong tarball is worse than none, because it would send a
- * reader to a download for a machine they do not have. */
-if (phrases.length !== published.length) {
+/* One phrase per target, naming both of its components, each phrase used
+ * once. A target with no such phrase is a failure naming the target, never a
+ * guess: the sentence above the command is what tells a reader which machine
+ * it is for. */
+const taken = new Set();
+for (const target of published) {
+ const naming = phrases.filter((phrase) => !taken.has(phrase) && score(target, phrase) === 2);
+ if (naming.length !== 1) {
+ throw new Error(
+ `install-targets: ${naming.length === 0 ? "no" : "more than one"} phrase ` +
+ `in ${CLAIM}'s "${sentence}" names ${target.target} (${target.os} ` +
+ `${target.arch}). Either the sentence was reworded or a spelling ` +
+ `belongs in ALIAS.`,
+ );
+ }
+ taken.add(naming[0]);
+}
+
+/* And no phrase there names a platform that is not one of the targets: a
+ * sentence offering the one-liner to a third machine would send that reader to
+ * a refusal. A phrase that names no component of any target is not a
+ * platform, such as where the install goes. */
+const stray = phrases.filter(
+ (phrase) => !taken.has(phrase) && published.some((target) => score(target, phrase) > 0),
+);
+if (stray.length > 0) {
throw new Error(
- `install-targets: ${CLAIM} names ${phrases.length} platform(s) ` +
- `(${phrases.join(" / ")}) under its one-liner and ${SOURCE} ships ` +
- `${published.length} tarball(s) (${[...accepted].join(", ")}). Every ` +
- `published target needs the name a reader is shown.`,
+ `install-targets: ${CLAIM} offers the ${SOURCE} one-liner to ` +
+ `${stray.join(" / ")}, and ${SOURCE} ships ${[...accepted].join(", ")}. ` +
+ `Every platform the sentence names needs a published tarball.`,
);
}
-const taken = new Set();
-const targets = published.map((target) => {
- const ranked = phrases
- .filter((phrase) => !taken.has(phrase))
- .map((phrase) => ({ phrase, points: score(target, phrase) }))
- .sort((a, b) => b.points - a.points);
- const [best, next] = ranked;
- if (best.points === 0 || (next && next.points === best.points)) {
+/* The prose name of a target, in README.md's own words. The pattern is built
+ * from the target's own components and the aliases above, and what is
+ * recorded is the text README.md matched with, so the spelling a reader is
+ * shown is the repository's and never this file's. */
+const spell = (part) => ALIAS.get(part.toLowerCase()) ?? part;
+const spoken = (os, arch, target) => {
+ const match = new RegExp(
+ `\\b${spell(os)}[ -]${spell(arch).replace(/[-_]/g, "[-_]")}\\b`,
+ "i",
+ ).exec(readme);
+ if (!match) {
throw new Error(
- `install-targets: no single platform name in ${CLAIM} matches ` +
- `${target.target} (${target.os} ${target.arch}). Candidates: ` +
- `${ranked.map((r) => `${r.phrase} = ${r.points}`).join("; ")}. Either ` +
- `the sentence was reworded or a spelling belongs in ALIAS.`,
+ `install-targets: ${CLAIM} never names ${target} in prose, so there is ` +
+ `no sentence in the repository to take the name beside its archive ` +
+ `from.`,
);
}
- taken.add(best.phrase);
- return { target: target.target, machine: best.phrase };
-});
+ return match[0];
+};
+const targets = published.map((target) => ({
+ target: target.target,
+ machine: spoken(target.os, target.arch, target.target),
+}));
/* What the installer asks the release for, and where. Kept as the shell wrote
* them, `$version` and `$target` included, so the generated record holds the
@@ -344,39 +387,23 @@ if (!refused.some((line) => line.includes(windowsTarget))) {
);
}
-/* And README.md's statement of it, under "Getting it", where the zip is
+/* And INSTALL.md's statement of it, under "Windows", where the zip is
* spelled with a version rather than a placeholder. The asset template is
* turned into the pattern that file would have to match, so a renamed asset
* fails here instead of becoming a 404 on the download page. */
const spelled = new RegExp(
psAsset[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("\\$Version", "\\d+\\.\\d+\\.\\d+"),
);
-if (!spelled.test(readme)) {
+if (!spelled.test(install)) {
throw new Error(
- `install-targets: ${CLAIM} no longer spells a file of the shape ` +
+ `install-targets: ${INSTALL} no longer spells a file of the shape ` +
`"${psAsset[1]}", so ${WINDOWS} is the only statement of what a release ` +
`publishes for Windows and this script has nothing to cross-check.`,
);
}
-/* The prose name, in README.md's own words. The pattern is built from the
- * target's own components and the aliases above, and what is recorded is the
- * text README.md matched with, so the spelling a reader is shown is the
- * repository's and never this file's. */
-const spell = (part) => ALIAS.get(part) ?? part;
const [windowsOs, windowsArch] = windowsTarget.split("-");
-const spoken = new RegExp(
- `\\b${spell(windowsOs)}[ -]${spell(windowsArch).replace(/[-_]/g, "[-_]")}\\b`,
- "i",
-).exec(readme);
-if (!spoken) {
- throw new Error(
- `install-targets: ${CLAIM} never names ${windowsTarget} in prose, so ` +
- `there is no sentence in the repository to take the name beside the zip ` +
- `from. The other targets take theirs from the sentence under the ` +
- `one-liner; this one is named wherever the README names it.`,
- );
-}
+const windowsMachine = spoken(windowsOs, windowsArch, windowsTarget);
const commit = execFileSync("git", ["-C", repo, "rev-parse", "--short", "HEAD"], {
encoding: "utf8",
@@ -386,7 +413,7 @@ const record = {
provenance: {
generator: "scripts/install-targets.mjs",
source: `${SOURCE}, ${WINDOWS}`,
- claim: CLAIM,
+ claim: `${CLAIM}, ${INSTALL}`,
commit,
},
repo: slug[1],
@@ -401,7 +428,7 @@ const record = {
windows: {
source: WINDOWS,
target: windowsTarget,
- machine: spoken[0],
+ machine: windowsMachine,
asset: psAsset[1],
download: psBase[1].replace("$Repo", slug[1]),
},
diff --git a/scripts/proofs.mjs b/scripts/proofs.mjs
index 19a9c71..bf3b88e 100755
--- a/scripts/proofs.mjs
+++ b/scripts/proofs.mjs
@@ -219,7 +219,7 @@ const ALIAS = new Map([["x86_64-windows-gnu", "x86_64-w64-mingw32"]]);
const auditedHere = audit.values.map((target) => ALIAS.get(target) ?? target);
// And the same gate over the audit set. scripts/targets.mjs derives it from
-// README's sentence about which targets are exempt; this script reads the loop
+// INSTALL.md's sentence about which targets are exempt; this script reads the loop
// itself. Two independent readings of one fact, so a disagreement means one of
// them is wrong rather than that the page has a choice to make.
if (Array.isArray(published.audited)) {
@@ -232,7 +232,7 @@ if (Array.isArray(published.audited)) {
`the audit loop covers ${auditedHere.join(", ")} and ` +
`src/generated/targets.json says ${published.audited.join(", ")}. ` +
`They differ on ${[...new Set(apart)].join(", ")}. One reads the ` +
- `workflow and one reads README.md: a page cannot publish both.`,
+ `workflow and one reads INSTALL.md: a page cannot publish both.`,
);
}
}
diff --git a/scripts/samples.mjs b/scripts/samples.mjs
index ae062cd..d0a86c5 100755
--- a/scripts/samples.mjs
+++ b/scripts/samples.mjs
@@ -196,8 +196,13 @@ for (const [root, count] of [[breakRoot, "break"], [agentRoot, "agent"]]) {
// README recordings --------------------------------------------------------
-const readmePath = resolve(repo, "README.md");
-const readmeLines = readFileSync(readmePath, "utf8").split("\n");
+// README.md, and INSTALL.md beside it: 0.20.1 moved installing by hand out of
+// README.md and into its own file, and the install page quotes those blocks
+// from where they now live.
+const README = "README.md";
+const INSTALL = "INSTALL.md";
+const fileLines = (file) => readFileSync(resolve(repo, file), "utf8").split("\n");
+const readmeLines = fileLines(README);
function fencedBlocks(lines) {
const blocks = [];
@@ -221,13 +226,12 @@ function fencedBlocks(lines) {
return blocks;
}
-// Each entry is a claim that one block in README.md says this. The anchor is
-// matched, never the line number, because a line number is a guess about a file
-// nobody edits with the site in mind.
+// Each entry is a claim that one block in README.md (or the `file` it names)
+// says this. The anchor is matched, never the line number, because a line
+// number is a guess about a file nobody edits with the site in mind.
const WANTED = {
first_run: { lang: "console", anchor: "$ iyi run main.iyi" },
the_rule: { lang: "console", anchor: "rm -r app" },
- library_deleted: { lang: "console", anchor: "rm -rf samples/iyi/kemal" },
non_exhaustive: { lang: "console", anchor: "case is not exhaustive" },
import_missing: { lang: "console", anchor: "is not imported here" },
orphan_rule: { lang: "console", anchor: "an impl must live in the module" },
@@ -239,28 +243,33 @@ const WANTED = {
// run, with no prompt and no output, so the site sets them as commands and
// does not draw a prompt the README never printed. The same on the Windows
// side, where the fence says `powershell` and the two lines are the one
- // install.ps1 runs and the hello it leaves behind.
+ // install.ps1 runs and the hello it leaves behind. The by-hand lines are
+ // INSTALL.md's, set the same way.
install: { lang: "sh", anchor: "install.sh | sh" },
- install_manual: { lang: "sh", anchor: "tar -xzf iyi-" },
+ install_manual: { lang: "sh", anchor: "tar -xzf iyi-", file: INSTALL },
install_windows: { lang: "powershell", anchor: "install.ps1 | iex" },
- install_manual_windows: { lang: "powershell", anchor: "Expand-Archive iyi-" },
+ install_manual_windows: { lang: "powershell", anchor: "Expand-Archive iyi-", file: INSTALL },
};
-const blocks = fencedBlocks(readmeLines);
+const blocksOf = new Map([
+ [README, fencedBlocks(readmeLines)],
+ [INSTALL, fencedBlocks(fileLines(INSTALL))],
+]);
const readme = {};
for (const [id, want] of Object.entries(WANTED)) {
- const hits = blocks.filter((b) => b.text.includes(want.anchor));
+ const file = want.file ?? README;
+ const hits = blocksOf.get(file).filter((b) => b.text.includes(want.anchor));
if (hits.length === 0) {
die(
- `README.md has no fenced block containing "${want.anchor}", which the ` +
+ `${file} has no fenced block containing "${want.anchor}", which the ` +
`lesson "${id}" quotes. Either the passage moved and the anchor needs ` +
- `updating, or the lesson is claiming something the README no longer says.`,
+ `updating, or the lesson is claiming something ${file} no longer says.`,
);
}
if (hits.length > 1) {
die(
- `"${want.anchor}" matches ${hits.length} blocks in README.md ` +
+ `"${want.anchor}" matches ${hits.length} blocks in ${file} ` +
`(lines ${hits.map((h) => h.from).join(", ")}), so "${id}" is ambiguous. ` +
`Narrow the anchor.`,
);
@@ -268,7 +277,7 @@ for (const [id, want] of Object.entries(WANTED)) {
const [hit] = hits;
if (hit.lang !== want.lang) {
die(
- `"${id}" expected a ${want.lang} block and README.md:${hit.from} is ` +
+ `"${id}" expected a ${want.lang} block and ${file}:${hit.from} is ` +
`${hit.lang || "unlabelled"}. Output is quoted as output or not at all.`,
);
}
@@ -276,8 +285,8 @@ for (const [id, want] of Object.entries(WANTED)) {
text: hit.text,
from: hit.from,
to: hit.to,
- source: "README.md",
- cite: `README.md, ${hit.to > hit.from ? `lines ${hit.from} to ${hit.to}` : `line ${hit.from}`}`,
+ source: file,
+ cite: `${file}, ${hit.to > hit.from ? `lines ${hit.from} to ${hit.to}` : `line ${hit.from}`}`,
};
}
diff --git a/scripts/targets.mjs b/scripts/targets.mjs
index 3f9ff92..1ebeae3 100755
--- a/scripts/targets.mjs
+++ b/scripts/targets.mjs
@@ -41,6 +41,9 @@ const out = resolve(site, "src", "generated");
const WORKFLOW = ".github/workflows/iyi.yml";
const CLAIM = "README.md";
+/* Where the object audit is stated. It was README.md's FAQ until 0.20.1,
+ * which moved the targets to INSTALL.md and left the portability row behind. */
+const AUDIT_CLAIM = "INSTALL.md";
/**
* Spellings the repository uses for one thing. A triple says `x86_64` and
@@ -104,23 +107,23 @@ const claimed = (() => {
/**
* The targets whose emitted object CI reads, and the two it does not.
*
- * A third reading of the same file, added because the why page had said "the
+ * A third reading of the same claim, added because the why page had said "the
* rest type-check and have the object they emit audited", which is one claim
- * too strong: README.md's own FAQ says the audit covers seven of the nine and
- * names the two it misses. A page that overclaims a check is the same defect
- * as a page that overstates a figure, and it is worse here, because the check
- * is the thing the page is offering as evidence.
+ * too strong: INSTALL.md's Targets section says the audit covers seven of the
+ * nine and names the two it misses. A page that overclaims a check is the
+ * same defect as a page that overstates a figure, and it is worse here,
+ * because the check is the thing the page is offering as evidence.
*
* The count is cross-checked against the exceptions: seven audited plus two
* named has to be the nine the workflow type-checks, or one of the two
* sentences has moved and neither can be trusted.
*/
const audit = (() => {
- const text = read(CLAIM);
+ const text = read(AUDIT_CLAIM);
const counted = text.match(/audits the emitted object\s+for\s+(\S+)\s+of\s+the\s+nine/);
if (!counted) {
throw new Error(
- `targets: ${CLAIM} no longer says how many triples CI audits the ` +
+ `targets: ${AUDIT_CLAIM} no longer says how many triples CI audits the ` +
`emitted object for, so the site cannot say which of them it does.`,
);
}
@@ -129,7 +132,7 @@ const audit = (() => {
);
if (exempt.length === 0) {
throw new Error(
- `targets: ${CLAIM} says the object audit covers ${counted[1]} triples ` +
+ `targets: ${AUDIT_CLAIM} says the object audit covers ${counted[1]} triples ` +
`and names none of the ones it leaves out. The sentence that named ` +
`them has moved, so the site would be publishing an exception list ` +
`it cannot see.`,
@@ -138,7 +141,7 @@ const audit = (() => {
for (const triple of exempt) {
if (!typechecked.includes(triple)) {
throw new Error(
- `targets: ${CLAIM} exempts ${triple} from the object audit and ` +
+ `targets: ${AUDIT_CLAIM} exempts ${triple} from the object audit and ` +
`${WORKFLOW} does not type-check it at all, so the two files ` +
`disagree about what the platform list even is.`,
);
@@ -229,14 +232,14 @@ if (facts.structural.targets !== typechecked.length) {
);
}
-/* The audit's own arithmetic: the triples it covers plus the ones README
+/* The audit's own arithmetic: the triples it covers plus the ones INSTALL.md
* exempts have to be the whole list. A sentence that said seven while the
* exemption named one would leave a triple in neither set, and the page would
* be silent about it rather than wrong about it, which is harder to notice. */
const audited = typechecked.filter((triple) => !audit.exempt.includes(triple));
if (SPELLED[audited.length] !== audit.count) {
throw new Error(
- `targets: ${CLAIM} says the object audit covers ${audit.count} triples, ` +
+ `targets: ${AUDIT_CLAIM} says the object audit covers ${audit.count} triples, ` +
`and exempting ${audit.exempt.join(" and ")} from ${typechecked.length} ` +
`leaves ${audited.length}. The two sentences no longer agree.`,
);
@@ -256,7 +259,13 @@ writeFileSync(
resolve(out, "targets.json"),
`${JSON.stringify(
{
- provenance: { generator: "scripts/targets.mjs", source: WORKFLOW, claim: CLAIM, commit },
+ provenance: {
+ generator: "scripts/targets.mjs",
+ source: WORKFLOW,
+ claim: CLAIM,
+ auditClaim: AUDIT_CLAIM,
+ commit,
+ },
typechecked,
ran,
audited,
diff --git a/src/content/learn/declarations-not-bodies.mdx b/src/content/learn/declarations-not-bodies.mdx
index 4caaf22..5c49c81 100644
--- a/src/content/learn/declarations-not-bodies.mdx
+++ b/src/content/learn/declarations-not-bodies.mdx
@@ -19,7 +19,7 @@ bought: >-
every push, so the boundary is a gate rather than a claim.
sources:
- file: README.md
- note: the two console sequences, quoted with their lines on each block, and the paragraph under the first one
+ note: the console sequence, quoted with its lines on the block, the paragraph under it, and the sentence that does the same to the Kemal port
- file: samples/iyi/app/greeter.iyi
note: the library the two builds import, shown whole
- file: samples/iyi/modules.iyi
@@ -84,11 +84,11 @@ and no bodies at all. `greeting` is missing from it, because it never carried
Two files is a demonstration. A web framework is a test.
-
- `samples/iyi/webapp.iyi` is a port of Kemal onto iyi's rules, and its router -lives in `samples/iyi/kemal`. That whole directory goes away between the two -builds, and the program still builds, links, runs, and prints the same output. +lives in `samples/iyi/kemal`. README.md takes it through the same three steps: +build with `--emit-iyimod mods`, `rm -rf samples/iyi/kemal`, and build again with +`--use-iyimod mods`. That whole directory goes away between the two builds, and +the program still builds, links, runs, and prints the same output. ## The roundtrip gate @@ -108,8 +108,8 @@ is to take the source away and see whether the build still produces a program that behaves. What that boundary saves in seconds belongs to a machine rather than to a -language, so this page will not quote you a number. README.md publishes them with -the machine attached and the command that measures them. +language, so this page will not quote you a number. BENCHMARKS.md publishes them +with the machine attached and the command that measures them. ## Acyclic imports diff --git a/src/lib/release.ts b/src/lib/release.ts index b4d554c..533285c 100644 --- a/src/lib/release.ts +++ b/src/lib/release.ts @@ -5,7 +5,7 @@ * recorded them. A version number and a target name are facts about the tree * like any other figure here, so neither is typed into a page: one generator * refuses to write a record whose changelog states no release, the other - * refuses one whose installers and README disagree about what a release + * refuses one whose installers and install docs disagree about what a release * ships. * * The site does not publish the changelog itself. So everything here that @@ -53,7 +53,7 @@ export function releaseUrl(release: Release = latestRelease()): string { * * Nothing here is written down twice. `scripts/install-targets.mjs` reads the * targets out of the installer's own `case` arms, cross-checks them against the - * refusal it prints and against README.md, and carries the `asset=` and + * refusal it prints and against README.md and INSTALL.md, and carries the `asset=` and * `base=` strings across with `$version` and `$target` still in them; this * fills those in from the recorded release. The pairs were a literal here * until then, which meant a third published tarball would have appeared diff --git a/src/pages/install.astro b/src/pages/install.astro index e4d3951..0744bbd 100644 --- a/src/pages/install.astro +++ b/src/pages/install.astro @@ -8,7 +8,7 @@ * reader's own first: `src/components/Platform.astro` is the switch and * `src/lib/platform.ts` is what it decides on. * - * Every command on this page is quoted out of README.md by + * Every command on this page is quoted out of README.md or INSTALL.md by * scripts/samples.mjs, and every file name and URL is built from what the two * installers themselves ask a release for, through * scripts/install-targets.mjs. A machine's name is README.md's own word for @@ -177,8 +177,8 @@ const tarballs = assets.map((asset) => asset.target).join(" or ");
Other platforms, or the current tree rather than a release: clone the
repository and make. It needs a Crystal compiler to
- bootstrap from and LLVM; the README's{" "}
- Getting it{" "}
+ bootstrap from and LLVM; INSTALL.md's{" "}
+ From source{" "}
section names the versions and every command.