From c71857a068e309e0f876447627e71072b544793b Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 6 Oct 2025 04:10:36 +0200 Subject: [PATCH] Jump into REPL when executing with --repl-on-error and an error occurs, when the REPL is builtin you will have access to the full stack then. (0.25.0) --- CMakeLists.txt | 6 +- examples/repl_on_error.fun | 42 ++++++++ extra/examples/pcsc2_example.fun | 53 +++++++++++ {examples => extra/examples}/pcsc_demo.fun | 0 lib/io/pcsc2.fun | 106 +++++++++++++++++++++ src/fun.c | 13 ++- src/repl.c | 51 ++++++++++ src/vm.c | 37 +++++++ src/vm.h | 8 +- 9 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 examples/repl_on_error.fun create mode 100755 extra/examples/pcsc2_example.fun rename {examples => extra/examples}/pcsc_demo.fun (100%) create mode 100644 lib/io/pcsc2.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index 9715f56..bcad443 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.24.0 LANGUAGES C) +project(fun VERSION 0.25.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -101,12 +101,12 @@ endif() target_link_libraries(fun PRIVATE fun_core) # Internal test programs -add_executable(fun_test +add_executable(fun_test src/fun_test.c ) target_link_libraries(fun_test PRIVATE fun_core) -add_executable(test_opcodes +add_executable(test_opcodes src/test_opcodes.c ) target_link_libraries(test_opcodes PRIVATE fun_core) diff --git a/examples/repl_on_error.fun b/examples/repl_on_error.fun new file mode 100644 index 0000000..cf26a96 --- /dev/null +++ b/examples/repl_on_error.fun @@ -0,0 +1,42 @@ +#!/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-06 + */ + +// Demonstration of --repl-on-error debugging. +// Run: fun --repl-on-error examples/repl_on_error.fun +// When the runtime error occurs, the REPL opens. Try commands: +// :backtrace +// :stack +// :locals +// +// This script triggers an index-out-of-range error inside a nested call. + +fun inner() + a = [1, 2, 3] + // Out-of-range access to cause a runtime error: + print(a[10]) + +fun outer() + inner() + +outer() + +/* Expected output (ruuning with --repl-on-error you will end up in the REPL + * with full stack access; :backtrace, :stack and :locals): +Runtime error: index out of range + (at ./examples/repl_on_error.fun:12 in inner, op INDEX_GET @ip 9) +Entering REPL due to runtime error (code 1) + (at ./examples/repl_on_error.fun:12 in inner, op INDEX_GET @ip 9) +Fun 0.25.0 REPL +Type :help for commands. Submit an empty line to run. +fun> +*/ diff --git a/extra/examples/pcsc2_example.fun b/extra/examples/pcsc2_example.fun new file mode 100755 index 0000000..2b97b41 --- /dev/null +++ b/extra/examples/pcsc2_example.fun @@ -0,0 +1,53 @@ +#!/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 + */ + +// Example using stdlib PCSC2 class. +// Establish context, list readers, connect to first (if any), +// send a sample SELECT MF APDU, print results, and clean up. + +#include + +pc = PCSC2() + +ctx = pc.establish() +print("ctx=" + to_string(ctx)) + +readers = pc.list_readers(ctx) +print("readers=" + to_string(readers)) + +number h = 0 +if len(readers) > 0 + h = pc.connect(ctx, readers[1]) + print("handle=" + to_string(h)) + + if h != 0 + // APDU 1: SELECT applet by AID (should return 251 data bytes, constant) + resp = pc.transmit_hex(h, "00a4040c0cD2760001354B414E4D30310000") + print(to_string(resp)) + print("resp.data_hex=" + resp["data_hex"]) + print("resp.sw1=" + to_string(resp["sw1"]) + " sw2=" + to_string(resp["sw2"]) + " code=" + to_string(resp["code"])) + number dlen1 = len(resp["data_hex"]) / 2 + print("SELECT AID data length=" + to_string(dlen1) + " (expected 251)") + + // APDU 2: GET CHALLENGE 8 (should return 8 random bytes) + resp = pc.transmit_hex(h, "0084000008") + print(to_string(resp)) + print("resp.data_hex=" + resp["data_hex"]) + print("resp.sw1=" + to_string(resp["sw1"]) + " sw2=" + to_string(resp["sw2"]) + " code=" + to_string(resp["code"])) + number dlen2 = len(resp["data_hex"]) / 2 + print("GET CHALLENGE data length=" + to_string(dlen2) + " (expected 8)") + + _ = pc.disconnect(h) + +_ = pc.release(ctx) +print("done") diff --git a/examples/pcsc_demo.fun b/extra/examples/pcsc_demo.fun similarity index 100% rename from examples/pcsc_demo.fun rename to extra/examples/pcsc_demo.fun diff --git a/lib/io/pcsc2.fun b/lib/io/pcsc2.fun new file mode 100644 index 0000000..ae788cd --- /dev/null +++ b/lib/io/pcsc2.fun @@ -0,0 +1,106 @@ +#!/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 + */ + +// PCSC2 stdlib abstraction class wrapping VM PC/SC opcodes. +// Goal: Provide clean, object-oriented access to PC/SC with safe fallbacks +// when the VM is built without PCSC support. All methods avoid throwing and +// return neutral values (0, [], or a default map). + +include + +class PCSC2() + // Establish a new PC/SC context. Returns ctx id (>0) or 0 if unavailable. + fun establish(this) + res = pcsc_establish() + if res == nil + return 0 + return res + + // Release a PC/SC context. Returns 1 on success, 0 otherwise. + fun release(this, ctx) + res = pcsc_release(ctx) + if res == nil + return 0 + return res + + // List available reader names for a context. Returns [] on failure. + fun list_readers(this, ctx) + readers = pcsc_list_readers(ctx) + if readers == nil + return [] + return readers + + // Connect to a given reader name for a context. Returns handle id (>0) or 0. + fun connect(this, ctx, reader_name) + res = pcsc_connect(ctx, to_string(reader_name)) + if res == nil + return 0 + return res + + // Disconnect a card handle. Returns 1 on success or 0. + fun disconnect(this, handle) + res = pcsc_disconnect(handle) + if res == nil + return 0 + return res + + // Transmit raw APDU bytes (array of numbers 0..255). + // Returns map {"data":[], "sw1":n, "sw2":n, "code":n} + fun transmit_bytes(this, handle, bytes) + res = pcsc_transmit(handle, bytes) + // Normalize to map regardless of VM build + if res == nil + m = {} + m["data"] = [] + m["sw1"] = -1 + m["sw2"] = -1 + m["code"] = -2 + return m + t = typeof(res) + if t != "Map" + m = {} + m["data"] = [] + m["sw1"] = -1 + m["sw2"] = -1 + m["code"] = -2 + return m + if res["data"] == nil + res["data"] = [] + return res + + // Transmit hex APDU string. Returns a convenience map with hex payload too: + // {"data_hex":string, "sw1":n, "sw2":n, "code":n} + fun transmit_hex(this, handle, hex) + arr = hex_to_bytes(hex) + res = pcsc_transmit(handle, arr) + // Normalize result to a map first + number sw1 = -1 + number sw2 = -1 + number code = -2 + data_arr = [] + if res != nil && typeof(res) == "Map" + if res["sw1"] != nil + sw1 = res["sw1"] + if res["sw2"] != nil + sw2 = res["sw2"] + if res["code"] != nil + code = res["code"] + if res["data"] != nil + data_arr = res["data"] + dh = bytes_to_hex(data_arr) + m = {} + m["data_hex"] = dh + m["sw1"] = sw1 + m["sw2"] = sw2 + m["code"] = code + return m diff --git a/src/fun.c b/src/fun.c index 2773992..b614452 100644 --- a/src/fun.c +++ b/src/fun.c @@ -24,11 +24,13 @@ static void print_usage(const char *prog) { printf("Fun %s\n", FUN_VERSION); printf("Usage:\n"); #ifdef FUN_WITH_REPL - printf(" %s [--trace|-t] [script.fun]\n", prog ? prog : "fun"); + printf(" %s [--trace|-t] [--repl-on-error] [script.fun]\n", prog ? prog : "fun"); printf(" %s --help | -h\n", prog ? prog : "fun"); printf(" %s --version | -V\n", prog ? prog : "fun"); printf("\n"); - printf("Options:\n --trace, -t Print executed ops and stack tops during run\n\n"); + printf("Options:\n"); + printf(" --trace, -t Print executed ops and stack tops during run\n"); + printf(" --repl-on-error Enter interactive REPL on runtime error with stack preserved\n\n"); printf("When no script is provided, a REPL starts. Submit an empty line to execute the buffer.\n"); #else printf(" %s [--trace|-t] \n", prog ? prog : "fun"); @@ -59,6 +61,13 @@ int main(int argc, char **argv) { vm.trace_enabled = 1; continue; } +#ifdef FUN_WITH_REPL + if (strcmp(arg, "--repl-on-error") == 0) { + vm.repl_on_error = 1; + vm.on_error_repl = fun_run_repl; /* provide REPL entry to core VM */ + continue; + } +#endif /* first non-option assumed to be script path */ break; } diff --git a/src/repl.c b/src/repl.c index 1682b57..7a4a55f 100644 --- a/src/repl.c +++ b/src/repl.c @@ -845,6 +845,9 @@ static void show_repl_help(void) { printf(" :history [N] Show last N lines of history (default 50)\n"); printf(" :time on|off|toggle Toggle/enable/disable timing\n"); printf(" :env [NAME[=VALUE]] Get or set environment variable\n"); + printf(" :backtrace | :bt Show backtrace of VM frames (most recent first)\n"); + printf(" :stack [N] Show top N (default all) stack values\n"); + printf(" :locals [FRAME] Show locals of frame (default: current top frame)\n"); } static char *read_entire_file(const char *path, size_t *out_len) { @@ -1253,6 +1256,54 @@ int fun_run_repl(VM *vm) { env_set(name, val); } continue; + } else if (strcmp(cmd, "backtrace") == 0 || strcmp(cmd, "bt") == 0) { + if (vm->fp < 0) { printf("(no frames)\n"); continue; } + printf("Backtrace (most recent call first):\n"); + for (int i = vm->fp; i >= 0; --i) { + Frame *f = &vm->frames[i]; + const char *fname = (f->fn && f->fn->name) ? f->fn->name : ""; + const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : ""; + int ip = f->ip - 1; + printf(" #%d %s at %s ip=%d line=%d\n", i, fname, sfile, ip, vm->current_line); + } + continue; + } else if (strcmp(cmd, "stack") == 0) { + int n = -1; + const char *p = lstrip(arg); + if (p && *p) n = atoi(p); + int count = vm->sp + 1; + if (count <= 0) { printf("(stack empty)\n"); continue; } + int start = 0; + if (n > 0 && n < count) start = count - n; + printf("Stack size=%d\n", count); + for (int i = start; i < count; ++i) { + char *sv = value_to_string_alloc(&vm->stack[i]); + printf("[%d] %s\n", i, sv ? sv : "nil"); + free(sv); + } + continue; + } else if (strcmp(cmd, "locals") == 0) { + int idx = vm->fp; + const char *p = lstrip(arg); + if (p && *p) { + int v = atoi(p); + if (v >= 0 && v <= vm->fp) idx = v; + } + if (idx < 0) { printf("(no current frame)\n"); continue; } + Frame *f = &vm->frames[idx]; + const char *fname = (f->fn && f->fn->name) ? f->fn->name : ""; + printf("Locals in frame #%d (%s):\n", idx, fname); + int any = 0; + for (int i = 0; i < MAX_FRAME_LOCALS; ++i) { + if (f->locals[i].type != VAL_NIL) { + char *sv = value_to_string_alloc(&f->locals[i]); + printf(" %d: %s\n", i, sv ? sv : "nil"); + free(sv); + any = 1; + } + } + if (!any) printf(" (no non-nil locals)\n"); + continue; } else { printf("Unknown command. Use :help\n"); continue; diff --git a/src/vm.c b/src/vm.c index 6f1a2ba..d58abc1 100644 --- a/src/vm.c +++ b/src/vm.c @@ -77,6 +77,27 @@ static int fun_vm_fprintf(FILE *stream, const char *fmt, ...) { /* Redirect fprintf within this translation unit so opcode handlers use our wrapper */ #define fprintf fun_vm_fprintf +/* Intercept exit() in this translation unit so VM errors don't terminate the process outright */ +#include + +static jmp_buf g_vm_err_jmp; + +static void fun_vm_exit(int code) { + if (g_active_vm && g_active_vm->repl_on_error) { + /* Jump back to vm_run to allow dropping into the REPL with intact VM state */ + longjmp(g_vm_err_jmp, code ? code : 1); + } + /* Fallback: terminate immediately if not in REPL-on-error mode */ +#ifdef _WIN32 + _exit(code); +#else + _Exit(code); +#endif +} + +/* Redirect exit inside this TU (affects included opcode handlers) */ +#define exit(code) fun_vm_exit(code) + /* Opcode case include index (vm_case_*.inc): - Core/stack/frame: @@ -202,6 +223,8 @@ void vm_init(VM *vm) { vm->instr_count = 0; vm->exit_code = 0; vm->trace_enabled = 0; + vm->repl_on_error = 0; + vm->on_error_repl = NULL; for (int i = 0; i < MAX_GLOBALS; ++i) vm->globals[i] = make_nil(); } @@ -249,6 +272,20 @@ void vm_run(VM *vm, Bytecode *entry) { vm->current_line = 1; g_active_vm = vm; + /* set error trap if REPL-on-error is enabled */ + if (vm->repl_on_error) { + int jcode = setjmp(g_vm_err_jmp); + if (jcode != 0) { + /* We got here from a trapped exit() in an error path */ + fprintf(stderr, "Entering REPL due to runtime error (code %d)\n", jcode); + if (vm->on_error_repl) { + vm->on_error_repl(vm); + } + g_active_vm = NULL; + return; + } + } + /* start with entry frame (no args) */ vm_push_frame(vm, entry, 0, NULL); diff --git a/src/vm.h b/src/vm.h index f2b60de..6fe27a0 100644 --- a/src/vm.h +++ b/src/vm.h @@ -49,7 +49,7 @@ typedef struct { Value locals[MAX_FRAME_LOCALS]; } Frame; -typedef struct { +struct VM { Value stack[STACK_SIZE]; int sp; @@ -68,7 +68,11 @@ typedef struct { int exit_code; // process exit code set by OP_EXIT int trace_enabled; // when non-zero, print executed ops and stack -} VM; + int repl_on_error; // when non-zero, enter REPL on runtime error (preserve stack) + int (*on_error_repl)(struct VM *vm); // optional hook to run REPL on error +}; + +typedef struct VM VM; // initialize VM (zero state) void vm_init(VM *vm);