feat(http): Http.* poll-based HTTP/HTTPS client over a native NSURLConnection backend (#6)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m26s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 21s

Adds the Http.* namespace and its transport, the HTTP client from #6. The JSON
companion the proposal called for already shipped as Json.* (#44).

- runtime/native/http.ll: the macOS transport — NSURLConnection driven through
  the objc runtime C ABI (no ObjC/C source), run on a detached pthread so the
  frame never blocks; TLS is the system's, on by default. A fixed slot pool holds
  each in-flight request; the worker publishes status/body/response behind an
  atomic done flag (release/acquire). Spliced + Foundation linked only when a
  program uses Http.*.
- runtime/native/http.ludic: the Http.* runtime — get/post/request, the
  open/set/body/send builder, poll/status/ok/text/body_len/header/free, plus a
  pure-Ludic response parser (Http.parse + case-insensitive header lookup) that
  is transport-independent and portable.
- compiler: Http.* dispatch, g_uses_http splice, hs_* transport intrinsics +
  declarations, the conditional Foundation link, and a new \r string/char escape
  the protocol needs.
- docs: a full docs/language/http section (16 pages); check-impl/check-docs green.
- test: examples/library/http.ludic self-asserts the parser offline (Darwin-gated
  build via the canonical path, since it links Foundation).

Verified end to end against real endpoints: HTTPS GET (200 + headers + body) and
POST (body + custom header). HTTP is out-of-band and never feeds the
deterministic sim. Reseeded; suites green (81 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 18:10:53 +03:00
parent 4eef5ebbce
commit 3df6640fa5
29 changed files with 24936 additions and 22396 deletions

View file

@ -0,0 +1,9 @@
---
id: http
title: Http
order: 8
---
A poll-based HTTP / HTTPS client for out-of-band data — leaderboards, cloud saves, remote config, telemetry, downloads. Open a request with <a href="http-get.html"><code>Http.get</code></a> / <a href="http-post.html"><code>Http.post</code></a> (or <a href="http-open.html"><code>Http.open</code></a> to add headers first), then each frame call <a href="http-poll.html"><code>Http.poll</code></a> — it returns <code>-1</code> while pending, <code>0</code> on error, or the status code — so the frame never blocks. Read the reply with <a href="http-status.html"><code>Http.status</code></a> / <a href="http-ok.html"><code>ok</code></a> / <a href="http-text.html"><code>text</code></a> / <a href="http-header.html"><code>header</code></a>, and pair it with <code>Json.*</code> for (de)serialization. TLS is the system's, on by default.
HTTP depends on the network and the wall clock, so — like <code>Net.*</code> and <code>Time.now</code> — it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation. The transport is macOS-only for now; the raw-response parser (<a href="http-parse.html"><code>Http.parse</code></a>) is pure and portable.

View file

@ -0,0 +1,24 @@
---
id: http-body
name: Http.body
category: http
kind: namespace-method
tokens: Http.body
sig: Http.body(handle, body) -> void
tip: Set a string request body.
order: 5
ns: Http
member: body
---
Sets a NUL-terminated string request body on an opened request.
```ludic
program Demo {
entry {
let h = Http.open("POST", "https://example.com/api")
Http.body(h, "{\"hi\":1}")
Http.send(h)
}
}
```

View file

@ -0,0 +1,25 @@
---
id: http-body_bytes
name: Http.body_bytes
category: http
kind: namespace-method
tokens: Http.body_bytes
sig: Http.body_bytes(handle, bytes, len) -> void
tip: Set a raw byte request body.
order: 6
ns: Http
member: body_bytes
---
Sets a raw request body of <code>len</code> bytes (for binary uploads).
```ludic
program Demo {
entry {
let buf = bytes(4)
let h = Http.open("POST", "https://example.com/upload")
Http.body_bytes(h, buf, 4)
Http.send(h)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: http-body_len
name: Http.body_len
category: http
kind: namespace-method
tokens: Http.body_len
sig: Http.body_len(handle) -> int
tip: The response body length in bytes.
order: 12
ns: Http
member: body_len
---
Returns the length of the response body in bytes.
```ludic
program Demo {
entry {
let h = Http.get("https://example.com")
if Http.poll(h) > 0 { print(Http.body_len(h)) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: http-free
name: Http.free
category: http
kind: namespace-method
tokens: Http.free
sig: Http.free(handle) -> void
tip: Release a request's resources.
order: 14
ns: Http
member: free
---
Releases a request handle and the resources it owns (the body copy and any retained response). Free each request once you are done reading it.
```ludic
program Demo {
entry {
let h = Http.get("https://example.com")
if Http.poll(h) > 0 { Http.free(h) }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: http-get
name: Http.get
category: http
kind: namespace-method
tokens: Http.get
sig: Http.get(url) -> int
tip: Start an async GET request.
order: 0
ns: Http
member: get
---
Starts an asynchronous GET and returns a request handle. Poll it each frame with <a href="http-poll.html"><code>Http.poll</code></a>; the frame never blocks.
```ludic
program Demo {
entry {
let h = Http.get("https://example.com/scores")
}
}
```

View file

@ -0,0 +1,23 @@
---
id: http-header
name: Http.header
category: http
kind: namespace-method
tokens: Http.header
sig: Http.header(handle, name) -> str
tip: A response header value (case-insensitive).
order: 13
ns: Http
member: header
---
Returns a response header value by name (case-insensitive), or an empty result if absent.
```ludic
program Demo {
entry {
let h = Http.get("https://example.com")
if Http.poll(h) > 0 { print(Http.header(h, "Content-Type")) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: http-ok
name: Http.ok
category: http
kind: namespace-method
tokens: Http.ok
sig: Http.ok(handle) -> bool
tip: Was the response status 2xx?
order: 10
ns: Http
member: ok
---
Returns whether the response status is in the 2xx success range.
```ludic
program Demo {
entry {
let h = Http.get("https://example.com")
if Http.poll(h) > 0 { if Http.ok(h) { print(1) } }
}
}
```

View file

@ -0,0 +1,25 @@
---
id: http-open
name: Http.open
category: http
kind: namespace-method
tokens: Http.open
sig: Http.open(method, url) -> int
tip: Open a request to configure before sending.
order: 3
ns: Http
member: open
---
Opens a request without sending it, so headers and a body can be added first with <a href="http-set.html"><code>Http.set</code></a> / <a href="http-body.html"><code>Http.body</code></a>, then dispatched with <a href="http-send.html"><code>Http.send</code></a>.
```ludic
program Demo {
entry {
let h = Http.open("POST", "https://example.com/api")
Http.set(h, "Authorization", "Bearer token")
Http.body(h, "{}")
Http.send(h)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: http-parse
name: Http.parse
category: http
kind: namespace-method
tokens: Http.parse
sig: Http.parse(bytes, len) -> int
tip: Parse a raw HTTP response into a handle.
order: 15
ns: Http
member: parse
---
Parses a raw HTTP/1.1 response (status line + headers + body) into a ready handle — useful for caches, custom transports, and tests. Transport-independent and pure.
```ludic
program Demo {
entry {
let raw = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhi"
let h = Http.parse(raw, Text.length(raw))
print(Http.status(h))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: http-poll
name: Http.poll
category: http
kind: namespace-method
tokens: Http.poll
sig: Http.poll(handle) -> int
tip: Poll a request; -1 pending, 0 error, else status.
order: 8
ns: Http
member: poll
---
Polls a request without blocking: returns <code>-1</code> while pending, <code>0</code> on a transport error, else the HTTP status code once the reply lands.
```ludic
program Demo {
entry {
let h = Http.get("https://example.com")
let st = Http.poll(h)
if st > 0 { print(Http.status(h)) }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: http-post
name: Http.post
category: http
kind: namespace-method
tokens: Http.post
sig: Http.post(url, body) -> int
tip: Start an async POST with a body.
order: 1
ns: Http
member: post
---
Starts an asynchronous POST with a string body, returning a request handle.
```ludic
program Demo {
entry {
let h = Http.post("https://example.com/score", "{\"score\":42}")
}
}
```

View file

@ -0,0 +1,22 @@
---
id: http-request
name: Http.request
category: http
kind: namespace-method
tokens: Http.request
sig: Http.request(method, url, body) -> int
tip: Start a request with any method.
order: 2
ns: Http
member: request
---
Starts a request with an explicit method ("GET", "PUT", "DELETE", …) and optional body.
```ludic
program Demo {
entry {
let h = Http.request("PUT", "https://example.com/item/1", "payload")
}
}
```

View file

@ -0,0 +1,23 @@
---
id: http-send
name: Http.send
category: http
kind: namespace-method
tokens: Http.send
sig: Http.send(handle) -> void
tip: Dispatch an opened request.
order: 7
ns: Http
member: send
---
Dispatches an opened request onto the background worker. After this the request is in flight; poll it.
```ludic
program Demo {
entry {
let h = Http.open("GET", "https://example.com")
Http.send(h)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: http-set
name: Http.set
category: http
kind: namespace-method
tokens: Http.set
sig: Http.set(handle, name, value) -> void
tip: Set a request header before sending.
order: 4
ns: Http
member: set
---
Sets a request header on an opened request (before <a href="http-send.html"><code>Http.send</code></a>).
```ludic
program Demo {
entry {
let h = Http.open("GET", "https://example.com")
Http.set(h, "Accept", "application/json")
Http.send(h)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: http-status
name: Http.status
category: http
kind: namespace-method
tokens: Http.status
sig: Http.status(handle) -> int
tip: The response HTTP status code.
order: 9
ns: Http
member: status
---
Returns the cached HTTP status code (<code>0</code> until the reply lands or on error).
```ludic
program Demo {
entry {
let h = Http.get("https://example.com")
if Http.poll(h) > 0 { print(Http.status(h)) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: http-text
name: Http.text
category: http
kind: namespace-method
tokens: Http.text
sig: Http.text(handle) -> str
tip: The response body as text.
order: 11
ns: Http
member: text
---
Returns the response body as a NUL-terminated string. Pair with <code>Json.parse</code> (#44) for JSON APIs.
```ludic
program Demo {
entry {
let h = Http.get("https://example.com")
if Http.poll(h) > 0 { print(Http.text(h)) }
}
}
```