feat(http): Http.* poll-based HTTP/HTTPS client over a native NSURLConnection backend (#6)
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:
parent
4eef5ebbce
commit
3df6640fa5
29 changed files with 24936 additions and 22396 deletions
20
changes/http-namespace.md
Normal file
20
changes/http-namespace.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**HTTP client standard library (#6).** `Http.*` — a poll-based HTTP/HTTPS client
|
||||
for out-of-band data (leaderboards, cloud saves, remote config, telemetry,
|
||||
downloads). `Http.get` / `Http.post` / `Http.request` start a request (or
|
||||
`Http.open` + `Http.set` headers + `Http.body` + `Http.send` to build one up);
|
||||
`Http.poll` returns `-1` while pending, `0` on error, else the status code, so
|
||||
the frame never blocks; `Http.status` / `ok` / `text` / `body_len` / `header`
|
||||
read the reply, and `Http.free` releases it. TLS is the system's, on by default
|
||||
with certificate verification — an `https://` URL just works. Pairs with the
|
||||
`Json.*` companion (already shipped in #44): `Json.parse(Http.text(h))`.
|
||||
|
||||
The transport (`runtime/native/http.ll`) drives NSURLConnection through the objc
|
||||
runtime's C ABI on a detached pthread — same hand-written-IR, no-ObjC/no-C style
|
||||
as cocoa.ll — and is spliced with Foundation only when a program uses `Http.*`
|
||||
(macOS for now). The response parser (`Http.parse`, header lookup) is pure Ludic
|
||||
and portable, so the suite exercises it offline with no network. HTTP depends on
|
||||
the network and wall clock and is explicitly out-of-band: it must never feed the
|
||||
deterministic lockstep/replay simulation, like `Net.*` and `Time.now`. Also adds
|
||||
the `\r` string/char escape the protocol needs.
|
||||
9
docs/language/http/_section.md
Normal file
9
docs/language/http/_section.md
Normal 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.
|
||||
24
docs/language/http/http-body.md
Normal file
24
docs/language/http/http-body.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/http/http-body_bytes.md
Normal file
25
docs/language/http/http-body_bytes.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/http/http-body_len.md
Normal file
23
docs/language/http/http-body_len.md
Normal 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)) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/http/http-free.md
Normal file
23
docs/language/http/http-free.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/http/http-get.md
Normal file
22
docs/language/http/http-get.md
Normal 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")
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/http/http-header.md
Normal file
23
docs/language/http/http-header.md
Normal 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")) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/http/http-ok.md
Normal file
23
docs/language/http/http-ok.md
Normal 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) } }
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/http/http-open.md
Normal file
25
docs/language/http/http-open.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/http/http-parse.md
Normal file
24
docs/language/http/http-parse.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/http/http-poll.md
Normal file
24
docs/language/http/http-poll.md
Normal 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)) }
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/http/http-post.md
Normal file
22
docs/language/http/http-post.md
Normal 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}")
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/http/http-request.md
Normal file
22
docs/language/http/http-request.md
Normal 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")
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/http/http-send.md
Normal file
23
docs/language/http/http-send.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/http/http-set.md
Normal file
24
docs/language/http/http-set.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/http/http-status.md
Normal file
23
docs/language/http/http-status.md
Normal 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)) }
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/http/http-text.md
Normal file
23
docs/language/http/http-text.md
Normal 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)) }
|
||||
}
|
||||
}
|
||||
```
|
||||
39
examples/library/http.ludic
Normal file
39
examples/library/http.ludic
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# http.ludic — the Http.* standard library (#6). A poll-based HTTP/HTTPS client:
|
||||
#
|
||||
# let h = Http.get("https://api.example.com/scores?level=3")
|
||||
# handler Update { # each frame, never blocking
|
||||
# let st = Http.poll(h) # -1 pending, 0 error, else HTTP status
|
||||
# if st > 0 {
|
||||
# if Http.ok(h) { let body = Http.text(h) } # pair with Json.parse(...) (#44)
|
||||
# Http.free(h)
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# TLS is the system's, on by default, so https:// just works. HTTP is out-of-band
|
||||
# and must never feed the deterministic sim (like Net.* and Time.now).
|
||||
#
|
||||
# The transport runs on a background thread (macOS backend, http.ll) so it can't
|
||||
# be driven deterministically here; the response PARSER is pure Ludic, so this
|
||||
# self-checking run exercises Http.parse / status / ok / body / headers offline:
|
||||
# bin/ludic examples/library/http.ludic -> 200 1 5 hello 42 text/plain 404 0
|
||||
program HttpDemo {
|
||||
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||
|
||||
entry {
|
||||
let raw = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nX-Answer: 42\r\n\r\nhello"
|
||||
let h = Http.parse(raw, Text.length(raw))
|
||||
print(Http.status(h)) # 200
|
||||
print(bi(Http.ok(h))) # 1 — 2xx
|
||||
print(Http.body_len(h)) # 5
|
||||
print(Http.text(h)) # hello
|
||||
print(Http.header(h, "X-Answer")) # 42
|
||||
print(Http.header(h, "content-type")) # text/plain — header lookup is case-insensitive
|
||||
Http.free(h)
|
||||
|
||||
let nf = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"
|
||||
let h2 = Http.parse(nf, Text.length(nf))
|
||||
print(Http.status(h2)) # 404
|
||||
print(bi(Http.ok(h2))) # 0 — not 2xx
|
||||
Http.free(h2)
|
||||
}
|
||||
}
|
||||
306
runtime/native/http.ll
Normal file
306
runtime/native/http.ll
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
; ============================================================================
|
||||
; http.ll — the macOS HTTP transport, written in LLVM IR.
|
||||
;
|
||||
; Like cocoa.ll / audio.ll, no Objective-C and no C source: this drives
|
||||
; NSURLConnection through the objc runtime's C ABI, and runs the (blocking)
|
||||
; request on a detached pthread so the game's frame loop never stalls. TLS is
|
||||
; the system's, on by default with certificate verification — an https:// URL
|
||||
; just works. ludicc splices this in and links Foundation only when a program
|
||||
; uses Http.*.
|
||||
;
|
||||
; A small fixed pool of slots (0..HTTP_SLOTS-1) each hold one in-flight request:
|
||||
; the pending NSURLRequest, the worker's pthread id, and — once the worker
|
||||
; finishes — the status code, a malloc'd copy of the body bytes, and the
|
||||
; retained NSHTTPURLResponse (so header queries can be answered later). The done
|
||||
; flag is an atomic released by the worker and acquired by the poller, so the
|
||||
; body / status / response the poller reads are the ones the worker wrote.
|
||||
;
|
||||
; Exposed to Ludic (through the http_* intrinsics):
|
||||
; http_req_new(method, url) -> req http_req_header(req, name, val)
|
||||
; http_req_body(req, bytes, len) http_send(slot, req)
|
||||
; http_done(slot) -> bool http_status(slot) -> int
|
||||
; http_body(slot) -> ptr http_blen(slot) -> int
|
||||
; http_header(slot, name) -> cstr http_free(slot)
|
||||
; ============================================================================
|
||||
|
||||
declare ptr @objc_getClass(ptr)
|
||||
declare ptr @sel_registerName(ptr)
|
||||
declare ptr @objc_msgSend(ptr, ptr, ...)
|
||||
declare ptr @malloc(i64)
|
||||
declare void @free(ptr)
|
||||
declare ptr @memcpy(ptr, ptr, i64)
|
||||
declare i32 @pthread_create(ptr, ptr, ptr, ptr)
|
||||
declare i32 @pthread_detach(ptr)
|
||||
|
||||
@.h_nsurl = private unnamed_addr constant [6 x i8] c"NSURL\00"
|
||||
@.h_nsstr = private unnamed_addr constant [9 x i8] c"NSString\00"
|
||||
@.h_nsdata = private unnamed_addr constant [7 x i8] c"NSData\00"
|
||||
@.h_murl = private unnamed_addr constant [20 x i8] c"NSMutableURLRequest\00"
|
||||
@.h_conn = private unnamed_addr constant [16 x i8] c"NSURLConnection\00"
|
||||
@.h_pool = private unnamed_addr constant [18 x i8] c"NSAutoreleasePool\00"
|
||||
|
||||
@.h_urlws = private unnamed_addr constant [15 x i8] c"URLWithString:\00"
|
||||
@.h_utf8 = private unnamed_addr constant [22 x i8] c"stringWithUTF8String:\00"
|
||||
@.h_reqw = private unnamed_addr constant [16 x i8] c"requestWithURL:\00"
|
||||
@.h_smeth = private unnamed_addr constant [15 x i8] c"setHTTPMethod:\00"
|
||||
@.h_shdr = private unnamed_addr constant [29 x i8] c"setValue:forHTTPHeaderField:\00"
|
||||
@.h_sbody = private unnamed_addr constant [13 x i8] c"setHTTPBody:\00"
|
||||
@.h_dwbl = private unnamed_addr constant [22 x i8] c"dataWithBytes:length:\00"
|
||||
@.h_bytes = private unnamed_addr constant [6 x i8] c"bytes\00"
|
||||
@.h_length = private unnamed_addr constant [7 x i8] c"length\00"
|
||||
@.h_ssync = private unnamed_addr constant [48 x i8] c"sendSynchronousRequest:returningResponse:error:\00"
|
||||
@.h_status = private unnamed_addr constant [11 x i8] c"statusCode\00"
|
||||
@.h_vhdr = private unnamed_addr constant [25 x i8] c"valueForHTTPHeaderField:\00"
|
||||
@.h_utf8g = private unnamed_addr constant [11 x i8] c"UTF8String\00"
|
||||
@.h_alloc = private unnamed_addr constant [6 x i8] c"alloc\00"
|
||||
@.h_init = private unnamed_addr constant [5 x i8] c"init\00"
|
||||
@.h_drain = private unnamed_addr constant [6 x i8] c"drain\00"
|
||||
@.h_retain = private unnamed_addr constant [7 x i8] c"retain\00"
|
||||
@.h_rel = private unnamed_addr constant [8 x i8] c"release\00"
|
||||
@.h_empty = private unnamed_addr constant [1 x i8] c"\00"
|
||||
|
||||
; HTTP_SLOTS = 8
|
||||
@H_req = internal global [8 x ptr] zeroinitializer
|
||||
@H_resp = internal global [8 x ptr] zeroinitializer
|
||||
@H_body = internal global [8 x ptr] zeroinitializer
|
||||
@H_blen = internal global [8 x i32] zeroinitializer
|
||||
@H_status = internal global [8 x i32] zeroinitializer
|
||||
@H_done = internal global [8 x i32] zeroinitializer
|
||||
|
||||
; --- request construction ---------------------------------------------------
|
||||
|
||||
; Build an NSMutableURLRequest for `method` + `url`; returns it retained.
|
||||
define ptr @hs_req_new(ptr %method, ptr %url) {
|
||||
entry:
|
||||
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
|
||||
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
|
||||
%nsurlstr = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %url)
|
||||
%urlcls = call ptr @objc_getClass(ptr @.h_nsurl)
|
||||
%sel_uw = call ptr @sel_registerName(ptr @.h_urlws)
|
||||
%nsurl = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %urlcls, ptr %sel_uw, ptr %nsurlstr)
|
||||
%nou = icmp eq ptr %nsurl, null
|
||||
br i1 %nou, label %fail, label %mk
|
||||
mk:
|
||||
%reqcls = call ptr @objc_getClass(ptr @.h_murl)
|
||||
%sel_rw = call ptr @sel_registerName(ptr @.h_reqw)
|
||||
%req = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %reqcls, ptr %sel_rw, ptr %nsurl)
|
||||
%nor = icmp eq ptr %req, null
|
||||
br i1 %nor, label %fail, label %setm
|
||||
setm:
|
||||
%mstr = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %method)
|
||||
%sel_m = call ptr @sel_registerName(ptr @.h_smeth)
|
||||
%rm = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_m, ptr %mstr)
|
||||
; retain so it survives the caller's autorelease context until http_send.
|
||||
%sel_ret = call ptr @sel_registerName(ptr @.h_retain)
|
||||
%rr = call ptr (ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_ret)
|
||||
ret ptr %req
|
||||
fail:
|
||||
ret ptr null
|
||||
}
|
||||
|
||||
define void @hs_req_header(ptr %req, ptr %name, ptr %val) {
|
||||
entry:
|
||||
%nil = icmp eq ptr %req, null
|
||||
br i1 %nil, label %out, label %go
|
||||
go:
|
||||
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
|
||||
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
|
||||
%n = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %name)
|
||||
%v = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %val)
|
||||
%sel_sh = call ptr @sel_registerName(ptr @.h_shdr)
|
||||
%r = call ptr (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_sh, ptr %v, ptr %n)
|
||||
br label %out
|
||||
out:
|
||||
ret void
|
||||
}
|
||||
|
||||
define void @hs_req_body(ptr %req, ptr %bytes, i32 %len) {
|
||||
entry:
|
||||
%nil = icmp eq ptr %req, null
|
||||
br i1 %nil, label %out, label %go
|
||||
go:
|
||||
%datacls = call ptr @objc_getClass(ptr @.h_nsdata)
|
||||
%sel_d = call ptr @sel_registerName(ptr @.h_dwbl)
|
||||
%l64 = sext i32 %len to i64
|
||||
%data = call ptr (ptr, ptr, ptr, i64) @objc_msgSend(ptr %datacls, ptr %sel_d, ptr %bytes, i64 %l64)
|
||||
%sel_sb = call ptr @sel_registerName(ptr @.h_sbody)
|
||||
%r = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_sb, ptr %data)
|
||||
br label %out
|
||||
out:
|
||||
ret void
|
||||
}
|
||||
|
||||
; --- the worker + dispatch --------------------------------------------------
|
||||
|
||||
; The detached worker: run the blocking request for one slot and publish the
|
||||
; result. arg is the slot index encoded as a pointer.
|
||||
define ptr @hs_worker(ptr %arg) {
|
||||
entry:
|
||||
%slot64 = ptrtoint ptr %arg to i64
|
||||
%slot = trunc i64 %slot64 to i32
|
||||
%poolcls = call ptr @objc_getClass(ptr @.h_pool)
|
||||
%sel_a = call ptr @sel_registerName(ptr @.h_alloc)
|
||||
%p0 = call ptr (ptr, ptr) @objc_msgSend(ptr %poolcls, ptr %sel_a)
|
||||
%sel_i = call ptr @sel_registerName(ptr @.h_init)
|
||||
%pool = call ptr (ptr, ptr) @objc_msgSend(ptr %p0, ptr %sel_i)
|
||||
|
||||
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
|
||||
%req = load ptr, ptr %reqp
|
||||
|
||||
%respslot = alloca ptr
|
||||
%errslot = alloca ptr
|
||||
store ptr null, ptr %respslot
|
||||
store ptr null, ptr %errslot
|
||||
%conncls = call ptr @objc_getClass(ptr @.h_conn)
|
||||
%sel_ss = call ptr @sel_registerName(ptr @.h_ssync)
|
||||
%data = call ptr (ptr, ptr, ptr, ptr, ptr) @objc_msgSend(ptr %conncls, ptr %sel_ss, ptr %req, ptr %respslot, ptr %errslot)
|
||||
|
||||
%nodata = icmp eq ptr %data, null
|
||||
br i1 %nodata, label %publish, label %havebody
|
||||
havebody:
|
||||
%sel_len = call ptr @sel_registerName(ptr @.h_length)
|
||||
%blen64 = call i64 (ptr, ptr) @objc_msgSend(ptr %data, ptr %sel_len)
|
||||
%blen = trunc i64 %blen64 to i32
|
||||
%sel_by = call ptr @sel_registerName(ptr @.h_bytes)
|
||||
%src = call ptr (ptr, ptr) @objc_msgSend(ptr %data, ptr %sel_by)
|
||||
; malloc blen+1 and copy, NUL-terminating so the Ludic side can read text.
|
||||
%cap = add i64 %blen64, 1
|
||||
%buf = call ptr @malloc(i64 %cap)
|
||||
%cp = call ptr @memcpy(ptr %buf, ptr %src, i64 %blen64)
|
||||
%term = getelementptr i8, ptr %buf, i64 %blen64
|
||||
store i8 0, ptr %term
|
||||
%bodyp = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
|
||||
store ptr %buf, ptr %bodyp
|
||||
%blenp = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
|
||||
store i32 %blen, ptr %blenp
|
||||
br label %publish
|
||||
publish:
|
||||
; status + retained response (for later header queries), from the out-param.
|
||||
%resp = load ptr, ptr %respslot
|
||||
%noresp = icmp eq ptr %resp, null
|
||||
br i1 %noresp, label %done, label %withresp
|
||||
withresp:
|
||||
%sel_ret = call ptr @sel_registerName(ptr @.h_retain)
|
||||
%rr = call ptr (ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_ret)
|
||||
%respp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
|
||||
store ptr %resp, ptr %respp
|
||||
%sel_st = call ptr @sel_registerName(ptr @.h_status)
|
||||
%st64 = call i64 (ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_st)
|
||||
%st = trunc i64 %st64 to i32
|
||||
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
|
||||
store i32 %st, ptr %stp
|
||||
br label %done
|
||||
done:
|
||||
; release the request we retained in http_send, drain the pool, then publish
|
||||
; the done flag last with release ordering so the reader sees the writes above.
|
||||
%sel_rel = call ptr @sel_registerName(ptr @.h_rel)
|
||||
%rrel = call ptr (ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_rel)
|
||||
%sel_dr = call ptr @sel_registerName(ptr @.h_drain)
|
||||
%dr = call ptr (ptr, ptr) @objc_msgSend(ptr %pool, ptr %sel_dr)
|
||||
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
|
||||
store atomic i32 1, ptr %donep release, align 4
|
||||
ret ptr null
|
||||
}
|
||||
|
||||
; Start the request on `slot` running on a detached worker thread.
|
||||
define void @hs_send(i32 %slot, ptr %req) {
|
||||
entry:
|
||||
; reset the slot
|
||||
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
|
||||
store atomic i32 0, ptr %donep release, align 4
|
||||
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
|
||||
store i32 0, ptr %stp
|
||||
%bodyp = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
|
||||
store ptr null, ptr %bodyp
|
||||
%blenp = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
|
||||
store i32 0, ptr %blenp
|
||||
%respp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
|
||||
store ptr null, ptr %respp
|
||||
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
|
||||
store ptr %req, ptr %reqp
|
||||
%tid = alloca i64
|
||||
%arg = inttoptr i32 %slot to ptr
|
||||
%rc = call i32 @pthread_create(ptr %tid, ptr null, ptr @hs_worker, ptr %arg)
|
||||
%t = load i64, ptr %tid
|
||||
%tp = inttoptr i64 %t to ptr
|
||||
%d = call i32 @pthread_detach(ptr %tp)
|
||||
ret void
|
||||
}
|
||||
|
||||
; --- result accessors -------------------------------------------------------
|
||||
|
||||
define i32 @hs_done(i32 %slot) {
|
||||
entry:
|
||||
%p = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
|
||||
%v = load atomic i32, ptr %p acquire, align 4
|
||||
ret i32 %v
|
||||
}
|
||||
|
||||
define i32 @hs_status(i32 %slot) {
|
||||
entry:
|
||||
%p = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
|
||||
%v = load i32, ptr %p
|
||||
ret i32 %v
|
||||
}
|
||||
|
||||
define ptr @hs_body(i32 %slot) {
|
||||
entry:
|
||||
%p = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
|
||||
%v = load ptr, ptr %p
|
||||
ret ptr %v
|
||||
}
|
||||
|
||||
define i32 @hs_blen(i32 %slot) {
|
||||
entry:
|
||||
%p = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
|
||||
%v = load i32, ptr %p
|
||||
ret i32 %v
|
||||
}
|
||||
|
||||
; A response header value as a C string, or "" if absent / no response yet.
|
||||
define ptr @hs_header(i32 %slot, ptr %name) {
|
||||
entry:
|
||||
%rp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
|
||||
%resp = load ptr, ptr %rp
|
||||
%nores = icmp eq ptr %resp, null
|
||||
br i1 %nores, label %empty, label %ask
|
||||
ask:
|
||||
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
|
||||
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
|
||||
%ns = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %name)
|
||||
%sel_vh = call ptr @sel_registerName(ptr @.h_vhdr)
|
||||
%val = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_vh, ptr %ns)
|
||||
%noval = icmp eq ptr %val, null
|
||||
br i1 %noval, label %empty, label %tocstr
|
||||
tocstr:
|
||||
%sel_g = call ptr @sel_registerName(ptr @.h_utf8g)
|
||||
%c = call ptr (ptr, ptr) @objc_msgSend(ptr %val, ptr %sel_g)
|
||||
ret ptr %c
|
||||
empty:
|
||||
ret ptr @.h_empty
|
||||
}
|
||||
|
||||
; Free a slot's owned resources (the body copy and the retained response).
|
||||
define void @hs_free(i32 %slot) {
|
||||
entry:
|
||||
%bp = getelementptr [8 x ptr], ptr @H_body, i32 0, i32 %slot
|
||||
%b = load ptr, ptr %bp
|
||||
%nb = icmp eq ptr %b, null
|
||||
br i1 %nb, label %resp, label %fb
|
||||
fb:
|
||||
call void @free(ptr %b)
|
||||
store ptr null, ptr %bp
|
||||
br label %resp
|
||||
resp:
|
||||
%rp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
|
||||
%r = load ptr, ptr %rp
|
||||
%nr = icmp eq ptr %r, null
|
||||
br i1 %nr, label %out, label %rr
|
||||
rr:
|
||||
%sel_rel = call ptr @sel_registerName(ptr @.h_rel)
|
||||
%x = call ptr (ptr, ptr) @objc_msgSend(ptr %r, ptr %sel_rel)
|
||||
store ptr null, ptr %rp
|
||||
br label %out
|
||||
out:
|
||||
ret void
|
||||
}
|
||||
280
runtime/native/http.ludic
Normal file
280
runtime/native/http.ludic
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
# ============================================================================
|
||||
# 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
|
||||
}
|
||||
|
|
@ -231,6 +231,27 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "pitch") { bare = "audio_pitch"; push(labels, "v") }
|
||||
if (meth == "is_playing") { bare = "audio_is_playing"; push(labels, "id") }
|
||||
}
|
||||
# Http.* (#6) — a poll-based HTTP/HTTPS client (runtime/native/http.ludic +
|
||||
# http.ll). Out-of-band, never part of the deterministic sim. Pairs with Json.*
|
||||
# (#44) for (de)serialization: Json.parse(Http.text(h)).
|
||||
if (ns == "Http") {
|
||||
if (meth == "get") { bare = "http_get"; push(labels, "url") }
|
||||
if (meth == "post") { bare = "http_post"; push(labels, "url"); push(labels, "body") }
|
||||
if (meth == "request") { bare = "http_request"; push(labels, "method"); push(labels, "url"); push(labels, "body") }
|
||||
if (meth == "open") { bare = "http_open"; push(labels, "method"); push(labels, "url") }
|
||||
if (meth == "set") { bare = "http_set_header"; push(labels, "handle"); push(labels, "name"); push(labels, "value") }
|
||||
if (meth == "body") { bare = "http_body"; push(labels, "handle"); push(labels, "body") }
|
||||
if (meth == "body_bytes"){ bare = "http_body_n"; push(labels, "handle"); push(labels, "bytes"); push(labels, "len") }
|
||||
if (meth == "send") { bare = "http_send_req"; push(labels, "handle") }
|
||||
if (meth == "poll") { bare = "http_poll"; push(labels, "handle") }
|
||||
if (meth == "status") { bare = "http_status_of"; push(labels, "handle") }
|
||||
if (meth == "ok") { bare = "http_ok"; push(labels, "handle") }
|
||||
if (meth == "text") { bare = "http_text"; push(labels, "handle") }
|
||||
if (meth == "body_len") { bare = "http_body_len"; push(labels, "handle") }
|
||||
if (meth == "header") { bare = "http_header_of"; push(labels, "handle"); push(labels, "name") }
|
||||
if (meth == "free") { bare = "http_close"; push(labels, "handle") }
|
||||
if (meth == "parse") { bare = "http_parse"; push(labels, "bytes"); push(labels, "len") }
|
||||
}
|
||||
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
||||
# Each is a pure alias — the callee is rewritten to the bare name below.
|
||||
if (ns == "World") {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,16 @@ function emit_header() -> void {
|
|||
emith("declare i32 @snd_playing(ptr)\n")
|
||||
emith("declare void @snd_set_volume(ptr, i32)\n")
|
||||
emith("declare void @snd_set_rate(ptr, i32)\n")
|
||||
emith("declare ptr @hs_req_new(ptr, ptr)\n") # #6 HTTP transport (http.ll)
|
||||
emith("declare void @hs_req_header(ptr, ptr, ptr)\n")
|
||||
emith("declare void @hs_req_body(ptr, ptr, i32)\n")
|
||||
emith("declare void @hs_send(i32, ptr)\n")
|
||||
emith("declare i32 @hs_done(i32)\n")
|
||||
emith("declare i32 @hs_status(i32)\n")
|
||||
emith("declare ptr @hs_body(i32)\n")
|
||||
emith("declare i32 @hs_blen(i32)\n")
|
||||
emith("declare ptr @hs_header(i32, ptr)\n")
|
||||
emith("declare void @hs_free(i32)\n")
|
||||
emith("@__stderrp = external global ptr\n")
|
||||
emith("@__stdoutp = external global ptr\n")
|
||||
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ function is_intrinsic2(name: pointer) -> bool {
|
|||
if (name == "win_pad") or (name == "win_touch") { return true } # #51 gamepad / touch
|
||||
if (name == "snd_load") or (name == "snd_play") or (name == "snd_stop") { return true } # #22 audio
|
||||
if (name == "snd_playing") or (name == "snd_set_volume") or (name == "snd_set_rate") { return true }
|
||||
if (name == "hs_req_new") or (name == "hs_req_header") or (name == "hs_req_body") { return true } # #6 HTTP
|
||||
if (name == "hs_send") or (name == "hs_done") or (name == "hs_status") or (name == "hs_body") { return true }
|
||||
if (name == "hs_blen") or (name == "hs_header") or (name == "hs_free") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -72,5 +75,16 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val {
|
|||
if (name == "snd_playing") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @snd_playing(ptr {a})`), "int") }
|
||||
if (name == "snd_set_volume") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @snd_set_volume(ptr "); emit(a); emit(", i32 "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
if (name == "snd_set_rate") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @snd_set_rate(ptr "); emit(a); emit(", i32 "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
# #6 HTTP transport — NSURLConnection on a background thread (http.ll).
|
||||
if (name == "hs_req_new") { let a = arg_code(e, 0); let b = arg_code(e, 1); return val(emit_bind(`call ptr @hs_req_new(ptr {a}, ptr {b})`), "pointer") }
|
||||
if (name == "hs_req_header") { let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2); emit(" call void @hs_req_header(ptr "); emit(a); emit(", ptr "); emit(b); emit(", ptr "); emit(c); emit(")\n"); return val("0", "void") }
|
||||
if (name == "hs_req_body") { let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2); emit(" call void @hs_req_body(ptr "); emit(a); emit(", ptr "); emit(b); emit(", i32 "); emit(c); emit(")\n"); return val("0", "void") }
|
||||
if (name == "hs_send") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @hs_send(i32 "); emit(a); emit(", ptr "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
if (name == "hs_done") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @hs_done(i32 {a})`), "int") }
|
||||
if (name == "hs_status") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @hs_status(i32 {a})`), "int") }
|
||||
if (name == "hs_body") { let a = arg_code(e, 0); return val(emit_bind(`call ptr @hs_body(i32 {a})`), "pointer") }
|
||||
if (name == "hs_blen") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @hs_blen(i32 {a})`), "int") }
|
||||
if (name == "hs_header") { let a = arg_code(e, 0); let b = arg_code(e, 1); return val(emit_bind(`call ptr @hs_header(i32 {a}, ptr {b})`), "pointer") }
|
||||
if (name == "hs_free") { let a = arg_code(e, 0); emit(" call void @hs_free(i32 "); emit(a); emit(")\n"); return val("0", "void") }
|
||||
return val("0", "void")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ function lex(src: pointer) -> void {
|
|||
let e = src[i + 1]
|
||||
var r = e
|
||||
if e == 110 { r = 10 }
|
||||
if e == 114 { r = 13 }
|
||||
if e == 116 { r = 9 }
|
||||
if e == 48 { r = 0 }
|
||||
out[j] = r; j = j + 1; i = i + 2
|
||||
|
|
@ -86,6 +87,7 @@ function lex(src: pointer) -> void {
|
|||
if src[i] == 92 {
|
||||
let e = src[i + 1]
|
||||
if e == 110 { v = 10 }
|
||||
if e == 114 { v = 13 }
|
||||
if e == 116 { v = 9 }
|
||||
if e == 48 { v = 0 }
|
||||
i = i + 2
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ function p_postfix() -> Node {
|
|||
if e.a.kind == E_ID and e.a.s == "Motion" and e.s == "to" { g_uses_anim_rt = true }
|
||||
# Audio.* (#22) — any Audio method splices the audio runtime.
|
||||
if e.a.kind == E_ID and e.a.s == "Audio" { g_uses_audio = true }
|
||||
# Http.* (#6) — any Http method splices the HTTP client runtime.
|
||||
if e.a.kind == E_ID and e.a.s == "Http" { g_uses_http = true }
|
||||
# Tween.to/chain/delay/value/stop/parallel (#48): the fluent stateful handles
|
||||
# live in tween.ludic, advanced by an engine-owned system each Update tick.
|
||||
if e.a.kind == E_ID and e.a.s == "Tween" and (e.s == "to" or e.s == "chain" or e.s == "delay" or e.s == "value" or e.s == "stop" or e.s == "parallel") { g_uses_tween_rt = true }
|
||||
|
|
@ -429,6 +431,7 @@ var g_uses_input: bool = false # a program used Input.bind/down/poll/… (act
|
|||
var g_uses_anim_rt: bool = false # Anim.play/clip/on_frame/fired or Motion.to (#48) -> splice systems.ludic + force the reflection ABI
|
||||
var g_uses_tween_rt: bool = false # Tween.to/chain/delay/… (#48) -> splice tween.ludic + run esys_tween each Update
|
||||
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
|
||||
var g_uses_http: bool = false # Http.* (#6) -> splice http.ludic; links http.ll + Foundation (macOS)
|
||||
|
||||
function already_loaded(full: pointer) -> bool {
|
||||
var i = 0
|
||||
|
|
@ -657,6 +660,14 @@ function maybe_splice_runtime() -> void {
|
|||
do_import("runtime/native/audio.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# Http.* (#6): splice the HTTP client. The transport (hs_* intrinsics) is native
|
||||
# (linked from http.ll), but the response parser is pure Ludic; self-contained,
|
||||
# so it works in a plain program too.
|
||||
if g_uses_http {
|
||||
cur_dir = ""
|
||||
do_import("runtime/native/http.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# Anim.play/Motion.to sugar (#48): the writes live in systems.ludic and use the
|
||||
# reflection ABI, so splice it and force the world table even when the game does
|
||||
# not otherwise trip uses_engine_systems.
|
||||
|
|
|
|||
46230
selfhost/ludicc.seed.ll
46230
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -173,6 +173,13 @@ entry {
|
|||
cmd = `{cmd} {audio} -Wl,-needed_framework,AVFoundation`
|
||||
}
|
||||
}
|
||||
# Http.* (#6) links the native transport and Foundation (NSURLConnection etc.,
|
||||
# reached by name). Works headless too, so it is outside the windowed block —
|
||||
# macOS-only for now, which is where the toolchain runs.
|
||||
if g_uses_http {
|
||||
let http = path_join(home, "runtime/native/http.ll")
|
||||
cmd = `{cmd} {http} -Wl,-needed_framework,Foundation`
|
||||
}
|
||||
cmd = `{cmd} -o {out}`
|
||||
|
||||
let rc = run(cmd)
|
||||
|
|
|
|||
|
|
@ -257,6 +257,16 @@ function cmd_test() -> int {
|
|||
feat_case("library/input_actions", " xwa", "1 0 1 1 0 1 0", "input_actions.ludic (#7 action maps + rebinding + deterministic replay)")
|
||||
feat_case("library/input_device", "", "1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1", "input_device.ludic (#50 multi-key held state + analog axis/vector + mouse + gamepad/touch + full-state replay)")
|
||||
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
|
||||
# #6 Http.* — the client links Foundation (macOS-only), so build it through the
|
||||
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
|
||||
# run exercises the pure-Ludic response parser; no network is touched.
|
||||
if is_darwin() {
|
||||
if shq("LUDIC_HOME=. bin/ludicc examples/library/http.ludic -o /tmp/x_http_demo > /tmp/x_http.out 2>&1") {
|
||||
let got = capture_line("/tmp/x_http_demo < /dev/null")
|
||||
if (got == "200 1 5 hello 42 text/plain 404 0") { ok("http.ludic (#6 Http.parse/status/ok/text/header — pure response parser)") }
|
||||
else { bad2("http.ludic", `got [{got}]`) }
|
||||
} else { bad2("http.ludic build", capture_line("tail -1 /tmp/x_http.out")) }
|
||||
}
|
||||
|
||||
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
||||
net_case("events/scoped", "2")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue