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>
280 lines
9.2 KiB
Text
280 lines
9.2 KiB
Text
# ============================================================================
|
|
# http.ludic — the Http.* standard library (#6).
|
|
#
|
|
# A poll-based HTTP/HTTPS client. A request is opened, optionally given headers
|
|
# and a body, then sent; each frame the game polls it, and when the reply lands
|
|
# it reads the status, body and headers — the frame never blocks (the request
|
|
# runs on a background thread in the platform backend, http.ll). TLS is the
|
|
# system's, on by default, so an https:// URL just works.
|
|
#
|
|
# HTTP is explicitly OUT-OF-BAND: it depends on the network and wall clock and
|
|
# must never feed the deterministic lockstep/replay simulation — it is for
|
|
# scores, config, telemetry and downloads, exactly like Net.* and Time.now. The
|
|
# JSON companion the proposal called for already shipped as Json.* (#44); pair
|
|
# them: Json.parse(Http.text(h)).
|
|
#
|
|
# The transport (the http_* intrinsics) is macOS-only for now; the response
|
|
# PARSER below (Http.parse and the header lookup) is pure Ludic, so it works on
|
|
# any target and is what the test suite exercises without touching the network.
|
|
# ============================================================================
|
|
|
|
const HTTP_SLOTS: int = 8
|
|
|
|
var h_ready: bool = false
|
|
var h_used: words = null # slot in use
|
|
var h_native: words = null # 1 = backed by the platform fetch, 0 = parsed offline
|
|
var h_sent: words = null # 1 = dispatched (native)
|
|
var h_res: words = null # 1 = a native fetch has been resolved into the fields below
|
|
var h_status: words = null # cached HTTP status (0 = network error / unset)
|
|
var h_blen: words = null # body length
|
|
var h_req: pointers = null # the pending NSURLRequest (native, before send)
|
|
var h_body: pointers = null # body bytes (NUL-terminated); owned by the slot
|
|
var h_hdr: pointers = null # raw header block (parsed handles only); owned by the slot
|
|
|
|
function http_init() -> void {
|
|
if h_ready { return }
|
|
h_used = words(HTTP_SLOTS)
|
|
h_native = words(HTTP_SLOTS)
|
|
h_sent = words(HTTP_SLOTS)
|
|
h_res = words(HTTP_SLOTS)
|
|
h_status = words(HTTP_SLOTS)
|
|
h_blen = words(HTTP_SLOTS)
|
|
h_req = bytes(HTTP_SLOTS * 8)
|
|
h_body = bytes(HTTP_SLOTS * 8)
|
|
h_hdr = bytes(HTTP_SLOTS * 8)
|
|
var i = 0
|
|
while i < HTTP_SLOTS {
|
|
h_used[i] = 0; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
|
|
h_status[i] = 0; h_blen[i] = 0
|
|
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
|
|
i = i + 1
|
|
}
|
|
h_ready = true
|
|
}
|
|
|
|
function http_slot_alloc() -> int {
|
|
http_init()
|
|
var i = 0
|
|
while i < HTTP_SLOTS {
|
|
if h_used[i] == 0 {
|
|
h_used[i] = 1; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
|
|
h_status[i] = 0; h_blen[i] = 0
|
|
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
|
|
return i
|
|
}
|
|
i = i + 1
|
|
}
|
|
return 0 - 1
|
|
}
|
|
|
|
function http_valid(h: int) -> bool {
|
|
http_init()
|
|
if (h < 1) or (h > HTTP_SLOTS) { return false }
|
|
return h_used[h - 1] != 0
|
|
}
|
|
|
|
# ---- length of a NUL-terminated C string -----------------------------------
|
|
function http_cstr_len(p: pointer) -> int {
|
|
if p == null { return 0 }
|
|
var n = 0
|
|
while p[n] != 0 { n = n + 1 }
|
|
return n
|
|
}
|
|
|
|
# ---- request construction (native) -----------------------------------------
|
|
# Open a request without sending it, so headers / a body can be added first.
|
|
function http_open(method: pointer, url: pointer) -> int {
|
|
let req = hs_req_new(method, url)
|
|
if req == null { return 0 }
|
|
let s = http_slot_alloc()
|
|
if s < 0 { return 0 }
|
|
h_req[s] = req
|
|
h_native[s] = 1
|
|
return s + 1
|
|
}
|
|
|
|
function http_set_header(h: int, name: pointer, value: pointer) -> void {
|
|
if not http_valid(h) { return }
|
|
let s = h - 1
|
|
if (h_sent[s] == 0) and (h_req[s] != null) { hs_req_header(h_req[s], name, value) }
|
|
}
|
|
|
|
# Attach a body of `len` bytes (use http_body for a NUL-terminated string body).
|
|
function http_body_n(h: int, bytes_ptr: pointer, len: int) -> void {
|
|
if not http_valid(h) { return }
|
|
let s = h - 1
|
|
if (h_sent[s] == 0) and (h_req[s] != null) { hs_req_body(h_req[s], bytes_ptr, len) }
|
|
}
|
|
|
|
function http_body(h: int, body: pointer) -> void {
|
|
http_body_n(h, body, http_cstr_len(body))
|
|
}
|
|
|
|
# Dispatch the request onto the background worker.
|
|
function http_send_req(h: int) -> void {
|
|
if not http_valid(h) { return }
|
|
let s = h - 1
|
|
if (h_sent[s] == 0) and (h_req[s] != null) {
|
|
hs_send(s, h_req[s])
|
|
h_sent[s] = 1
|
|
}
|
|
}
|
|
|
|
# ---- convenience one-shots -------------------------------------------------
|
|
function http_get(url: pointer) -> int {
|
|
let h = http_open("GET", url)
|
|
if h != 0 { http_send_req(h) }
|
|
return h
|
|
}
|
|
|
|
function http_post(url: pointer, body: pointer) -> int {
|
|
let h = http_open("POST", url)
|
|
if h != 0 { http_body(h, body); http_send_req(h) }
|
|
return h
|
|
}
|
|
|
|
function http_request(method: pointer, url: pointer, body: pointer) -> int {
|
|
let h = http_open(method, url)
|
|
if h == 0 { return 0 }
|
|
if http_cstr_len(body) > 0 { http_body(h, body) }
|
|
http_send_req(h)
|
|
return h
|
|
}
|
|
|
|
# ---- polling + response ----------------------------------------------------
|
|
# -1 while pending, 0 on a transport error, else the HTTP status code.
|
|
function http_poll(h: int) -> int {
|
|
if not http_valid(h) { return 0 }
|
|
let s = h - 1
|
|
if h_native[s] == 0 { return h_status[s] } # a parsed handle is ready immediately
|
|
if h_sent[s] == 0 { return 0 - 1 }
|
|
if h_res[s] == 1 { return h_status[s] }
|
|
if hs_done(s) == 0 { return 0 - 1 }
|
|
# first time we see it finished: cache the fields off the worker.
|
|
h_status[s] = hs_status(s)
|
|
h_body[s] = hs_body(s)
|
|
h_blen[s] = hs_blen(s)
|
|
h_res[s] = 1
|
|
return h_status[s]
|
|
}
|
|
|
|
function http_status_of(h: int) -> int {
|
|
if not http_valid(h) { return 0 }
|
|
return h_status[h - 1]
|
|
}
|
|
|
|
function http_ok(h: int) -> bool {
|
|
let st = http_status_of(h)
|
|
return (st >= 200) and (st < 300)
|
|
}
|
|
|
|
function http_text(h: int) -> pointer {
|
|
if not http_valid(h) { return null }
|
|
return h_body[h - 1]
|
|
}
|
|
|
|
function http_body_len(h: int) -> int {
|
|
if not http_valid(h) { return 0 }
|
|
return h_blen[h - 1]
|
|
}
|
|
|
|
function http_header_of(h: int, name: pointer) -> pointer {
|
|
if not http_valid(h) { return null }
|
|
let s = h - 1
|
|
if h_native[s] == 1 { return hs_header(s, name) }
|
|
return http_find_header(h_hdr[s], name)
|
|
}
|
|
|
|
function http_close(h: int) -> void {
|
|
if not http_valid(h) { return }
|
|
let s = h - 1
|
|
if h_native[s] == 1 { hs_free(s) }
|
|
else {
|
|
if h_body[s] != null { free(h_body[s]) }
|
|
if h_hdr[s] != null { free(h_hdr[s]) }
|
|
}
|
|
h_used[s] = 0
|
|
h_req[s] = null; h_body[s] = null; h_hdr[s] = null
|
|
}
|
|
|
|
# ---- the pure-Ludic response parser (transport-independent) -----------------
|
|
# Copy [a, b) of src into a fresh NUL-terminated buffer.
|
|
function http_slice_dup(src: pointer, a: int, b: int) -> pointer {
|
|
var n = b - a
|
|
if n < 0 { n = 0 }
|
|
let out = bytes(n + 1)
|
|
var i = 0
|
|
while i < n { out[i] = src[a + i]; i = i + 1 }
|
|
out[n] = 0
|
|
return out
|
|
}
|
|
|
|
function http_is_digit(c: int) -> bool { return (c >= 48) and (c <= 57) }
|
|
function http_lower(c: int) -> int { if (c >= 65) and (c <= 90) { return c + 32 }; return c }
|
|
|
|
# Parse a raw HTTP/1.1 response (status line + headers + blank line + body) into
|
|
# a ready handle. Useful for cached/custom transports and offline tests.
|
|
function http_parse(resp: pointer, len: int) -> int {
|
|
let s = http_slot_alloc()
|
|
if s < 0 { return 0 }
|
|
h_native[s] = 0
|
|
|
|
# status: the token after the first space on the status line.
|
|
var i = 0
|
|
while (i < len) and (resp[i] != 32) { i = i + 1 } # skip "HTTP/1.1"
|
|
while (i < len) and (resp[i] == 32) { i = i + 1 } # skip the space(s)
|
|
var st = 0
|
|
while (i < len) and http_is_digit(resp[i]) { st = (st * 10) + (resp[i] - 48); i = i + 1 }
|
|
h_status[s] = st
|
|
|
|
# header/body split at the first CRLFCRLF.
|
|
var split = 0 - 1
|
|
var j = 0
|
|
while (j + 3) < len {
|
|
if (resp[j] == 13) and (resp[j + 1] == 10) and (resp[j + 2] == 13) and (resp[j + 3] == 10) {
|
|
split = j
|
|
j = len
|
|
} else { j = j + 1 }
|
|
}
|
|
var hdr_end = split
|
|
var body_start = split + 4
|
|
if split < 0 { hdr_end = len; body_start = len } # headers only, no body
|
|
|
|
# header block starts after the status line's CRLF.
|
|
var hs = 0
|
|
while (hs + 1) < len {
|
|
if (resp[hs] == 13) and (resp[hs + 1] == 10) { hs = hs + 2; break }
|
|
hs = hs + 1
|
|
}
|
|
if hs > hdr_end { hs = hdr_end }
|
|
h_hdr[s] = http_slice_dup(resp, hs, hdr_end)
|
|
h_body[s] = http_slice_dup(resp, body_start, len)
|
|
h_blen[s] = len - body_start
|
|
if h_blen[s] < 0 { h_blen[s] = 0 }
|
|
return s + 1
|
|
}
|
|
|
|
# Look up a header value in a raw header block (case-insensitive), returning a
|
|
# fresh NUL-terminated copy, or null if absent. Lines are "Name: value\r\n".
|
|
function http_find_header(hdr: pointer, name: pointer) -> pointer {
|
|
if hdr == null { return null }
|
|
let nlen = http_cstr_len(name)
|
|
let hlen = http_cstr_len(hdr)
|
|
var i = 0
|
|
while i < hlen {
|
|
# match name at the start of a line, followed by ':'.
|
|
var k = 0
|
|
while (k < nlen) and ((i + k) < hlen) and (http_lower(hdr[i + k]) == http_lower(name[k])) { k = k + 1 }
|
|
if (k == nlen) and ((i + k) < hlen) and (hdr[i + k] == 58) {
|
|
var v = i + k + 1
|
|
while (v < hlen) and (hdr[v] == 32) { v = v + 1 } # skip spaces after ':'
|
|
var e = v
|
|
while (e < hlen) and (hdr[e] != 13) and (hdr[e] != 10) { e = e + 1 }
|
|
return http_slice_dup(hdr, v, e)
|
|
}
|
|
# advance to the next line.
|
|
while (i < hlen) and (hdr[i] != 10) { i = i + 1 }
|
|
i = i + 1
|
|
}
|
|
return null
|
|
}
|