diff --git a/CMakeLists.txt b/CMakeLists.txt index f2621ea..81c3ace 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/examples/blocking/http_server_cgi.fun b/examples/blocking/http_server_cgi.fun new file mode 100644 index 0000000..bb14147 --- /dev/null +++ b/examples/blocking/http_server_cgi.fun @@ -0,0 +1,185 @@ +#!/usr/bin/env fun + +/* + * Minimal CGI-capable HTTP server (blocking) + */ + +#include +#include +#include + +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", "

CGI produced no output

") + 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", "

404 Not Found

") + + 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() diff --git a/examples/data/htdocs/hello.fun b/examples/data/htdocs/hello.fun new file mode 100644 index 0000000..2512d14 --- /dev/null +++ b/examples/data/htdocs/hello.fun @@ -0,0 +1,28 @@ +#!/usr/bin/env fun + +#include + +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 + "Fun CGI" +html = html + "

Hello, " + cgi.escape_html(name) + "!

" +html = html + "

REQUEST_METHOD: " + cgi.escape_html(cgi.env["REQUEST_METHOD"]) + "

" +html = html + "

QUERY_STRING: " + cgi.escape_html(cgi.env["QUERY_STRING"]) + "

" +html = html + "

User-Agent cookie: " + cgi.escape_html(ua) + "

" +html = html + "

Params

" + to_string(cgi.params()) + "
" +html = html + "" + +// Send CGI headers + body +cgi.send(html) diff --git a/lib/net/cgi.fun b/lib/net/cgi.fun new file mode 100644 index 0000000..d16af35 --- /dev/null +++ b/lib/net/cgi.fun @@ -0,0 +1,182 @@ +// Minimal, parser-friendly CGI helper (incrementally extend as needed) + +#include + +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 diff --git a/src/parser.c b/src/parser.c index abb6711..f40cdd5 100644 --- a/src/parser.c +++ b/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 ; 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 in range(a, b) * - for in + * - for (, ) in */ 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 ===== */ /* 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 ===== */ + /* 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; } }