diff --git a/CMakeLists.txt b/CMakeLists.txt index d366211..0e1f033 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.21.3 LANGUAGES C) +project(fun VERSION 0.21.4 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/exit_example.fun b/examples/exit_example.fun new file mode 100755 index 0000000..a43f1dd --- /dev/null +++ b/examples/exit_example.fun @@ -0,0 +1,21 @@ +#!/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-04 + */ + +/* Demonstrate exiting a Fun script with a specific exit code + * Run: + * fun examples/exit_example.fun + * Then in your shell, check the code with: echo $? + */ + +/* exit with code 7 */ +exit 7 diff --git a/src/bytecode.h b/src/bytecode.h index 30a7e3f..c40f07b 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -157,7 +157,10 @@ typedef enum { OP_SOCK_RECV, // pops maxlen, fd; returns data string ("" on EOF/error) OP_SOCK_CLOSE, // pops fd; returns 1/0 OP_SOCK_UNIX_LISTEN, // pops backlog, path; returns listen fd (>0) or 0 - OP_SOCK_UNIX_CONNECT // pops path; returns fd (>0) or 0 + OP_SOCK_UNIX_CONNECT, // pops path; returns fd (>0) or 0 + + // process control + OP_EXIT // pops code (or uses operand) and terminates script with exit code } OpCode; typedef struct { diff --git a/src/fun.c b/src/fun.c index 40f7d86..7d3d2cd 100644 --- a/src/fun.c +++ b/src/fun.c @@ -74,7 +74,7 @@ int main(int argc, char **argv) { vm_print_output(&vm); vm_clear_output(&vm); bytecode_free(bc); - return 0; + return vm.exit_code; } #ifdef FUN_WITH_REPL diff --git a/src/parser.c b/src/parser.c index f3edbf5..86efa72 100644 --- a/src/parser.c +++ b/src/parser.c @@ -2070,6 +2070,25 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si return; } + /* exit statement: exit [expr]? */ + if (strcmp(name, "exit") == 0) { + free(name); + skip_spaces(src, len, &local_pos); + size_t save_pos = local_pos; + if (emit_expression(bc, src, len, &local_pos)) { + /* expression result already on stack */ + } else { + /* default exit code 0 */ + local_pos = save_pos; + int ci = bytecode_add_constant(bc, make_int(0)); + bytecode_add_instruction(bc, OP_LOAD_CONST, ci); + } + bytecode_add_instruction(bc, OP_EXIT, 0); + *pos = local_pos; + skip_to_eol(src, len, pos); + return; + } + /* break / continue */ if (strcmp(name, "break") == 0) { free(name); diff --git a/src/vm.c b/src/vm.c index b805a9d..a074024 100644 --- a/src/vm.c +++ b/src/vm.c @@ -139,6 +139,8 @@ void vm_reset(VM *vm) { } // Clear output buffer vm_clear_output(vm); + // Reset exit code + vm->exit_code = 0; } void vm_dump_globals(VM *vm) { @@ -180,6 +182,7 @@ void vm_init(VM *vm) { vm->fp = -1; vm->output_count = 0; vm->instr_count = 0; + vm->exit_code = 0; for (int i = 0; i < MAX_GLOBALS; ++i) vm->globals[i] = make_nil(); } @@ -280,6 +283,7 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/core/call.c" #include "vm/core/dup.c" #include "vm/core/halt.c" + #include "vm/core/exit.c" #include "vm/core/jump.c" #include "vm/core/jump_if_false.c" #include "vm/core/load_const.c" diff --git a/src/vm.h b/src/vm.h index 952761a..f406521 100644 --- a/src/vm.h +++ b/src/vm.h @@ -39,7 +39,8 @@ static const char *opcode_names[] = { "THREAD_SPAWN","THREAD_JOIN","SLEEP_MS", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "PCSC_ESTABLISH","PCSC_RELEASE","PCSC_LIST_READERS","PCSC_CONNECT","PCSC_DISCONNECT","PCSC_TRANSMIT", - "SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT" + "SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT", + "EXIT" }; typedef struct { @@ -63,6 +64,8 @@ typedef struct { long long instr_count; // executed instructions in the last vm_run int current_line; // last executed source line (debug) + + int exit_code; // process exit code set by OP_EXIT } VM; // initialize VM (zero state) @@ -82,7 +85,7 @@ void vm_dump_globals(VM *vm); void vm_run(VM *vm, Bytecode *entry); static inline int opcode_is_valid(int op) { - return op >= OP_NOP && op <= OP_SOCK_UNIX_CONNECT; // all current opcodes + return op >= OP_NOP && op <= OP_EXIT; // all current opcodes } #endif diff --git a/src/vm/core/exit.c b/src/vm/core/exit.c new file mode 100644 index 0000000..d919a4e --- /dev/null +++ b/src/vm/core/exit.c @@ -0,0 +1,41 @@ +/** + * 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-04 + */ + +/** +* @file exit.c + * @brief Implements the OP_EXIT opcode to terminate the script with an exit code. + * + * Behavior: + * - Pops a value from the stack (if available) and converts it to an integer exit code. + * - Sets vm->exit_code. + * - Stops the VM execution immediately (returns from vm_run). + */ + +case OP_EXIT: { + int code = 0; + if (vm->sp >= 0) { + Value v = pop_value(vm); + if (v.type == VAL_INT) { + code = (int)v.i; + } else if (v.type == VAL_STRING) { + /* best-effort parse number from string */ + code = (int)strtoll(v.s, NULL, 10); + } else if (v.type == VAL_NIL) { + code = 0; + } else { + /* unsupported type for exit; default to 0 */ + code = 0; + } + free_value(v); + } + vm->exit_code = code; + return; +}