Some CGI Fun. (unstable) (0.39.5).
This commit is contained in:
parent
18ac230c21
commit
11d6fd358a
5 changed files with 769 additions and 44 deletions
|
|
@ -1,5 +1,5 @@
|
|||
cmake_minimum_required(VERSION 3.10)
|
||||
project(fun VERSION 0.39.4 LANGUAGES C)
|
||||
project(fun VERSION 0.39.5 LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
|
|
|||
185
examples/blocking/http_server_cgi.fun
Normal file
185
examples/blocking/http_server_cgi.fun
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env fun
|
||||
|
||||
/*
|
||||
* Minimal CGI-capable HTTP server (blocking)
|
||||
*/
|
||||
|
||||
#include <net/http_server.fun>
|
||||
#include <io/socket.fun>
|
||||
#include <strings.fun>
|
||||
|
||||
port = 8080
|
||||
htdocs = "./examples/data/htdocs"
|
||||
|
||||
// Lightweight inline HTTP server: reuse TcpServer directly so we can inject CGI env
|
||||
class HTTPCGIServer(number port)
|
||||
fun _construct(this, port)
|
||||
this.port = port
|
||||
this.srv = TcpServer(port, 10)
|
||||
this.htdocs = "./"
|
||||
|
||||
fun set_htdocs(this, path)
|
||||
this.htdocs = to_string(path)
|
||||
|
||||
fun start(this)
|
||||
if (this.srv.listen() <= 0)
|
||||
print("HTTP CGI Server: failed to listen on port " + to_string(this.port))
|
||||
return 0
|
||||
print("HTTP CGI Server: serving " + this.htdocs + " on port " + to_string(this.port))
|
||||
while true
|
||||
fd = this.srv.accept()
|
||||
if (fd > 0)
|
||||
this.handle_client(fd)
|
||||
return 1
|
||||
|
||||
fun _parse_headers(this, request)
|
||||
headers = {}
|
||||
lines = str_split(request, "\n")
|
||||
i = 1 // start after request line
|
||||
while (i < len(lines))
|
||||
ln = str_trim(lines[i])
|
||||
if (len(ln) == 0)
|
||||
break
|
||||
colon = find(ln, ":")
|
||||
if (colon > 0)
|
||||
k = str_to_upper(str_trim(substr(ln, 0, colon)))
|
||||
v = str_trim(substr(ln, colon + 1, len(ln) - colon - 1))
|
||||
headers[k] = v
|
||||
i = i + 1
|
||||
return headers
|
||||
|
||||
fun handle_client(this, fd)
|
||||
request = sock_recv(fd, 65536)
|
||||
if (len(request) == 0)
|
||||
sock_close(fd)
|
||||
return 0
|
||||
|
||||
lines = str_split(request, "\n")
|
||||
if (len(lines) == 0)
|
||||
sock_close(fd)
|
||||
return 0
|
||||
|
||||
reqline = str_trim(lines[0])
|
||||
parts = str_split(reqline, " ")
|
||||
if (len(parts) < 2)
|
||||
sock_close(fd)
|
||||
return 0
|
||||
|
||||
method = str_trim(parts[0])
|
||||
target = str_trim(parts[1])
|
||||
|
||||
// Split query string
|
||||
path = target
|
||||
query = ""
|
||||
q = find(target, "?")
|
||||
if (q >= 0)
|
||||
path = substr(target, 0, q)
|
||||
query = substr(target, q + 1, len(target) - q - 1)
|
||||
|
||||
if (path == "/")
|
||||
path = "/index.html"
|
||||
|
||||
file_path = this.htdocs + path
|
||||
|
||||
// Headers and body
|
||||
headers = this._parse_headers(request)
|
||||
|
||||
// Extract POST body
|
||||
body = ""
|
||||
header_end = find(request, "\r\n\r\n")
|
||||
if (header_end >= 0)
|
||||
body = substr(request, header_end + 4, len(request) - header_end - 4)
|
||||
else
|
||||
header_end = find(request, "\n\n")
|
||||
if (header_end >= 0)
|
||||
body = substr(request, header_end + 2, len(request) - header_end - 2)
|
||||
|
||||
// Serve CGI when file ends with .fun
|
||||
if (str_ends_with(path, ".fun"))
|
||||
// Build env vars according to CGI conventions
|
||||
host = headers["HOST"]
|
||||
if (len(host) == 0) host = "localhost"
|
||||
ua = headers["USER-AGENT"]
|
||||
cookie = headers["COOKIE"]
|
||||
ctype = headers["CONTENT-TYPE"]
|
||||
clen = headers["CONTENT-LENGTH"]
|
||||
|
||||
envs = []
|
||||
// Ensure Fun stdlib is available to CGI child process
|
||||
funlib = env("FUN_LIB_DIR")
|
||||
if (len(funlib) == 0)
|
||||
// Best-effort default when running from project root
|
||||
funlib = "./lib"
|
||||
push(envs, "FUN_LIB_DIR='" + funlib + "'")
|
||||
push(envs, "REQUEST_METHOD='" + method + "'")
|
||||
push(envs, "QUERY_STRING='" + query + "'")
|
||||
push(envs, "SCRIPT_NAME='" + path + "'")
|
||||
push(envs, "PATH_INFO='" + path + "'")
|
||||
push(envs, "SERVER_NAME='" + host + "'")
|
||||
push(envs, "SERVER_PORT='" + to_string(this.port) + "'")
|
||||
push(envs, "SERVER_PROTOCOL='HTTP/1.1'")
|
||||
push(envs, "HTTP_HOST='" + host + "'")
|
||||
if (len(ua) > 0)
|
||||
push(envs, "HTTP_USER_AGENT='" + ua + "'")
|
||||
if (len(cookie) > 0)
|
||||
push(envs, "HTTP_COOKIE='" + cookie + "'")
|
||||
if (len(ctype) > 0)
|
||||
push(envs, "CONTENT_TYPE='" + ctype + "'")
|
||||
if (len(clen) > 0)
|
||||
push(envs, "CONTENT_LENGTH='" + clen + "'")
|
||||
if (len(body) > 0)
|
||||
push(envs, "POST_DATA='" + body + "'")
|
||||
|
||||
// Decide which Fun interpreter to use for the CGI child
|
||||
// Priority:
|
||||
// 1) $FUN_EXEC (explicit override)
|
||||
// 2) ./build_debug/fun (when running from project root)
|
||||
// 3) ./build_release/fun (release build)
|
||||
// 4) fun (from PATH)
|
||||
exec = env("FUN_EXEC")
|
||||
if (len(exec) == 0)
|
||||
// crude existence check using read_file; good enough here
|
||||
if (len(read_file("./build_debug/fun")) > 0)
|
||||
exec = "./build_debug/fun"
|
||||
else
|
||||
if (len(read_file("./build_release/fun")) > 0)
|
||||
exec = "./build_release/fun"
|
||||
else
|
||||
exec = "fun"
|
||||
|
||||
// Run the Fun script as a CGI program
|
||||
cmd = join(envs, " ") + " " + exec + " " + file_path
|
||||
res = proc_run(cmd)
|
||||
out = res["out"]
|
||||
|
||||
if (len(out) == 0)
|
||||
this._send(fd, 500, "Internal Server Error", "<h1>CGI produced no output</h1>")
|
||||
else
|
||||
// If CGI output already includes HTTP headers, pass through
|
||||
// Otherwise, wrap it in a basic 200 response.
|
||||
if (find(out, "\r\n\r\n") >= 0 || find(out, "\n\n") >= 0)
|
||||
sock_send(fd, out)
|
||||
else
|
||||
this._send(fd, 200, "OK", out)
|
||||
else
|
||||
// Static file
|
||||
content = read_file(file_path)
|
||||
if (len(content) > 0)
|
||||
this._send(fd, 200, "OK", content)
|
||||
else
|
||||
this._send(fd, 404, "Not Found", "<h1>404 Not Found</h1>")
|
||||
|
||||
sock_close(fd)
|
||||
return 1
|
||||
|
||||
fun _send(this, fd, code, text, body)
|
||||
b = to_string(body)
|
||||
resp = "HTTP/1.1 " + to_string(code) + " " + text + "\r\n"
|
||||
resp = resp + "Content-Type: text/html; charset=utf-8\r\n"
|
||||
resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
|
||||
resp = resp + "Connection: close\r\n\r\n" + b
|
||||
sock_send(fd, resp)
|
||||
|
||||
server = HTTPCGIServer(port)
|
||||
server.set_htdocs(htdocs)
|
||||
server.start()
|
||||
28
examples/data/htdocs/hello.fun
Normal file
28
examples/data/htdocs/hello.fun
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env fun
|
||||
|
||||
#include <net/cgi.fun>
|
||||
|
||||
cgi = CGI()
|
||||
|
||||
name = cgi.param("name")
|
||||
if (len(name) == 0)
|
||||
name = "World"
|
||||
|
||||
ua = cgi.cookie("ua")
|
||||
if (len(ua) == 0)
|
||||
// Set a demo cookie via header()
|
||||
cgi.header("Set-Cookie", "ua=FunClient; Path=/; HttpOnly")
|
||||
|
||||
cgi.content_type("text/html; charset=utf-8")
|
||||
|
||||
html = ""
|
||||
html = html + "<html><head><title>Fun CGI</title></head><body>"
|
||||
html = html + "<h1>Hello, " + cgi.escape_html(name) + "!</h1>"
|
||||
html = html + "<p>REQUEST_METHOD: " + cgi.escape_html(cgi.env["REQUEST_METHOD"]) + "</p>"
|
||||
html = html + "<p>QUERY_STRING: " + cgi.escape_html(cgi.env["QUERY_STRING"]) + "</p>"
|
||||
html = html + "<p>User-Agent cookie: " + cgi.escape_html(ua) + "</p>"
|
||||
html = html + "<h2>Params</h2><pre>" + to_string(cgi.params()) + "</pre>"
|
||||
html = html + "</body></html>"
|
||||
|
||||
// Send CGI headers + body
|
||||
cgi.send(html)
|
||||
182
lib/net/cgi.fun
Normal file
182
lib/net/cgi.fun
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Minimal, parser-friendly CGI helper (incrementally extend as needed)
|
||||
|
||||
#include <strings.fun>
|
||||
|
||||
class CGI()
|
||||
fun _construct(this)
|
||||
this._params = {}
|
||||
this._cookies = {}
|
||||
this._headers = {}
|
||||
this._header_list = [] // preserve insertion order for emission without map iteration
|
||||
this._status = 200
|
||||
this._status_text = "OK"
|
||||
this._content_type = "text/html; charset=utf-8"
|
||||
|
||||
this.env = {}
|
||||
this.env["REQUEST_METHOD"] = env("REQUEST_METHOD")
|
||||
this.env["QUERY_STRING"] = env("QUERY_STRING")
|
||||
this.env["CONTENT_TYPE"] = env("CONTENT_TYPE")
|
||||
this.env["CONTENT_LENGTH"] = env("CONTENT_LENGTH")
|
||||
this.env["HTTP_COOKIE"] = env("HTTP_COOKIE")
|
||||
this.env["POST_DATA"] = env("POST_DATA")
|
||||
|
||||
// Cookies
|
||||
this._cookies = this._parse_cookies(this.env["HTTP_COOKIE"])
|
||||
|
||||
// Params from QUERY_STRING
|
||||
qs = this.env["QUERY_STRING"]
|
||||
if (typeof(qs) == "String" && len(qs) > 0)
|
||||
parsed_qs = this._parse_urlencoded(qs)
|
||||
this._merge_params(parsed_qs)
|
||||
|
||||
// Params from POST (x-www-form-urlencoded only)
|
||||
ct = this.env["CONTENT_TYPE"]
|
||||
pd = this.env["POST_DATA"]
|
||||
if (typeof(ct) == "String" && len(ct) > 0 && find(str_to_lower(ct), "application/x-www-form-urlencoded") >= 0)
|
||||
if (typeof(pd) == "String" && len(pd) > 0)
|
||||
parsed_pd = this._parse_urlencoded(pd)
|
||||
this._merge_params(parsed_pd)
|
||||
|
||||
fun param(this, name)
|
||||
arr = this._params[name]
|
||||
if (typeof(arr) != "Array")
|
||||
return ""
|
||||
if (len(arr) > 0)
|
||||
return arr[0]
|
||||
return ""
|
||||
|
||||
fun param_all(this, name)
|
||||
arr = this._params[name]
|
||||
if (typeof(arr) != "Array")
|
||||
return []
|
||||
return arr
|
||||
|
||||
fun params(this)
|
||||
// Return the internal map directly (no copying, to avoid map iteration)
|
||||
return this._params
|
||||
|
||||
fun cookie(this, name)
|
||||
c = this._cookies[name]
|
||||
if (typeof(c) != "String")
|
||||
return ""
|
||||
return c
|
||||
|
||||
fun cookies(this)
|
||||
// Return the internal cookies map directly
|
||||
return this._cookies
|
||||
|
||||
fun status(this, code, text)
|
||||
this._status = to_number(code)
|
||||
if (len(text) > 0)
|
||||
this._status_text = text
|
||||
|
||||
fun content_type(this, ct)
|
||||
this._content_type = to_string(ct)
|
||||
|
||||
fun header(this, name, value)
|
||||
k = to_string(name)
|
||||
v = to_string(value)
|
||||
this._headers[k] = v
|
||||
// Track in ordered list for emission
|
||||
push(this._header_list, [k, v])
|
||||
|
||||
fun redirect(this, location, code)
|
||||
c = to_number(code)
|
||||
if (c == 0)
|
||||
c = 302
|
||||
if (c == 301)
|
||||
this.status(c, "Moved Permanently")
|
||||
else
|
||||
this.status(c, "Found")
|
||||
this.header("Location", to_string(location))
|
||||
|
||||
fun header_str(this)
|
||||
out = "Status: " + to_string(this._status) + " " + this._status_text + "\r\n"
|
||||
out = out + "Content-Type: " + this._content_type + "\r\n"
|
||||
// Emit headers from the ordered list
|
||||
i = 0
|
||||
n = len(this._header_list)
|
||||
while (i < n)
|
||||
pair = this._header_list[i]
|
||||
if (typeof(pair) == "Array" && len(pair) >= 2)
|
||||
out = out + to_string(pair[0]) + ": " + to_string(pair[1]) + "\r\n"
|
||||
i = i + 1
|
||||
out = out + "\r\n"
|
||||
return out
|
||||
|
||||
fun send(this, body)
|
||||
print(this.header_str() + to_string(body))
|
||||
|
||||
fun escape_html(this, s)
|
||||
a = to_string(s)
|
||||
a = str_replace_all(a, "&", "&")
|
||||
a = str_replace_all(a, "<", "<")
|
||||
a = str_replace_all(a, ">", ">")
|
||||
a = str_replace_all(a, "\"", """)
|
||||
a = str_replace_all(a, "'", "'")
|
||||
return a
|
||||
|
||||
// Minimal url-decoder: '+' -> space; %XX for ASCII printable
|
||||
fun url_decode(this, s)
|
||||
src = to_string(s)
|
||||
// Simplified for parser-compatibility: only translate '+' to space
|
||||
return str_replace_all(src, "+", " ")
|
||||
|
||||
fun _merge_params(this, pairs)
|
||||
// pairs: array of [key, value] entries
|
||||
if (typeof(pairs) != "Array")
|
||||
return 0
|
||||
i = 0
|
||||
n = len(pairs)
|
||||
while (i < n)
|
||||
p = pairs[i]
|
||||
if (typeof(p) == "Array" && len(p) >= 2)
|
||||
key = to_string(p[0])
|
||||
val = to_string(p[1])
|
||||
a = this._params[key]
|
||||
if (typeof(a) != "Array")
|
||||
this._params[key] = []
|
||||
push(this._params[key], val)
|
||||
i = i + 1
|
||||
return 1
|
||||
|
||||
fun _parse_urlencoded(this, s)
|
||||
out = [] // array of [key, value]
|
||||
src = to_string(s)
|
||||
if (len(src) == 0)
|
||||
return out
|
||||
parts = str_split(src, "&")
|
||||
i = 0
|
||||
lp = len(parts)
|
||||
while (i < lp)
|
||||
kv = parts[i]
|
||||
if (typeof(kv) == "String" && len(kv) > 0)
|
||||
eq = find(kv, "=")
|
||||
if (eq >= 0)
|
||||
k = substr(kv, 0, eq)
|
||||
v = substr(kv, eq + 1, len(kv) - eq - 1)
|
||||
else
|
||||
k = kv
|
||||
v = ""
|
||||
key = this.url_decode(k)
|
||||
val = this.url_decode(v)
|
||||
push(out, [key, val])
|
||||
i = i + 1
|
||||
return out
|
||||
|
||||
fun _parse_cookies(this, cookie_str)
|
||||
out = {}
|
||||
if (len(cookie_str) == 0)
|
||||
return out
|
||||
semi = str_split(cookie_str, ";")
|
||||
i = 0
|
||||
while (i < len(semi))
|
||||
part = str_trim(semi[i])
|
||||
if (len(part) > 0)
|
||||
eq = find(part, "=")
|
||||
if (eq >= 0)
|
||||
k = str_trim(substr(part, 0, eq))
|
||||
v = str_trim(substr(part, eq + 1, len(part) - eq - 1))
|
||||
out[k] = v
|
||||
i = i + 1
|
||||
return out
|
||||
416
src/parser.c
416
src/parser.c
|
|
@ -63,6 +63,29 @@ static int g_err_col = 0;
|
|||
/* ---- compiler-generated temporary counter ---- */
|
||||
static int g_temp_counter = 0;
|
||||
|
||||
/* ---- runtime debug control (for suppressing noisy stdout in production/CGI) ---- */
|
||||
static int env_truthy(const char *name) {
|
||||
const char *v = getenv(name);
|
||||
if (!v) return 0;
|
||||
if (strcmp(v, "1") == 0) return 1;
|
||||
if (strcmp(v, "true") == 0) return 1;
|
||||
if (strcmp(v, "TRUE") == 0) return 1;
|
||||
if (strcmp(v, "yes") == 0) return 1;
|
||||
if (strcmp(v, "YES") == 0) return 1;
|
||||
if (strcmp(v, "on") == 0) return 1;
|
||||
if (strcmp(v, "ON") == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fun_debug_enabled(void) {
|
||||
/* Allow enabling debug dumps at runtime via environment.
|
||||
* Default is OFF to avoid contaminating stdout (e.g., CGI responses).
|
||||
*/
|
||||
if (env_truthy("FUN_TRACE")) return 1;
|
||||
if (env_truthy("FUN_DEBUG")) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Declared type metadata encoding in types[]:
|
||||
0 = dynamic/untyped;
|
||||
positive/negative 8/16/32/64 = integers (negative means signed);
|
||||
|
|
@ -4308,8 +4331,10 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos)
|
|||
return 0;
|
||||
}
|
||||
#ifdef FUN_DEBUG
|
||||
/* DEBUG: show compiled call */
|
||||
printf("compile: CALL %s with %d arg(s)\n", name, argc);
|
||||
/* DEBUG: show compiled call (only when FUN_DEBUG/FUN_TRACE enabled at runtime) */
|
||||
if (fun_debug_enabled()) {
|
||||
printf("compile: CALL %s with %d arg(s)\n", name, argc);
|
||||
}
|
||||
#endif
|
||||
bytecode_add_instruction(bc, OP_CALL, argc);
|
||||
/* postfix indexing, slice, and dot access/method calls */
|
||||
|
|
@ -5502,7 +5527,12 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si
|
|||
int gi = (lidx < 0) ? sym_index(name) : -1;
|
||||
skip_spaces(src, len, &local_pos);
|
||||
|
||||
/* object field assignment: name.field = expr (only if '=' follows) */
|
||||
/* object field assignment: supports
|
||||
- name.field = expr
|
||||
- name.field[expr] = expr
|
||||
- name.field[expr1][expr2] = expr
|
||||
If pattern doesn't match an assignment, fall back to expression stmt (e.g., method call).
|
||||
*/
|
||||
if (local_pos < len && src[local_pos] == '.') {
|
||||
size_t stmt_start = *pos; /* for expression fallback */
|
||||
size_t look = local_pos + 1; /* point after '.' */
|
||||
|
|
@ -5514,7 +5544,104 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si
|
|||
return;
|
||||
}
|
||||
skip_spaces(src, len, &look);
|
||||
if (look >= len || src[look] != '=') {
|
||||
if (look < len && src[look] == '[') {
|
||||
/* Handle name.field[...][...] = value */
|
||||
/* Load base container and resolve field: base[field] -> inner container on stack */
|
||||
if (lidx >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lidx);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi);
|
||||
}
|
||||
int fci = bytecode_add_constant(bc, make_string(fname));
|
||||
free(fname);
|
||||
bytecode_add_instruction(bc, OP_LOAD_CONST, fci);
|
||||
bytecode_add_instruction(bc, OP_INDEX_GET, 0);
|
||||
|
||||
/* Now parse one or more [expr] */
|
||||
for (;;) {
|
||||
if (!(look < len && src[look] == '[')) break;
|
||||
look++; /* consume '[' */
|
||||
if (!emit_expression(bc, src, len, &look)) {
|
||||
parser_fail(look, "Expected index expression after '['");
|
||||
free(name);
|
||||
return;
|
||||
}
|
||||
if (!consume_char(src, len, &look, ']')) {
|
||||
parser_fail(look, "Expected ']' after index");
|
||||
free(name);
|
||||
return;
|
||||
}
|
||||
skip_spaces(src, len, &look);
|
||||
if (look < len && src[look] == '[') {
|
||||
/* Need to dereference one level: inner = inner[index] */
|
||||
bytecode_add_instruction(bc, OP_INDEX_GET, 0);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* Expect '=' to assign into the last container with last index on stack */
|
||||
if (look >= len || src[look] != '=') {
|
||||
/* Not an assignment: fallback to expression statement */
|
||||
free(name);
|
||||
size_t expr_pos = stmt_start;
|
||||
if (emit_expression(bc, src, len, &expr_pos)) {
|
||||
bytecode_add_instruction(bc, OP_POP, 0);
|
||||
}
|
||||
*pos = expr_pos;
|
||||
skip_to_eol(src, len, pos);
|
||||
return;
|
||||
}
|
||||
look++; /* skip '=' */
|
||||
if (!emit_expression(bc, src, len, &look)) {
|
||||
parser_fail(look, "Expected expression after '='");
|
||||
free(name);
|
||||
return;
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_INDEX_SET, 0);
|
||||
free(name);
|
||||
*pos = look;
|
||||
skip_to_eol(src, len, pos);
|
||||
return;
|
||||
} else if (look < len && src[look] == '=') {
|
||||
/* Simple name.field = value
|
||||
* Previous implementation pushed (container, key) first and then evaluated value.
|
||||
* That relies on CALL preserving underlying stack entries. To be robust,
|
||||
* we first evaluate the value into a temporary local, then push (container, key),
|
||||
* then load the temporary and perform INDEX_SET.
|
||||
*/
|
||||
|
||||
/* Advance to after '=' and parse value into a temporary local */
|
||||
local_pos = look + 1;
|
||||
if (!emit_expression(bc, src, len, &local_pos)) {
|
||||
parser_fail(local_pos, "Expected expression after '='");
|
||||
free(fname);
|
||||
free(name);
|
||||
return;
|
||||
}
|
||||
/* store value to a hidden temp local */
|
||||
int tmp_local = local_add("__assign_tmp");
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, tmp_local);
|
||||
|
||||
/* Load container variable */
|
||||
if (lidx >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lidx);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi);
|
||||
}
|
||||
/* Push key */
|
||||
int kci = bytecode_add_constant(bc, make_string(fname));
|
||||
free(fname);
|
||||
bytecode_add_instruction(bc, OP_LOAD_CONST, kci);
|
||||
|
||||
/* Load value from temp and set */
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, tmp_local);
|
||||
bytecode_add_instruction(bc, OP_INDEX_SET, 0);
|
||||
|
||||
free(name);
|
||||
*pos = local_pos;
|
||||
skip_to_eol(src, len, pos);
|
||||
return;
|
||||
} else {
|
||||
/* Not an assignment: treat as expression statement (e.g., obj.method(...)) */
|
||||
free(fname);
|
||||
free(name);
|
||||
|
|
@ -5526,33 +5653,6 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si
|
|||
skip_to_eol(src, len, pos);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Confirmed assignment: emit container, key, value, then INDEX_SET */
|
||||
/* Load container variable */
|
||||
if (lidx >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lidx);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi);
|
||||
}
|
||||
/* Push key */
|
||||
int kci = bytecode_add_constant(bc, make_string(fname));
|
||||
free(fname);
|
||||
bytecode_add_instruction(bc, OP_LOAD_CONST, kci);
|
||||
|
||||
/* Advance local_pos to after '=' and parse value expression */
|
||||
local_pos = look + 1; /* skip '=' */
|
||||
if (!emit_expression(bc, src, len, &local_pos)) {
|
||||
parser_fail(local_pos, "Expected expression after '='");
|
||||
free(name);
|
||||
return;
|
||||
}
|
||||
/* perform set: pops value, key, container (in that order) */
|
||||
bytecode_add_instruction(bc, OP_INDEX_SET, 0);
|
||||
|
||||
free(name);
|
||||
*pos = local_pos;
|
||||
skip_to_eol(src, len, pos);
|
||||
return;
|
||||
}
|
||||
|
||||
/* array element assignment: name[expr] = expr and nested: name[expr1][expr2] = expr */
|
||||
|
|
@ -5879,7 +5979,7 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos,
|
|||
}
|
||||
|
||||
/* class definition -> factory function */
|
||||
if (starts_with(src, len, *pos, "class")) {
|
||||
if (starts_with(src, len, *pos, "class") && (*pos + 5 < len) && (src[*pos + 5] == ' ' || src[*pos + 5] == '\t')) {
|
||||
*pos += 5;
|
||||
skip_spaces(src, len, pos);
|
||||
/* class name */
|
||||
|
|
@ -6417,7 +6517,7 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos,
|
|||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(src, len, *pos, "fun")) {
|
||||
if (starts_with(src, len, *pos, "fun") && (*pos + 3 < len) && (src[*pos + 3] == ' ' || src[*pos + 3] == '\t')) {
|
||||
/* parse header: fun name(arg, ...) */
|
||||
*pos += 3;
|
||||
skip_spaces(src, len, pos);
|
||||
|
|
@ -6498,10 +6598,12 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos,
|
|||
bytecode_add_instruction(fn_bc, OP_RETURN, 0);
|
||||
|
||||
#ifdef FUN_DEBUG
|
||||
/* DEBUG: dump compiled function bytecode */
|
||||
printf("=== compiled function %s (%d params) ===\n", fname, env.count);
|
||||
bytecode_dump(fn_bc);
|
||||
printf("=== end function %s ===\n", fname);
|
||||
/* DEBUG: dump compiled function bytecode (guarded by runtime env) */
|
||||
if (fun_debug_enabled()) {
|
||||
printf("=== compiled function %s (%d params) ===\n", fname, env.count);
|
||||
bytecode_dump(fn_bc);
|
||||
printf("=== end function %s ===\n", fname);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* bind function to global: LOAD_CONST <fn> ; STORE_GLOBAL fgi */
|
||||
|
|
@ -6517,16 +6619,51 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos,
|
|||
/* for-sugar:
|
||||
* - for <ident> in range(a, b)
|
||||
* - for <ident> in <array-expr>
|
||||
* - for (<keyIdent>, <valIdent>) in <map-expr>
|
||||
*/
|
||||
if (starts_with(src, len, *pos, "for")) {
|
||||
*pos += 3;
|
||||
skip_spaces(src, len, pos);
|
||||
|
||||
/* loop variable name */
|
||||
char *ivar = NULL;
|
||||
if (!read_identifier_into(src, len, pos, &ivar)) {
|
||||
parser_fail(*pos, "Expected loop variable after 'for'");
|
||||
return;
|
||||
/* Optional tuple '(k, v)' for map iteration */
|
||||
int tuple_mode = 0; /* 0 = single var, 1 = (k,v) */
|
||||
char *ivar = NULL; /* single variable name OR key variable when tuple */
|
||||
char *vvar = NULL; /* value variable when tuple */
|
||||
|
||||
if (*pos < len && src[*pos] == '(') {
|
||||
/* Parse '(k, v)' */
|
||||
(*pos)++;
|
||||
skip_spaces(src, len, pos);
|
||||
if (!read_identifier_into(src, len, pos, &ivar)) {
|
||||
parser_fail(*pos, "Expected identifier after '(' in for tuple");
|
||||
return;
|
||||
}
|
||||
skip_spaces(src, len, pos);
|
||||
if (!consume_char(src, len, pos, ',')) {
|
||||
parser_fail(*pos, "Expected ',' between key and value in for tuple");
|
||||
free(ivar);
|
||||
return;
|
||||
}
|
||||
skip_spaces(src, len, pos);
|
||||
if (!read_identifier_into(src, len, pos, &vvar)) {
|
||||
parser_fail(*pos, "Expected value identifier after ',' in for tuple");
|
||||
free(ivar);
|
||||
return;
|
||||
}
|
||||
skip_spaces(src, len, pos);
|
||||
if (!consume_char(src, len, pos, ')')) {
|
||||
parser_fail(*pos, "Expected ')' to close for tuple");
|
||||
free(ivar);
|
||||
free(vvar);
|
||||
return;
|
||||
}
|
||||
tuple_mode = 1;
|
||||
} else {
|
||||
/* loop variable name */
|
||||
if (!read_identifier_into(src, len, pos, &ivar)) {
|
||||
parser_fail(*pos, "Expected loop variable after 'for'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
skip_spaces(src, len, pos);
|
||||
|
|
@ -6672,7 +6809,7 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos,
|
|||
|
||||
free(ivar);
|
||||
continue;
|
||||
} else {
|
||||
} else if (!tuple_mode) {
|
||||
/* ===== array iteration: for ivar in <expr> ===== */
|
||||
/* Evaluate the iterable once and store in a temp */
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
|
|
@ -6819,6 +6956,199 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos,
|
|||
|
||||
free(ivar);
|
||||
continue;
|
||||
} else {
|
||||
/* ===== map iteration with tuple: for (k, v) in <expr> ===== */
|
||||
/* Evaluate the map expr once: store in temp */
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
parser_fail(*pos, "Expected map expression after 'in'");
|
||||
free(ivar);
|
||||
free(vvar);
|
||||
return;
|
||||
}
|
||||
char mapname[64];
|
||||
snprintf(mapname, sizeof(mapname), "__for_map_%d", g_temp_counter++);
|
||||
int lmap = -1, gmap = -1;
|
||||
if (g_locals) {
|
||||
lmap = local_add(mapname);
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, lmap);
|
||||
} else {
|
||||
gmap = sym_index(mapname);
|
||||
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gmap);
|
||||
}
|
||||
|
||||
/* keys = keys(map) */
|
||||
if (lmap >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lmap);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gmap);
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_KEYS, 0);
|
||||
char keysname[64];
|
||||
snprintf(keysname, sizeof(keysname), "__for_keys_%d", g_temp_counter++);
|
||||
int lkeys = -1, gkeys = -1;
|
||||
if (g_locals) {
|
||||
lkeys = local_add(keysname);
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, lkeys);
|
||||
} else {
|
||||
gkeys = sym_index(keysname);
|
||||
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gkeys);
|
||||
}
|
||||
|
||||
/* len(keys) */
|
||||
if (lkeys >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lkeys);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gkeys);
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_LEN, 0);
|
||||
char lenname[64];
|
||||
snprintf(lenname, sizeof(lenname), "__for_klen_%d", g_temp_counter++);
|
||||
int llen = -1, glen = -1;
|
||||
if (g_locals) {
|
||||
llen = local_add(lenname);
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, llen);
|
||||
} else {
|
||||
glen = sym_index(lenname);
|
||||
bytecode_add_instruction(bc, OP_STORE_GLOBAL, glen);
|
||||
}
|
||||
|
||||
/* i = 0 */
|
||||
int c0m = bytecode_add_constant(bc, make_int(0));
|
||||
bytecode_add_instruction(bc, OP_LOAD_CONST, c0m);
|
||||
char iname[64];
|
||||
snprintf(iname, sizeof(iname), "__for_ki_%d", g_temp_counter++);
|
||||
int li = -1, gi = -1;
|
||||
if (g_locals) {
|
||||
li = local_add(iname);
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, li);
|
||||
} else {
|
||||
gi = sym_index(iname);
|
||||
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gi);
|
||||
}
|
||||
|
||||
/* end of header line */
|
||||
skip_to_eol(src, len, pos);
|
||||
|
||||
/* loop start */
|
||||
int loop_start = bc->instr_count;
|
||||
/* condition: i < len */
|
||||
if (li >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, li);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi);
|
||||
}
|
||||
if (llen >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, llen);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, glen);
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_LT, 0);
|
||||
int jmp_false = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0);
|
||||
|
||||
/* key = keys[i] */
|
||||
if (lkeys >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lkeys);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gkeys);
|
||||
}
|
||||
if (li >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, li);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi);
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_INDEX_GET, 0);
|
||||
|
||||
/* assign to key variable (ivar) */
|
||||
int lk = local_find(ivar);
|
||||
int gk = -1;
|
||||
if (lk < 0) {
|
||||
if (g_locals)
|
||||
lk = local_add(ivar);
|
||||
else
|
||||
gk = sym_index(ivar);
|
||||
}
|
||||
if (lk >= 0) {
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, lk);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gk);
|
||||
}
|
||||
|
||||
/* value = map[key] */
|
||||
if (lmap >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lmap);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gmap);
|
||||
}
|
||||
/* load key again */
|
||||
if (lk >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, lk);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gk);
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_INDEX_GET, 0);
|
||||
|
||||
/* assign to value variable (vvar) */
|
||||
int lv = local_find(vvar);
|
||||
int gv = -1;
|
||||
if (lv < 0) {
|
||||
if (g_locals)
|
||||
lv = local_add(vvar);
|
||||
else
|
||||
gv = sym_index(vvar);
|
||||
}
|
||||
if (lv >= 0) {
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, lv);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gv);
|
||||
}
|
||||
|
||||
/* enter loop context */
|
||||
LoopCtx ctx = {{0}, 0, {0}, 0, g_loop_ctx};
|
||||
g_loop_ctx = &ctx;
|
||||
|
||||
/* body */
|
||||
int body_indent = 0;
|
||||
size_t look_body = *pos;
|
||||
if (read_line_start(src, len, &look_body, &body_indent) && body_indent > current_indent) {
|
||||
parse_block(bc, src, len, pos, body_indent);
|
||||
} else {
|
||||
/* empty body ok */
|
||||
}
|
||||
|
||||
/* continue target: i = i + 1 */
|
||||
int cont_label = bc->instr_count;
|
||||
int c1m = bytecode_add_constant(bc, make_int(1));
|
||||
if (li >= 0) {
|
||||
bytecode_add_instruction(bc, OP_LOAD_LOCAL, li);
|
||||
bytecode_add_instruction(bc, OP_LOAD_CONST, c1m);
|
||||
bytecode_add_instruction(bc, OP_ADD, 0);
|
||||
bytecode_add_instruction(bc, OP_STORE_LOCAL, li);
|
||||
} else {
|
||||
bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi);
|
||||
bytecode_add_instruction(bc, OP_LOAD_CONST, c1m);
|
||||
bytecode_add_instruction(bc, OP_ADD, 0);
|
||||
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gi);
|
||||
}
|
||||
|
||||
/* back edge */
|
||||
bytecode_add_instruction(bc, OP_JUMP, loop_start);
|
||||
|
||||
/* end label */
|
||||
int end_label = bc->instr_count;
|
||||
bytecode_set_operand(bc, jmp_false, end_label);
|
||||
|
||||
/* patch continue/break */
|
||||
for (int bi = 0; bi < ctx.cont_count; ++bi) {
|
||||
bytecode_set_operand(bc, ctx.continue_jumps[bi], cont_label);
|
||||
}
|
||||
for (int bi = 0; bi < ctx.break_count; ++bi) {
|
||||
bytecode_set_operand(bc, ctx.break_jumps[bi], end_label);
|
||||
}
|
||||
g_loop_ctx = ctx.prev;
|
||||
|
||||
free(ivar);
|
||||
free(vvar);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue