From cb3eb0a49841091dfefbfd2a9b666c5810cd4936 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 27 Mar 2026 00:44:40 +0100 Subject: [PATCH] Added some basic asyncio stuff. (0.39.12) --- CMakeLists.txt | 2 +- examples/io/async_http_client.fun | 91 +++++++++++++++++++++++++++++++ src/bytecode.h | 5 ++ src/parser.c | 82 ++++++++++++++++++++++++++++ src/vm.c | 8 +++ src/vm.h | 1 + src/vm/os/fd_poll_read.c | 50 +++++++++++++++++ src/vm/os/fd_poll_write.c | 50 +++++++++++++++++ src/vm/os/fd_set_nonblock.c | 40 ++++++++++++++ 9 files changed, 328 insertions(+), 1 deletion(-) create mode 100755 examples/io/async_http_client.fun create mode 100644 src/vm/os/fd_poll_read.c create mode 100644 src/vm/os/fd_poll_write.c create mode 100644 src/vm/os/fd_set_nonblock.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a49396..039818c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.39.11 LANGUAGES C) +project(fun VERSION 0.39.12 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/io/async_http_client.fun b/examples/io/async_http_client.fun new file mode 100755 index 0000000..6504b6c --- /dev/null +++ b/examples/io/async_http_client.fun @@ -0,0 +1,91 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-26 + */ + +/* + * Async-ish HTTP GET using new FD helper opcodes + * + * Demonstrates: + * - tcp_connect(host, port) + * - fd_set_nonblock(fd, on) + * - fd_poll_write(fd, timeout_ms) and fd_poll_read(fd, timeout_ms) + * - non-blocking send/recv loop + */ + +host = "example.org" +port = 80 + +fd = tcp_connect(host, port) +if (fd == 0) + print("connect failed") + exit(1) + +ok = fd_set_nonblock(fd, 1) +if (ok == 0) + print("failed to set nonblocking") + sock_close(fd) + exit(1) + +req = "GET / HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n" +to_send = len(req) +sent_total = 0 + +// Write request in a non-blocking fashion +while (sent_total < to_send) + // Wait until socket is writable (timeout 1000ms) + wr = fd_poll_write(fd, 1000) + if (wr < 0) + print("poll write error") + sock_close(fd) + exit(1) + if (wr == 0) + // timeout, loop again + continue + + chunk = substr(req, sent_total, to_send - sent_total) + n = sock_send(fd, chunk) + if (n < 0) + print("send error") + sock_close(fd) + exit(1) + sent_total = sent_total + n + +// Read response non-blocking until peer closes +buf = "" +while (true) + rd = fd_poll_read(fd, 2000) // wait up to 2s for data + if (rd < 0) + print("poll read error") + break + if (rd == 0) + // timeout or EOF; try a final read to see if closed + data = sock_recv(fd, 4096) + if (len(data) == 0) + // assume connection closed + break + buf = buf + data + continue + + data = sock_recv(fd, 4096) + if (len(data) == 0) + // closed + break + buf = buf + data + +sock_close(fd) + +// Print the first lines of the response to show it's working +print(substr(buf, 0, 200)) + +/* +Expected: prints the beginning of an HTTP response from example.org +*/ diff --git a/src/bytecode.h b/src/bytecode.h index 555ebd8..cacf59f 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -220,6 +220,11 @@ typedef enum { OP_SOCK_UNIX_LISTEN, // pops backlog, path; returns listen fd (>0) or 0 OP_SOCK_UNIX_CONNECT, // pops path; returns fd (>0) or 0 + // Async-friendly FD helpers (UNIX platforms) + OP_FD_SET_NONBLOCK, // pops on:int (0/1), fd:int; returns 1 on success, 0 on error/unsupported + OP_FD_POLL_READ, // pops timeout_ms:int, fd:int; returns 1 if readable, 0 if timeout/EOF, -1 on error + OP_FD_POLL_WRITE, // pops timeout_ms:int, fd:int; returns 1 if writable, 0 if timeout, -1 on error + // process control OP_EXIT, // pops code (or uses operand) and terminates script with exit code diff --git a/src/parser.c b/src/parser.c index f40cdd5..e964b28 100644 --- a/src/parser.c +++ b/src/parser.c @@ -2853,6 +2853,88 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* Async-friendly FD helpers */ + if (strcmp(name, "fd_set_nonblock") == 0) { + (*pos)++; /* '(' */ + /* Expect (fd, on) -> push fd then on so VM pops on first */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "fd_set_nonblock expects (fd, on)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "fd_set_nonblock expects (fd, on)"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "fd_set_nonblock expects (fd, on)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after fd_set_nonblock args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_FD_SET_NONBLOCK, 0); + free(name); + return 1; + } + if (strcmp(name, "fd_poll_read") == 0) { + (*pos)++; /* '(' */ + /* Expect (fd, timeout_ms) -> push fd then timeout so VM pops timeout first */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "fd_poll_read expects (fd, timeout_ms)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "fd_poll_read expects (fd, timeout_ms)"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "fd_poll_read expects (fd, timeout_ms)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after fd_poll_read args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_FD_POLL_READ, 0); + free(name); + return 1; + } + if (strcmp(name, "fd_poll_write") == 0) { + (*pos)++; /* '(' */ + /* Expect (fd, timeout_ms) */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "fd_poll_write expects (fd, timeout_ms)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "fd_poll_write expects (fd, timeout_ms)"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "fd_poll_write expects (fd, timeout_ms)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after fd_poll_write args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_FD_POLL_WRITE, 0); + free(name); + return 1; + } /* Serial builtins */ if (strcmp(name, "serial_open") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index 140eead..e32fd04 100644 --- a/src/vm.c +++ b/src/vm.c @@ -36,6 +36,9 @@ #include /* For hidden input (password) handling in OP_INPUT_LINE */ #include +/* For non-blocking sockets and polling */ +#include +#include // #include #endif @@ -857,6 +860,11 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/os/socket_unix_connect.c" #include "vm/os/socket_unix_listen.c" +/* Async-friendly FD helpers (UNIX) */ +#include "vm/os/fd_set_nonblock.c" +#include "vm/os/fd_poll_read.c" +#include "vm/os/fd_poll_write.c" + #ifdef FUN_WITH_PCSC #include "vm/pcsc/connect.c" #include "vm/pcsc/disconnect.c" diff --git a/src/vm.h b/src/vm.h index ca07050..8b47615 100644 --- a/src/vm.h +++ b/src/vm.h @@ -49,6 +49,7 @@ static const char *opcode_names[] = { "INI_LOAD", "INI_FREE", "INI_GET_STRING", "INI_GET_INT", "INI_GET_DOUBLE", "INI_GET_BOOL", "INI_SET", "INI_UNSET", "INI_SAVE", "XML_PARSE", "XML_ROOT", "XML_NAME", "XML_TEXT", "SOCK_TCP_LISTEN", "SOCK_TCP_ACCEPT", "SOCK_TCP_CONNECT", "SOCK_SEND", "SOCK_RECV", "SOCK_CLOSE", "SOCK_UNIX_LISTEN", "SOCK_UNIX_CONNECT", + "FD_SET_NONBLOCK", "FD_POLL_READ", "FD_POLL_WRITE", "EXIT", "OS_LIST_DIR", "TK_BIND", diff --git a/src/vm/os/fd_poll_read.c b/src/vm/os/fd_poll_read.c new file mode 100644 index 0000000..6c7d61a --- /dev/null +++ b/src/vm/os/fd_poll_read.c @@ -0,0 +1,50 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-26 + */ + +case OP_FD_POLL_READ: { + /* Pops timeout_ms:int, fd:int; pushes 1 if readable, 0 on timeout/EOF, -1 on error */ + Value to = pop_value(vm); + Value fdv = pop_value(vm); + int rc = -1; +#ifdef __unix__ + if (fdv.type != VAL_INT || to.type != VAL_INT) { + fprintf(stderr, "Runtime type error: fd_poll_read expects (int fd, int timeout_ms)\n"); + rc = -1; + } else { + int fd = (int)fdv.i; + int timeout_ms = (int)to.i; + struct pollfd pfd; + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + int pr = poll(&pfd, 1, timeout_ms); + if (pr > 0) { + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + rc = 0; /* treat as not readable / peer closed */ + } else if (pfd.revents & POLLIN) { + rc = 1; + } else { + rc = 0; + } + } else if (pr == 0) { + rc = 0; /* timeout */ + } else { + rc = -1; /* error */ + } + } +#else + (void)to; (void)fdv; +#endif + free_value(to); + free_value(fdv); + push_value(vm, make_int(rc)); + break; +} diff --git a/src/vm/os/fd_poll_write.c b/src/vm/os/fd_poll_write.c new file mode 100644 index 0000000..508c42c --- /dev/null +++ b/src/vm/os/fd_poll_write.c @@ -0,0 +1,50 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-26 + */ + +case OP_FD_POLL_WRITE: { + /* Pops timeout_ms:int, fd:int; pushes 1 if writable, 0 on timeout, -1 on error */ + Value to = pop_value(vm); + Value fdv = pop_value(vm); + int rc = -1; +#ifdef __unix__ + if (fdv.type != VAL_INT || to.type != VAL_INT) { + fprintf(stderr, "Runtime type error: fd_poll_write expects (int fd, int timeout_ms)\n"); + rc = -1; + } else { + int fd = (int)fdv.i; + int timeout_ms = (int)to.i; + struct pollfd pfd; + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + int pr = poll(&pfd, 1, timeout_ms); + if (pr > 0) { + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + rc = 0; /* treat as not writable */ + } else if (pfd.revents & POLLOUT) { + rc = 1; + } else { + rc = 0; + } + } else if (pr == 0) { + rc = 0; /* timeout */ + } else { + rc = -1; /* error */ + } + } +#else + (void)to; (void)fdv; +#endif + free_value(to); + free_value(fdv); + push_value(vm, make_int(rc)); + break; +} diff --git a/src/vm/os/fd_set_nonblock.c b/src/vm/os/fd_set_nonblock.c new file mode 100644 index 0000000..51e5135 --- /dev/null +++ b/src/vm/os/fd_set_nonblock.c @@ -0,0 +1,40 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-26 + */ + +case OP_FD_SET_NONBLOCK: { + /* Pops on:int (0/1), fd:int; pushes 1 on success, 0 on error/unsupported */ + Value onv = pop_value(vm); + Value fdv = pop_value(vm); + int ok = 0; +#ifdef __unix__ + if (fdv.type != VAL_INT || onv.type != VAL_INT) { + fprintf(stderr, "Runtime type error: fd_set_nonblock expects (int fd, int on)\n"); + ok = 0; + } else { + int fd = (int)fdv.i; + int on = (int)onv.i; + int flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) { + if (on) + flags |= O_NONBLOCK; + else + flags &= ~O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) == 0) ok = 1; + } + } +#else + (void)onv; (void)fdv; +#endif + free_value(onv); + free_value(fdv); + push_value(vm, make_int(ok ? 1 : 0)); + break; +}