diff --git a/CMakeLists.txt b/CMakeLists.txt index faa6517..0383cff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.10 LANGUAGES C) +project(fun VERSION 0.37.11 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/byte_for_demo.fun b/examples/error/byte_for_demo.fun similarity index 100% rename from examples/byte_for_demo.fun rename to examples/error/byte_for_demo.fun diff --git a/examples/random_demo.fun b/examples/random_demo.fun old mode 100644 new mode 100755 diff --git a/examples/random_number_example.fun b/examples/random_number_example.fun new file mode 100755 index 0000000..031d0dd --- /dev/null +++ b/examples/random_number_example.fun @@ -0,0 +1,43 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-12 + */ + +/* + * Example: Using OS-based random_number(len) builtin + * This generates cryptographically strong random bytes and returns them + * hex-encoded as a string of length 2*len (since each byte -> two hex chars). + */ + +print("--- random_number(len) demo ---") + +len_bytes = 16 +hexstr = random_number(len_bytes) +print("Requested bytes: " + to_string(len_bytes)) +print("Hex string: " + hexstr) +print("Hex length (should be 2*bytes = 32): " + to_string(len(hexstr))) + +// Zero length returns empty string +empty = random_number(0) +print("Empty (0 bytes) -> length: " + to_string(len(empty)) + " value: " + to_string(empty)) + +// You can request longer values as needed, e.g., 32 bytes -> 64 hex chars +hex64 = random_number(32) +print("32 bytes -> " + to_string(len(hex64)) + " hex chars") + +/* Possible output: +--- random_number(len) demo --- +Requested bytes: 16 +Hex string: d5f12eb93b71e78cc803aa802e76e3b4 +Hex length (should be 2*bytes = 32): 32 +Empty (0 bytes) -> length: 0 value: +32 bytes -> 64 hex chars +*/ diff --git a/make b/make index b8cc7b7..171c7f6 100755 --- a/make +++ b/make @@ -34,6 +34,23 @@ if [ "$target" = "all" ]; then -DFUN_USE_MUSL=OFF \ -DFUN_DEBUG=OFF \ && cmake --build build --target fun +elif [ "$target" = "all_debug" ]; then + rm -rf build \ + && cmake -S . -B build \ + -DFUN_WITH_PCSC=ON \ + -DFUN_WITH_REPL=ON \ + -DFUN_WITH_LIBSQL=ON \ + -DFUN_WITH_SQLITE=ON \ + -DFUN_WITH_CURL=ON \ + -DFUN_WITH_PCRE2=ON \ + -DFUN_WITH_XML2=ON \ + -DFUN_WITH_JSON=ON \ + -DFUN_WITH_TCLTK=ON \ + -DFUN_WITH_INI=ON \ + -DFUN_LINK_STATIC=OFF \ + -DFUN_USE_MUSL=OFF \ + -DFUN_DEBUG=ON \ + && cmake --build build --target fun elif [ "$target" = "alpine" ]; then rm -rf build \ && cmake -S . -B build \ @@ -67,23 +84,6 @@ elif [ "$target" = "debug" ]; then -DFUN_USE_MUSL=OFF \ -DFUN_DEBUG=ON \ && cmake --build build --target fun -elif [ "$target" = "debug_all" ]; then - rm -rf build \ - && cmake -S . -B build \ - -DFUN_WITH_PCSC=ON \ - -DFUN_WITH_REPL=ON \ - -DFUN_WITH_LIBSQL=ON \ - -DFUN_WITH_SQLITE=ON \ - -DFUN_WITH_CURL=ON \ - -DFUN_WITH_PCRE2=ON \ - -DFUN_WITH_XML2=ON \ - -DFUN_WITH_JSON=ON \ - -DFUN_WITH_TCLTK=ON \ - -DFUN_WITH_INI=ON \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_USE_MUSL=OFF \ - -DFUN_DEBUG=ON \ - && cmake --build build --target fun elif [ "$target" = "freebsd" ]; then rm -rf build \ && cmake -S . -B build \ @@ -155,9 +155,9 @@ else echo "Build target $target not found... aborting!"; echo "Available targets:"; echo " - all"; + echo " - all_debug"; echo " - alpine"; echo " - debug"; - echo " - debug_all"; echo " - freebsd"; echo " - minimal"; echo " - musl"; diff --git a/src/bytecode.c b/src/bytecode.c index 647c365..6bf4c6e 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -134,6 +134,7 @@ static const char *opcode_name(OpCode op) { case OP_THREAD_SPAWN: return "THREAD_SPAWN"; case OP_THREAD_JOIN: return "THREAD_JOIN"; case OP_SLEEP_MS: return "SLEEP_MS"; + case OP_RANDOM_NUMBER: return "RANDOM_NUMBER"; case OP_BAND: return "BAND"; case OP_BOR: return "BOR"; case OP_BXOR: return "BXOR"; diff --git a/src/bytecode.h b/src/bytecode.h index 12986e8..7a14da3 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -131,6 +131,7 @@ typedef enum { OP_THREAD_SPAWN, // operand: 0=no args, 1=has args; pops [args?], fn; pushes thread id (int>0) OP_THREAD_JOIN, // pops thread id; waits; pushes result value (or Nil) OP_SLEEP_MS, // pops milliseconds; sleeps; pushes Nil (for statement POP safety) + OP_RANDOM_NUMBER, // pops length; pushes hex string of that length from OS RNG (hex-encoded) // Bitwise (32-bit) and shifts/rotates OP_BAND, // pops b, a; pushes (uint32_t)(a & b) diff --git a/src/fun.c b/src/fun.c index b614452..afd7305 100644 --- a/src/fun.c +++ b/src/fun.c @@ -5,12 +5,11 @@ */ #include "bytecode.h" -#include "value.h" #include "vm.h" #include "parser.h" +#include #include #include -#include #ifdef FUN_WITH_REPL #include "repl.h" diff --git a/src/parser.c b/src/parser.c index b665f16..fbc3758 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1649,6 +1649,16 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) return 1; } + if (strcmp(name, "random_number") == 0) { + (*pos)++; /* '(' */ + /* expects exactly 1 arg: length */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "random_number expects 1 arg (length)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after random_number arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_RANDOM_NUMBER, 0); + free(name); + return 1; + } + /* threading */ if (strcmp(name, "thread_spawn") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index 7991a2d..d7fb110 100644 --- a/src/vm.c +++ b/src/vm.c @@ -666,6 +666,7 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/os/time_now_ms.c" #include "vm/os/clock_mono_ms.c" #include "vm/os/date_format.c" + #include "vm/os/random_number.c" /* Socket ops */ #include "vm/os/socket_tcp_listen.c" diff --git a/src/vm.h b/src/vm.h index df2c0b7..a69b0a4 100644 --- a/src/vm.h +++ b/src/vm.h @@ -37,6 +37,7 @@ static const char *opcode_names[] = { "READ_FILE","WRITE_FILE","ENV","INPUT_LINE","PROC_RUN","PROC_SYSTEM", "TIME_NOW_MS","CLOCK_MONO_MS","DATE_FORMAT", "THREAD_SPAWN","THREAD_JOIN","SLEEP_MS", + "RANDOM_NUMBER", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "JSON_PARSE","JSON_STRINGIFY","JSON_FROM_FILE","JSON_TO_FILE", "CURL_GET","CURL_POST","CURL_DOWNLOAD", diff --git a/src/vm/os/random_number.c b/src/vm/os/random_number.c new file mode 100644 index 0000000..36e3dab --- /dev/null +++ b/src/vm/os/random_number.c @@ -0,0 +1,139 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-12-12 + */ + +/* Generate OS-based random bytes and return them hex-encoded. + * Opcode: OP_RANDOM_NUMBER + * Stack: pops len (number of raw bytes), pushes hex string of length 2*len + */ + +#include +#include +#include +#include +#include + +/* Platform-specific headers guarded per OS to avoid leaking problematic macros */ +#if defined(_WIN32) || defined(_WIN64) + #include + #include +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + #include +#elif defined(__unix__) + #if __has_include() + #include + #endif + #include + #include +#endif + +case OP_RANDOM_NUMBER: { + /* pop requested raw byte length */ + Value lv = pop_value(vm); + if (lv.type != VAL_INT) { + fprintf(stderr, "Runtime type error: random_number(len) expects integer length\n"); + free_value(lv); + /* For safety, push empty string so callers expecting a value won't underflow */ + push_value(vm, make_string("")); + break; + } + int64_t len = lv.i; + if (len < 0) { + fprintf(stderr, "random_number error: negative length (%" PRId64 ")\n", len); + free_value(lv); + push_value(vm, make_string("")); + break; + } + if (len == 0) { + free_value(lv); + push_value(vm, make_string("")); + break; + } + + /* Cap to prevent excessive allocations (max 1 MiB raw -> 2 MiB hex) */ + const int64_t MAX_RAW = (1LL << 20); + if (len > MAX_RAW) { + fprintf(stderr, "random_number error: requested length too large (%" PRId64 ", max %" PRId64 ")\n", len, MAX_RAW); + free_value(lv); + push_value(vm, make_string("")); + break; + } + + unsigned char *raw = (unsigned char*)malloc((size_t)len); + char *hex = (char*)malloc((size_t)len * 2 + 1); + if (!raw || !hex) { + if (raw) free(raw); + if (hex) free(hex); + free_value(lv); + fprintf(stderr, "Out of memory in random_number\n"); + exit(1); + } + + int ok = 0; + + /* --- Fill raw with cryptographically secure random bytes from the OS --- */ +#if defined(_WIN32) || defined(_WIN64) + { + /* Windows: use BCryptGenRandom */ + NTSTATUS st = BCryptGenRandom(NULL, raw, (ULONG)len, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + ok = (st == 0); + } +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + { + arc4random_buf(raw, (size_t)len); + ok = 1; + } +#elif defined(__unix__) + { + /* Prefer getrandom if available, otherwise /dev/urandom */ + #if __has_include() + ssize_t n = getrandom(raw, (size_t)len, 0); + ok = (n == (ssize_t)len); + #else + ok = 0; + #endif + if (!ok) { + int fd = open("/dev/urandom", O_RDONLY); + if (fd >= 0) { + size_t off = 0; ssize_t n; + while (off < (size_t)len && (n = read(fd, raw + off, (size_t)len - off)) > 0) off += (size_t)n; + close(fd); + ok = (off == (size_t)len); + } + } + } +#else + ok = 0; +#endif + + if (!ok) { + free(raw); + free(hex); + free_value(lv); + fprintf(stderr, "random_number error: OS RNG unavailable or failed\n"); + exit(1); + } + + /* hex encode */ + static const char hexdig[] = "0123456789abcdef"; + for (int64_t i = 0; i < len; ++i) { + unsigned char b = raw[i]; + hex[2*i] = hexdig[(b >> 4) & 0xF]; + hex[2*i+1] = hexdig[b & 0xF]; + } + hex[len * 2] = '\0'; + + Value s = make_string(hex); + free(raw); + free(hex); + free_value(lv); + push_value(vm, s); + break; +}