diff --git a/CMakeLists.txt b/CMakeLists.txt index 129e922..6947a82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.18.0 LANGUAGES C) +project(fun VERSION 0.19.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/datetime_basic.fun b/examples/datetime_basic.fun new file mode 100644 index 0000000..20d162c --- /dev/null +++ b/examples/datetime_basic.fun @@ -0,0 +1,45 @@ +#!/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 + */ + +// Date/time opcode usage examples +#include + +fun main() + dt = DateTime() + + ms = dt.now_ms() + print(join(["now ms: ", to_string(ms)], "")) + + iso = dt.iso_now() + print(join(["iso now: ", iso], "")) + + // Formatting an arbitrary timestamp (epoch ms) + print(dt.format(ms, "%Y-%m-%d %H:%M:%S %Z")) + + // Measuring elapsed time with monotonic clock + start = dt.mono_ms() + + sleep(100) + sleep(10) + elapsed = dt.mono_ms() - start + print(join(["elapsed ~100ms => ", to_string(elapsed), " ms"], "")) + +// Invoke main when script is executed directly +main() + +/* Possible output: +now ms: 1759531591326 +iso now: 2025-10-04T00:46:31 +2025-10-04 00:46:31 CEST +elapsed ~100ms => 110 ms +*/ diff --git a/lib/utils/datetime.fun b/lib/utils/datetime.fun new file mode 100644 index 0000000..38517ca --- /dev/null +++ b/lib/utils/datetime.fun @@ -0,0 +1,32 @@ +// Date/time utilities abstraction for the Fun stdlib. +// Minimal version to validate syntax. + +/* + * Date/time utilities abstraction for the Fun stdlib. + * Provides a convenient class-based API over VM date/time opcodes. + * + * Methods: + * - now_ms() -> current wall clock ms since Unix epoch + * - mono_ms() -> monotonic clock ms (for measuring intervals) + * - format(ms, fmt) -> format an epoch-ms timestamp using strftime format + * - iso_now() -> ISO-8601 like local timestamp (YYYY-MM-DDTHH:MM:SS) + */ + +class DateTime() + // Current wall-clock time in milliseconds since Unix epoch + fun now_ms(this) + return time_now_ms() + + // Monotonic clock in milliseconds + fun mono_ms(this) + return clock_mono_ms() + + // Format an epoch-ms timestamp with a strftime() format string + fun format(this, ms, fmt) + // ensure argument order and types are okay + return date_format(to_number(ms), to_string(fmt)) + + // Convenience: ISO-like current datetime string (local time) + fun iso_now(this) + ms = time_now_ms() + return date_format(ms, "%Y-%m-%dT%H:%M:%S") diff --git a/src/bytecode.c b/src/bytecode.c index 6468910..901c67f 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -116,6 +116,30 @@ static const char *opcode_name(OpCode op) { case OP_HAS_KEY: return "HAS_KEY"; case OP_READ_FILE: return "READ_FILE"; case OP_WRITE_FILE: return "WRITE_FILE"; + case OP_ENV: return "ENV"; + case OP_INPUT_LINE: return "INPUT_LINE"; + case OP_PROC_RUN: return "PROC_RUN"; + case OP_PROC_SYSTEM: return "PROC_SYSTEM"; + case OP_TIME_NOW_MS: return "TIME_NOW_MS"; + case OP_CLOCK_MONO_MS: return "CLOCK_MONO_MS"; + case OP_DATE_FORMAT: return "DATE_FORMAT"; + case OP_THREAD_SPAWN: return "THREAD_SPAWN"; + case OP_THREAD_JOIN: return "THREAD_JOIN"; + case OP_SLEEP_MS: return "SLEEP_MS"; + case OP_BAND: return "BAND"; + case OP_BOR: return "BOR"; + case OP_BXOR: return "BXOR"; + case OP_BNOT: return "BNOT"; + case OP_SHL: return "SHL"; + case OP_SHR: return "SHR"; + case OP_ROTL: return "ROTL"; + case OP_ROTR: return "ROTR"; + case OP_PCSC_ESTABLISH: return "PCSC_ESTABLISH"; + case OP_PCSC_RELEASE: return "PCSC_RELEASE"; + case OP_PCSC_LIST_READERS: return "PCSC_LIST_READERS"; + case OP_PCSC_CONNECT: return "PCSC_CONNECT"; + case OP_PCSC_DISCONNECT: return "PCSC_DISCONNECT"; + case OP_PCSC_TRANSMIT: return "PCSC_TRANSMIT"; default: return "???"; } } diff --git a/src/bytecode.h b/src/bytecode.h index 6dd3a44..082cdb5 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -117,6 +117,9 @@ typedef enum { 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 + OP_TIME_NOW_MS, // pushes current wall-clock time in milliseconds since Unix epoch + OP_CLOCK_MONO_MS, // pushes monotonic clock in milliseconds (not wall time) + OP_DATE_FORMAT, // pops fmt string, ms epoch (int); pushes formatted date string using strftime // 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 09ce41f..0f65f01 100644 --- a/src/parser.c +++ b/src/parser.c @@ -670,6 +670,30 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + if (strcmp(name, "time_now_ms") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "time_now_ms expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_TIME_NOW_MS, 0); + free(name); + return 1; + } + if (strcmp(name, "clock_mono_ms") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "clock_mono_ms expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CLOCK_MONO_MS, 0); + free(name); + return 1; + } + if (strcmp(name, "date_format") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "date_format expects (ms:int, fmt:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "date_format expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "date_format expects (ms:int, fmt:string)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after date_format args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_DATE_FORMAT, 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 2b91edd..9e06dbe 100644 --- a/src/vm.c +++ b/src/vm.c @@ -326,6 +326,10 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/os/thread_spawn.c" #include "vm/os/proc_run.c" #include "vm/os/proc_system.c" + #include "vm/os/time_now_ms.c" + #include "vm/os/clock_mono_ms.c" + #include "vm/os/date_format.c" + #include "vm/pcsc/establish.c" #include "vm/pcsc/release.c" #include "vm/pcsc/list_readers.c" diff --git a/src/vm.h b/src/vm.h index 2dfc3f7..7028060 100644 --- a/src/vm.h +++ b/src/vm.h @@ -34,6 +34,7 @@ static const char *opcode_names[] = { "MIN","MAX","CLAMP","ABS","POW","RANDOM_SEED","RANDOM_INT", "MAKE_MAP","KEYS","VALUES","HAS_KEY", "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", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "PCSC_ESTABLISH","PCSC_RELEASE","PCSC_LIST_READERS","PCSC_CONNECT","PCSC_DISCONNECT","PCSC_TRANSMIT" diff --git a/src/vm/os/clock_mono_ms.c b/src/vm/os/clock_mono_ms.c new file mode 100644 index 0000000..25206e9 --- /dev/null +++ b/src/vm/os/clock_mono_ms.c @@ -0,0 +1,39 @@ +/** + * 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 clock_mono_ms.c + * @brief Implements OP_CLOCK_MONO_MS to push monotonic clock in ms. + * + * Stack before: [] + * Stack after: [int ms] + */ + +#include +#include + +case OP_CLOCK_MONO_MS: { + int64_t ms; +#if defined(CLOCK_MONOTONIC) && !defined(_WIN32) + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { + ms = (int64_t)ts.tv_sec * 1000 + (int64_t)(ts.tv_nsec / 1000000); + } else { + time_t s = time(NULL); + ms = (int64_t)s * 1000; + } +#else + time_t s = time(NULL); + ms = (int64_t)s * 1000; +#endif + push_value(vm, make_int(ms)); + break; +} diff --git a/src/vm/os/date_format.c b/src/vm/os/date_format.c new file mode 100644 index 0000000..6e6ba99 --- /dev/null +++ b/src/vm/os/date_format.c @@ -0,0 +1,72 @@ +/** +* 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 date_format.c + * @brief Implements OP_DATE_FORMAT to format an epoch milliseconds timestamp using strftime. + * + * Behavior: + * - Pops fmt string, then milliseconds since Unix epoch (int). + * - Formats the local time according to fmt and pushes a string. + * + * Errors: + * - If types are wrong, prints error and pushes empty string to keep stack safe. + */ + +#include +#include +#include + +case OP_DATE_FORMAT: { + Value fmt = pop_value(vm); + Value ms = pop_value(vm); + if (ms.type != VAL_INT || fmt.type != VAL_STRING) { + fprintf(stderr, "DATE_FORMAT expects (fmt:string, ms:int)\n"); + if (ms.type != VAL_NIL) free_value(ms); + if (fmt.type != VAL_NIL) free_value(fmt); + push_value(vm, make_string("")); + break; + } + time_t secs = (time_t)(ms.i / 1000); + struct tm tmv; +#if defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 1 + localtime_r(&secs, &tmv); +#else + struct tm *ptm = localtime(&secs); + if (!ptm) { + free_value(ms); free_value(fmt); + push_value(vm, make_string("")); + break; + } + tmv = *ptm; +#endif + /* Determine buffer size by attempting once with a reasonable size and retrying if needed */ + size_t cap = 128; + char *buf = (char*)malloc(cap); + size_t n = strftime(buf, cap, fmt.s, &tmv); + if (n == 0) { + /* try larger */ + cap = 512; + buf = (char*)realloc(buf, cap); + n = strftime(buf, cap, fmt.s, &tmv); + } + Value out; + if (n == 0) { + out = make_string(""); + } else { + out = make_string(buf); + } + free(buf); + free_value(ms); + free_value(fmt); + push_value(vm, out); + break; +} diff --git a/src/vm/os/time_now_ms.c b/src/vm/os/time_now_ms.c new file mode 100644 index 0000000..389e19c --- /dev/null +++ b/src/vm/os/time_now_ms.c @@ -0,0 +1,39 @@ +/** +* 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 time_now_ms.c + * @brief Implements OP_TIME_NOW_MS to push current wall-clock time in ms since Unix epoch. + * + * Stack before: [] + * Stack after: [int ms] + */ + +#include +#include + +case OP_TIME_NOW_MS: { + int64_t ms; +#if defined(CLOCK_REALTIME) && !defined(_WIN32) + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + ms = (int64_t)ts.tv_sec * 1000 + (int64_t)(ts.tv_nsec / 1000000); + } else { + time_t s = time(NULL); + ms = (int64_t)s * 1000; + } +#else + time_t s = time(NULL); + ms = (int64_t)s * 1000; +#endif + push_value(vm, make_int(ms)); + break; +}