From 1ac163a01e715cf690da9fb6f178792cf77020d1 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 2 Oct 2025 01:50:53 +0200 Subject: [PATCH] Added proc_run() and system() for processes and added to stdlib as class. (0.16.6) --- CMakeLists.txt | 2 +- examples/process_example.fun | 60 +++++++++++++++++++++ lib/core/README.md | 3 -- lib/core/string_to_bytes_ascii.fun | 12 ----- lib/io/process.fun | 47 ++++++++++++++++ src/bytecode.h | 2 + src/parser.c | 16 ++++++ src/vm.c | 2 + src/vm.h | 2 +- src/vm/os/proc_run.c | 87 ++++++++++++++++++++++++++++++ src/vm/os/proc_system.c | 37 +++++++++++++ 11 files changed, 253 insertions(+), 17 deletions(-) create mode 100755 examples/process_example.fun delete mode 100644 lib/core/README.md delete mode 100644 lib/core/string_to_bytes_ascii.fun create mode 100644 lib/io/process.fun create mode 100644 src/vm/os/proc_run.c create mode 100644 src/vm/os/proc_system.c diff --git a/CMakeLists.txt b/CMakeLists.txt index adcf949..7e3a7eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.16.5 LANGUAGES C) +project(fun VERSION 0.16.6 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/process_example.fun b/examples/process_example.fun new file mode 100755 index 0000000..23fed19 --- /dev/null +++ b/examples/process_example.fun @@ -0,0 +1,60 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-02 + */ + +/* + * Subprocess example using proc_run/system and the Process class. + */ + +#include + +print("=== Built-ins ===") +r = proc_run("echo Hello from Fun") +print(r["out"]) +print(join(["exit code: ", to_string(r["code"])], "")) + +code = system("sh -c 'exit 3'") +print(join(["system() exit code: ", to_string(code)], "")) + +print("=== Process class ===") +p = Process() +res = p.run("uname -s") +print(join(["OS: ", res["out"]], "")) + +res2 = p.run_merge_stderr("sh -c 'echo out; echo err 1>&2; exit 1'") +print("merged output:") +print(res2["out"]) +print(join(["exit code: ", to_string(res2["code"])], "")) + +ok = p.check_call("true") +print(join(["check_call(true): ", to_string(ok)], "")) + +ok2 = p.check_call("false") +print(join(["check_call(false): ", to_string(ok2)], "")) + +/* Expected output: +=== Built-ins === +Hello from Fun + +exit code: 0 +system() exit code: 3 +=== Process class === +OS: Linux + +merged output: +out +err + +exit code: 1 +check_call(true): 1 +check_call(false): 0 +*/ diff --git a/lib/core/README.md b/lib/core/README.md deleted file mode 100644 index 5f8ae73..0000000 --- a/lib/core/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# README - -In this directory every file should just contain one function if possible! diff --git a/lib/core/string_to_bytes_ascii.fun b/lib/core/string_to_bytes_ascii.fun deleted file mode 100644 index 09de6fd..0000000 --- a/lib/core/string_to_bytes_ascii.fun +++ /dev/null @@ -1,12 +0,0 @@ -/* - * This file is part of the Fun programming language. - * https://hanez.org/project/fun/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * Added: 2025-10-01 - */ - - diff --git a/lib/io/process.fun b/lib/io/process.fun new file mode 100644 index 0000000..0f5f5c3 --- /dev/null +++ b/lib/io/process.fun @@ -0,0 +1,47 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-02 + */ + +/* + * Process utilities built on proc_run() and system(). + * + * Methods: + * run(cmd) -> map { "out": string, "code": number } + * run_merge_stderr(cmd) -> same, but merges stderr into stdout (" 2>&1") + * system(cmd) -> exit code number + * check_call(cmd) -> 1 if exit code==0 else 0 + */ + +class Process() + // Execute command and capture stdout and exit code + fun run(this, cmd) + return proc_run(to_string(cmd)) + + // Merge stderr into stdout using shell redirection + fun run_merge_stderr(this, cmd) + c = to_string(cmd) + // Add a single space if needed + if (len(c) > 0) && (substr(c, len(c) - 1, 1) != " ") + c = join([c, " 2>&1"], "") + else + c = join([c, "2>&1"], "") + return proc_run(c) + + // Return the exit code of the command + fun system(this, cmd) + return system(to_string(cmd)) + + // Return 1 if command succeeds (exit code 0), else 0 + fun check_call(this, cmd) + code = system(to_string(cmd)) + if (code == 0) + return 1 + else + return 0 diff --git a/src/bytecode.h b/src/bytecode.h index 9481a3c..5f7b2f7 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -115,6 +115,8 @@ typedef enum { // OS OP_ENV, // pops name string; pushes value string (or "") OP_INPUT_LINE, // operand: 0=no prompt; 1=has prompt. Pops [prompt?]; pushes input string (no trailing newline) + OP_PROC_RUN, // pops command string; pushes map {"out": string, "code": int} + OP_PROC_SYSTEM, // pops command string; pushes exit code number // Threads OP_THREAD_SPAWN, // operand: 0=no args, 1=has args; pops [args?], fn; pushes thread id (int>0) diff --git a/src/parser.c b/src/parser.c index 32f31bf..530faad 100644 --- a/src/parser.c +++ b/src/parser.c @@ -654,6 +654,22 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + if (strcmp(name, "proc_run") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "proc_run expects 1 argument (command string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after proc_run arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_PROC_RUN, 0); + free(name); + return 1; + } + if (strcmp(name, "system") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "system expects 1 argument (command string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after system arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_PROC_SYSTEM, 0); + free(name); + return 1; + } if (strcmp(name, "env") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "env expects 1 argument"); free(name); return 0; } diff --git a/src/vm.c b/src/vm.c index 46f359b..8cd6589 100644 --- a/src/vm.c +++ b/src/vm.c @@ -319,6 +319,8 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/os/sleep_ms.c" #include "vm/os/thread_join.c" #include "vm/os/thread_spawn.c" + #include "vm/os/proc_run.c" + #include "vm/os/proc_system.c" #include "vm/strings/find.c" #include "vm/strings/split.c" diff --git a/src/vm.h b/src/vm.h index 18fae9e..4daaf77 100644 --- a/src/vm.h +++ b/src/vm.h @@ -33,7 +33,7 @@ static const char *opcode_names[] = { "ENUMERATE","ZIP", "MIN","MAX","CLAMP","ABS","POW","RANDOM_SEED","RANDOM_INT", "MAKE_MAP","KEYS","VALUES","HAS_KEY", - "READ_FILE","WRITE_FILE","ENV","INPUT_LINE", + "READ_FILE","WRITE_FILE","ENV","INPUT_LINE","PROC_RUN","PROC_SYSTEM", "THREAD_SPAWN","THREAD_JOIN","SLEEP_MS", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR" }; diff --git a/src/vm/os/proc_run.c b/src/vm/os/proc_run.c new file mode 100644 index 0000000..6368664 --- /dev/null +++ b/src/vm/os/proc_run.c @@ -0,0 +1,87 @@ +/** + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-02 + */ + +#ifdef __unix__ +#include +#endif + +case OP_PROC_RUN: { + /* Pops command string; pushes map {"out": string, "code": int} */ + Value cmdv = pop_value(vm); + char *cmd = value_to_string_alloc(&cmdv); + free_value(cmdv); + if (!cmd) { + Value m = make_map_empty(); + map_set(&m, "out", make_string("")); + map_set(&m, "code", make_int(-1)); + push_value(vm, m); + break; + } + + /* Use popen to run via shell and capture stdout */ + FILE *fp = popen(cmd, "r"); + int exit_code = -1; + char *out = NULL; + size_t cap = 0, len = 0; + if (fp) { + cap = 4096; + out = (char*)malloc(cap); + if (!out) { + pclose(fp); + free(cmd); + Value m = make_map_empty(); + map_set(&m, "out", make_string("")); + map_set(&m, "code", make_int(-1)); + push_value(vm, m); + break; + } + int c; + while ((c = fgetc(fp)) != EOF) { + if (len + 1 >= cap) { + cap *= 2; + char *nb = (char*)realloc(out, cap); + if (!nb) { + free(out); + pclose(fp); + free(cmd); + Value m = make_map_empty(); + map_set(&m, "out", make_string("")); + map_set(&m, "code", make_int(-1)); + push_value(vm, m); + goto done_push; + } + out = nb; + } + out[len++] = (char)c; + } + out[len] = '\0'; + int status = pclose(fp); +#ifdef __unix__ + if (WIFEXITED(status)) exit_code = WEXITSTATUS(status); + else exit_code = -1; +#else + exit_code = status; +#endif + } else { + out = strdup(""); + exit_code = -1; + } + + Value m = make_map_empty(); + map_set(&m, "out", make_string(out ? out : "")); + map_set(&m, "code", make_int(exit_code)); + push_value(vm, m); + +done_push: + if (out) free(out); + free(cmd); + break; +} diff --git a/src/vm/os/proc_system.c b/src/vm/os/proc_system.c new file mode 100644 index 0000000..fab9479 --- /dev/null +++ b/src/vm/os/proc_system.c @@ -0,0 +1,37 @@ +/** + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-02 + */ + +#ifdef __unix__ +#include +#endif + +case OP_PROC_SYSTEM: { + /* Pops command string; pushes exit code number */ + Value cmdv = pop_value(vm); + char *cmd = value_to_string_alloc(&cmdv); + free_value(cmdv); + if (!cmd) { + push_value(vm, make_int(-1)); + break; + } + int status = system(cmd); + int code = -1; +#ifdef __unix__ + if (status == -1) code = -1; + else if (WIFEXITED(status)) code = WEXITSTATUS(status); + else code = -1; +#else + code = status; +#endif + push_value(vm, make_int(code)); + free(cmd); + break; +}