From 51c0a0b306ecce20947a71bb981d1a3a6c7f900e Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 25 Feb 2026 14:48:37 -0600 Subject: [PATCH] tls and http --- http.cm | 427 +++++++++++++++++++++++++++++++++++++++++++++++++- meson.build | 2 + net/socket.c | 71 +++++++++ net/tls.c | 238 ++++++++++++++++++++++++++++ streamline.cm | 6 +- 5 files changed, 740 insertions(+), 4 deletions(-) create mode 100644 net/tls.c diff --git a/http.cm b/http.cm index 332a3e33..1cabcd11 100644 --- a/http.cm +++ b/http.cm @@ -1,14 +1,19 @@ var socket = use('socket') -var c_http = use('net/http') +var tls = use('net/tls') def CRLF = "\r\n" def status_texts = { "200": "OK", "201": "Created", "204": "No Content", + "301": "Moved Permanently", "302": "Found", "307": "Temporary Redirect", "400": "Bad Request", "401": "Unauthorized", "403": "Forbidden", "404": "Not Found", "405": "Method Not Allowed", "500": "Internal Server Error" } +// ============================================================ +// Server (unchanged) +// ============================================================ + function serve(port) { var fd = socket.socket("AF_INET", "SOCK_STREAM") socket.setsockopt(fd, "SOL_SOCKET", "SO_REUSEADDR", true) @@ -152,6 +157,10 @@ function sse_close(conn) { socket.close(conn) } +// ============================================================ +// Blocking client request (kept for compatibility) +// ============================================================ + function request(method, url, headers, body) { var parts = array(url, "/") var host_port = parts[2] @@ -221,13 +230,425 @@ function request(method, url, headers, body) { } } +// ============================================================ +// Requestor-based async fetch +// ============================================================ + +// parse_url requestor — sync, extract {scheme, host, port, path} from URL +var parse_url = function(callback, value) { + var url = null + var method = "GET" + var req_headers = null + var req_body = null + log.console("value type=" + text(is_text(value)) + " val=" + text(value)) + if (is_text(value)) { + url = value + log.console("url after assign=" + text(is_text(url)) + " url=" + text(url)) + } else { + url = value.url + if (value.method != null) method = value.method + if (value.headers != null) req_headers = value.headers + if (value.body != null) req_body = value.body + } + + // strip scheme + var scheme = "http" + var rest = url + var scheme_end = search(url, "://") + log.console("A: url_type=" + text(is_text(url)) + " scheme_end=" + text(scheme_end)) + if (scheme_end != null) { + scheme = lower(text(url, 0, scheme_end)) + rest = text(url, scheme_end + 3, length(url)) + log.console("B: scheme=" + scheme + " rest=" + rest + " rest_type=" + text(is_text(rest))) + } + + // split host from path + var slash = search(rest, "/") + var host_port = rest + var path = "/" + log.console("C: slash=" + text(slash)) + if (slash != null) { + host_port = text(rest, 0, slash) + path = text(rest, slash, length(rest)) + } + // split host:port + var hp = array(host_port, ":") + var host = hp[0] + var port = null + if (length(hp) > 1) { + port = number(hp[1]) + } else { + port = scheme == "https" ? 443 : 80 + } + + callback({ + scheme: scheme, host: host, port: port, path: path, + host_port: host_port, method: method, + req_headers: req_headers, req_body: req_body + }) + return null +} + +// resolve_dns requestor — blocking getaddrinfo, swappable later +var resolve_dns = function(callback, state) { + var ok = true + var addrs = null + var _resolve = function() { + addrs = socket.getaddrinfo(state.host, text(state.port)) + } disruption { + ok = false + } + _resolve() + if (!ok || addrs == null || length(addrs) == 0) { + callback(null, "dns resolution failed for " + state.host) + return null + } + callback(record(state, {address: addrs[0].address})) + return null +} + +// open_connection requestor — non-blocking connect + optional TLS +var open_connection = function(callback, state) { + var fd = socket.socket("AF_INET", "SOCK_STREAM") + var cancelled = false + + var cancel = function() { + var _close = null + if (!cancelled) { + cancelled = true + _close = function() { + socket.unwatch(fd) + socket.close(fd) + } disruption {} + _close() + } + } + + socket.setnonblock(fd) + + var finish_connect = function(the_fd) { + var ctx = null + if (state.scheme == "https") { + ctx = tls.wrap(the_fd, state.host) + } + callback(record(state, {fd: the_fd, tls: ctx})) + } + + // non-blocking connect — EINPROGRESS is expected + var connect_err = false + var _connect = function() { + socket.connect(fd, {address: state.address, port: state.port}) + } disruption { + connect_err = true + } + _connect() + + // if connect succeeded immediately (localhost, etc) + var _finish_immediate = null + if (!connect_err && !cancelled) { + _finish_immediate = function() { + finish_connect(fd) + } disruption { + cancel() + callback(null, "connection setup failed") + } + _finish_immediate() + return cancel + } + + // wait for connect to complete + socket.on_writable(fd, function() { + if (cancelled) return + var err = socket.getsockopt(fd, "SOL_SOCKET", "SO_ERROR") + if (err != 0) { + cancel() + callback(null, "connect failed (errno " + text(err) + ")") + return + } + var _finish = function() { + finish_connect(fd) + } disruption { + cancel() + callback(null, "connection setup failed") + } + _finish() + }) + + return cancel +} + +// send_request requestor — format + send HTTP/1.1 request +var send_request = function(callback, state) { + var cancelled = false + var cancel = function() { + cancelled = true + } + + var _send = function() { + var body_str = "" + var keys = null + var i = 0 + if (state.req_body != null) { + if (is_text(state.req_body)) body_str = state.req_body + else body_str = text(state.req_body) + } + + var req = state.method + " " + state.path + " HTTP/1.1" + CRLF + req = req + "Host: " + state.host_port + CRLF + req = req + "Connection: close" + CRLF + req = req + "User-Agent: cell/1.0" + CRLF + req = req + "Accept: */*" + CRLF + + if (state.req_headers != null) { + keys = array(state.req_headers) + i = 0 + while (i < length(keys)) { + req = req + keys[i] + ": " + state.req_headers[keys[i]] + CRLF + i = i + 1 + } + } + + if (length(body_str) > 0) { + req = req + "Content-Length: " + text(length(body_str)) + CRLF + } + + req = req + CRLF + body_str + + if (state.tls != null) { + tls.send(state.tls, req) + } else { + socket.send(state.fd, req) + } + } disruption { + if (!cancelled) callback(null, "send request failed") + return cancel + } + + _send() + if (!cancelled) callback(state) + return cancel +} + +// parse response headers from raw text +function parse_headers(raw) { + var hdr_end = search(raw, CRLF + CRLF) + if (hdr_end == null) return null + + var header_text = text(raw, 0, hdr_end) + var lines = array(header_text, CRLF) + var status_parts = array(lines[0], " ") + var status_code = number(status_parts[1]) + + var headers = {} + var i = 1 + var colon = null + while (i < length(lines)) { + colon = search(lines[i], ": ") + if (colon != null) { + headers[lower(text(lines[i], 0, colon))] = text(lines[i], colon + 2) + } + i = i + 1 + } + + return { + status: status_code, headers: headers, + body_start: hdr_end + 4 + } +} + +// decode chunked transfer encoding +function decode_chunked(body_text) { + var result = "" + var pos = 0 + var chunk_end = null + var chunk_size = null + while (pos < length(body_text)) { + chunk_end = search(text(body_text, pos), CRLF) + if (chunk_end == null) return result + chunk_size = number(text(body_text, pos, pos + chunk_end), 16) + if (chunk_size == null || chunk_size == 0) return result + pos = pos + chunk_end + 2 + result = result + text(body_text, pos, pos + chunk_size) + pos = pos + chunk_size + 2 + } + return result +} + +// receive_response requestor — async incremental receive +var receive_response = function(callback, state) { + var cancelled = false + var buffer = "" + var parsed = null + var content_length = null + var is_chunked = false + var body_complete = false + + var cancel = function() { + var _cleanup = null + if (!cancelled) { + cancelled = true + _cleanup = function() { + if (state.tls != null) { + tls.close(state.tls) + } else { + socket.unwatch(state.fd) + socket.close(state.fd) + } + } disruption {} + _cleanup() + } + } + + var finish = function() { + if (cancelled) return + var body_text = text(buffer, parsed.body_start) + + if (is_chunked) { + body_text = decode_chunked(body_text) + } + + // clean up connection + var _cleanup = function() { + if (state.tls != null) { + tls.close(state.tls) + } else { + socket.close(state.fd) + } + } disruption {} + _cleanup() + + callback({ + status: parsed.status, + headers: parsed.headers, + body: body_text + }) + } + + var check_complete = function() { + var te = null + var cl = null + var body_text = null + // still waiting for headers + if (parsed == null) { + parsed = parse_headers(buffer) + if (parsed == null) return false + te = parsed.headers["transfer-encoding"] + if (te != null && search(lower(te), "chunked") != null) { + is_chunked = true + } + cl = parsed.headers["content-length"] + if (cl != null) content_length = number(cl) + } + + body_text = text(buffer, parsed.body_start) + + if (is_chunked) { + // chunked: look for the terminating 0-length chunk + if (search(body_text, CRLF + "0" + CRLF) != null) return true + if (starts_with(body_text, "0" + CRLF)) return true + return false + } + + if (content_length != null) { + return length(body_text) >= content_length + } + + // connection: close — we read until EOF (handled by recv returning 0 bytes) + return false + } + + var on_data = function() { + if (cancelled) return + var chunk = null + var got_data = false + var eof = false + + var _recv = function() { + if (state.tls != null) { + chunk = tls.recv(state.tls, 16384) + } else { + chunk = socket.recv(state.fd, 16384) + } + } disruption { + // recv error — treat as EOF + eof = true + } + _recv() + + var chunk_text = null + if (!eof && chunk != null) { + stone(chunk) + chunk_text = text(chunk) + if (length(chunk_text) > 0) { + buffer = buffer + chunk_text + got_data = true + } else { + eof = true + } + } + + if (got_data && check_complete()) { + finish() + return + } + + if (eof) { + // connection closed — if we have headers, deliver what we have + if (parsed != null || parse_headers(buffer) != null) { + if (parsed == null) parsed = parse_headers(buffer) + finish() + } else { + cancel() + callback(null, "connection closed before headers received") + } + return + } + + // re-register for more data (one-shot watches) + if (!cancelled) { + if (state.tls != null) { + tls.on_readable(state.tls, on_data) + } else { + socket.on_readable(state.fd, on_data) + } + } + } + + // start reading + if (state.tls != null) { + tls.on_readable(state.tls, on_data) + } else { + socket.on_readable(state.fd, on_data) + } + + return cancel +} + +// ============================================================ +// fetch — composed requestor pipeline +// ============================================================ + +var fetch = function(callback, value) { + def pipeline = runtime_env.sequence([ + parse_url, + resolve_dns, + open_connection, + send_request, + receive_response + ]) + return pipeline(callback, value) +} + function close(fd) { socket.close(fd) } return { + // server serve: serve, accept: accept, on_request: on_request, - respond: respond, request: request, + respond: respond, close: close, sse_open: sse_open, sse_event: sse_event, sse_close: sse_close, - close: close, fetch: c_http.fetch + // client + fetch: fetch, + request: request } diff --git a/meson.build b/meson.build index dbe26259..63f0d4e3 100644 --- a/meson.build +++ b/meson.build @@ -34,6 +34,7 @@ if host_machine.system() == 'darwin' fworks = [ 'CoreFoundation', 'CFNetwork', + 'Security', ] foreach fkit : fworks deps += dependency('appleframeworks', modules: fkit) @@ -82,6 +83,7 @@ scripts = [ 'internal/os.c', 'internal/fd.c', 'net/http.c', + 'net/tls.c', 'net/socket.c', 'internal/enet.c', 'archive/miniz.c', diff --git a/net/socket.c b/net/socket.c index f76e51c0..3c94097c 100644 --- a/net/socket.c +++ b/net/socket.c @@ -24,6 +24,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif // Helper to convert JS value to file descriptor static int js2fd(JSContext *ctx, JSValueConst val) @@ -582,6 +585,69 @@ JSC_CCALL(socket_unwatch, return JS_NULL; ) +JSC_CCALL(socket_on_writable, + int sockfd = js2fd(js, argv[0]); + if (sockfd < 0) return JS_EXCEPTION; + if (!JS_IsFunction(argv[1])) + return JS_RaiseDisrupt(js, "on_writable: callback must be a function"); + actor_watch_writable(js, sockfd, argv[1]); + return JS_NULL; +) + +JSC_CCALL(socket_setnonblock, + int sockfd = js2fd(js, argv[0]); + if (sockfd < 0) return JS_EXCEPTION; +#ifdef _WIN32 + u_long mode = 1; + if (ioctlsocket(sockfd, FIONBIO, &mode) != 0) + return JS_RaiseDisrupt(js, "setnonblock failed"); +#else + int flags = fcntl(sockfd, F_GETFL, 0); + if (flags < 0 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0) + return JS_RaiseDisrupt(js, "setnonblock failed: %s", strerror(errno)); +#endif + return JS_NULL; +) + +JSC_CCALL(socket_getsockopt, + int sockfd = js2fd(js, argv[0]); + if (sockfd < 0) return JS_EXCEPTION; + + int level = SOL_SOCKET; + int optname = 0; + + // Parse level + if (JS_IsText(argv[1])) { + const char *level_str = JS_ToCString(js, argv[1]); + if (strcmp(level_str, "SOL_SOCKET") == 0) level = SOL_SOCKET; + else if (strcmp(level_str, "IPPROTO_TCP") == 0) level = IPPROTO_TCP; + else if (strcmp(level_str, "IPPROTO_IP") == 0) level = IPPROTO_IP; + else if (strcmp(level_str, "IPPROTO_IPV6") == 0) level = IPPROTO_IPV6; + JS_FreeCString(js, level_str); + } else { + level = js2number(js, argv[1]); + } + + // Parse option name + if (JS_IsText(argv[2])) { + const char *opt_str = JS_ToCString(js, argv[2]); + if (strcmp(opt_str, "SO_ERROR") == 0) optname = SO_ERROR; + else if (strcmp(opt_str, "SO_REUSEADDR") == 0) optname = SO_REUSEADDR; + else if (strcmp(opt_str, "SO_KEEPALIVE") == 0) optname = SO_KEEPALIVE; + else if (strcmp(opt_str, "SO_BROADCAST") == 0) optname = SO_BROADCAST; + JS_FreeCString(js, opt_str); + } else { + optname = js2number(js, argv[2]); + } + + int optval = 0; + socklen_t optlen = sizeof(optval); + if (getsockopt(sockfd, level, optname, &optval, &optlen) < 0) + return JS_RaiseDisrupt(js, "getsockopt failed: %s", strerror(errno)); + + return JS_NewInt32(js, optval); +) + static const JSCFunctionListEntry js_socket_funcs[] = { MIST_FUNC_DEF(socket, getaddrinfo, 3), MIST_FUNC_DEF(socket, socket, 3), @@ -600,7 +666,10 @@ static const JSCFunctionListEntry js_socket_funcs[] = { MIST_FUNC_DEF(socket, setsockopt, 4), MIST_FUNC_DEF(socket, close, 1), MIST_FUNC_DEF(socket, on_readable, 2), + MIST_FUNC_DEF(socket, on_writable, 2), MIST_FUNC_DEF(socket, unwatch, 1), + MIST_FUNC_DEF(socket, setnonblock, 1), + MIST_FUNC_DEF(socket, getsockopt, 3), }; JSValue js_core_socket_use(JSContext *js) { @@ -625,6 +694,8 @@ JSValue js_core_socket_use(JSContext *js) { JS_SetPropertyStr(js, mod.val, "SOL_SOCKET", JS_NewInt32(js, SOL_SOCKET)); JS_SetPropertyStr(js, mod.val, "SO_REUSEADDR", JS_NewInt32(js, SO_REUSEADDR)); + JS_SetPropertyStr(js, mod.val, "SO_ERROR", JS_NewInt32(js, SO_ERROR)); + JS_SetPropertyStr(js, mod.val, "SO_KEEPALIVE", JS_NewInt32(js, SO_KEEPALIVE)); JS_RETURN(mod.val); } diff --git a/net/tls.c b/net/tls.c new file mode 100644 index 00000000..743722e5 --- /dev/null +++ b/net/tls.c @@ -0,0 +1,238 @@ +#include "cell.h" + +#include +#include +#include +#include + +#if defined(__APPLE__) +/* SecureTransport — deprecated but functional, no external deps */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#include +#include +#include +#include +#include + +typedef struct { + SSLContextRef ssl; + int fd; +} tls_ctx; + +static void tls_ctx_free(JSRuntime *rt, tls_ctx *ctx) { + if (!ctx) return; + if (ctx->ssl) { + SSLClose(ctx->ssl); + CFRelease(ctx->ssl); + } + if (ctx->fd >= 0) + close(ctx->fd); + free(ctx); +} + +QJSCLASS(tls_ctx,) + +static OSStatus tls_read_cb(SSLConnectionRef conn, void *data, size_t *len) { + int fd = *(const int *)conn; + size_t requested = *len; + size_t total = 0; + while (total < requested) { + ssize_t n = read(fd, (char *)data + total, requested - total); + if (n > 0) { + total += n; + } else if (n == 0) { + *len = total; + return errSSLClosedGraceful; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + *len = total; + return (total > 0) ? noErr : errSSLWouldBlock; + } + *len = total; + return errSSLClosedAbort; + } + } + *len = total; + return noErr; +} + +static OSStatus tls_write_cb(SSLConnectionRef conn, const void *data, size_t *len) { + int fd = *(const int *)conn; + size_t requested = *len; + size_t total = 0; + while (total < requested) { + ssize_t n = write(fd, (const char *)data + total, requested - total); + if (n > 0) { + total += n; + } else if (n == 0) { + *len = total; + return errSSLClosedGraceful; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + *len = total; + return (total > 0) ? noErr : errSSLWouldBlock; + } + *len = total; + return errSSLClosedAbort; + } + } + *len = total; + return noErr; +} + +/* tls.wrap(fd, hostname) -> ctx */ +JSC_CCALL(tls_wrap, + int fd = -1; + if (JS_ToInt32(js, &fd, argv[0]) < 0) + return JS_RaiseDisrupt(js, "tls.wrap: fd must be a number"); + const char *hostname = JS_ToCString(js, argv[1]); + if (!hostname) + return JS_RaiseDisrupt(js, "tls.wrap: hostname must be a string"); + + tls_ctx *ctx = calloc(1, sizeof(tls_ctx)); + ctx->fd = fd; + ctx->ssl = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType); + if (!ctx->ssl) { + free(ctx); + JS_FreeCString(js, hostname); + return JS_RaiseDisrupt(js, "tls.wrap: SSLCreateContext failed"); + } + + SSLSetIOFuncs(ctx->ssl, tls_read_cb, tls_write_cb); + SSLSetConnection(ctx->ssl, &ctx->fd); + SSLSetPeerDomainName(ctx->ssl, hostname, strlen(hostname)); + JS_FreeCString(js, hostname); + + /* Retry handshake on non-blocking sockets (errSSLWouldBlock) */ + OSStatus status; + for (int attempts = 0; attempts < 200; attempts++) { + status = SSLHandshake(ctx->ssl); + if (status == noErr) break; + if (status != errSSLWouldBlock) break; + struct pollfd pfd = { .fd = ctx->fd, .events = POLLIN | POLLOUT }; + poll(&pfd, 1, 50); + } + if (status != noErr) { + CFRelease(ctx->ssl); + ctx->ssl = NULL; + ctx->fd = -1; /* don't close caller's fd */ + free(ctx); + return JS_RaiseDisrupt(js, "tls.wrap: handshake failed (status %d)", (int)status); + } + + return tls_ctx2js(js, ctx); +) + +/* tls.send(ctx, data) -> bytes_sent */ +JSC_CCALL(tls_send, + tls_ctx *ctx = js2tls_ctx(js, argv[0]); + if (!ctx || !ctx->ssl) + return JS_RaiseDisrupt(js, "tls.send: invalid context"); + + size_t len; + size_t written = 0; + OSStatus status; + + if (JS_IsText(argv[1])) { + const char *data = JS_ToCStringLen(js, &len, argv[1]); + status = SSLWrite(ctx->ssl, data, len, &written); + JS_FreeCString(js, data); + } else { + unsigned char *data = js_get_blob_data(js, &len, argv[1]); + if (!data) + return JS_RaiseDisrupt(js, "tls.send: invalid data"); + status = SSLWrite(ctx->ssl, data, len, &written); + } + + if (status != noErr && status != errSSLWouldBlock) + return JS_RaiseDisrupt(js, "tls.send: write failed (status %d)", (int)status); + + return JS_NewInt64(js, (int64_t)written); +) + +/* tls.recv(ctx, len) -> blob */ +JSC_CCALL(tls_recv, + tls_ctx *ctx = js2tls_ctx(js, argv[0]); + if (!ctx || !ctx->ssl) + return JS_RaiseDisrupt(js, "tls.recv: invalid context"); + + size_t len = 4096; + if (argc > 1) len = js2number(js, argv[1]); + + void *out; + ret = js_new_blob_alloc(js, len, &out); + if (JS_IsException(ret)) return ret; + + size_t received = 0; + OSStatus status = SSLRead(ctx->ssl, out, len, &received); + + if (status != noErr && status != errSSLWouldBlock && + status != errSSLClosedGraceful) { + return JS_RaiseDisrupt(js, "tls.recv: read failed (status %d)", (int)status); + } + + js_blob_stone(ret, received); + return ret; +) + +/* tls.close(ctx) -> null */ +JSC_CCALL(tls_close, + tls_ctx *ctx = js2tls_ctx(js, argv[0]); + if (!ctx) return JS_NULL; + if (ctx->ssl) { + SSLClose(ctx->ssl); + CFRelease(ctx->ssl); + ctx->ssl = NULL; + } + if (ctx->fd >= 0) { + close(ctx->fd); + ctx->fd = -1; + } + return JS_NULL; +) + +/* tls.fd(ctx) -> number — get underlying fd for on_readable */ +JSC_CCALL(tls_fd, + tls_ctx *ctx = js2tls_ctx(js, argv[0]); + if (!ctx) + return JS_RaiseDisrupt(js, "tls.fd: invalid context"); + return JS_NewInt32(js, ctx->fd); +) + +/* tls.on_readable(ctx, callback) -> null */ +JSC_CCALL(tls_on_readable, + tls_ctx *ctx = js2tls_ctx(js, argv[0]); + if (!ctx) + return JS_RaiseDisrupt(js, "tls.on_readable: invalid context"); + if (!JS_IsFunction(argv[1])) + return JS_RaiseDisrupt(js, "tls.on_readable: callback must be a function"); + actor_watch_readable(js, ctx->fd, argv[1]); + return JS_NULL; +) + +static const JSCFunctionListEntry js_tls_funcs[] = { + MIST_FUNC_DEF(tls, wrap, 2), + MIST_FUNC_DEF(tls, send, 2), + MIST_FUNC_DEF(tls, recv, 2), + MIST_FUNC_DEF(tls, close, 1), + MIST_FUNC_DEF(tls, fd, 1), + MIST_FUNC_DEF(tls, on_readable, 2), +}; + +JSValue js_core_net_tls_use(JSContext *js) { + JS_FRAME(js); + QJSCLASSPREP_NO_FUNCS(tls_ctx); + JS_ROOT(mod, JS_NewObject(js)); + JS_SetPropertyFunctionList(js, mod.val, js_tls_funcs, countof(js_tls_funcs)); + JS_RETURN(mod.val); +} + +#pragma clang diagnostic pop + +#else +/* Stub for non-Apple platforms — TLS not yet implemented */ +JSValue js_core_net_tls_use(JSContext *js) { + return JS_RaiseDisrupt(js, "TLS not available on this platform"); +} +#endif diff --git a/streamline.cm b/streamline.cm index 575c6188..14bf99d0 100644 --- a/streamline.cm +++ b/streamline.cm @@ -278,7 +278,11 @@ var streamline = function(ir, log) { store_index: [1, T_ARRAY, 2, T_INT], store_field: [1, T_RECORD], push: [1, T_ARRAY], load_index: [2, T_ARRAY, 3, T_INT], load_field: [2, T_RECORD], - pop: [2, T_ARRAY] + pop: [2, T_ARRAY], + is_text: [2, T_UNKNOWN], is_int: [2, T_UNKNOWN], is_num: [2, T_UNKNOWN], + is_bool: [2, T_UNKNOWN], is_null: [2, T_UNKNOWN], + is_array: [2, T_UNKNOWN], is_func: [2, T_UNKNOWN], + is_record: [2, T_UNKNOWN], is_blob: [2, T_UNKNOWN] } var infer_param_types = function(func) {