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)
This commit is contained in:
parent
41fa946f1c
commit
c71857a068
9 changed files with 309 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
42
examples/repl_on_error.fun
Normal file
42
examples/repl_on_error.fun
Normal file
|
|
@ -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 <you@hanez.org>
|
||||
* 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>
|
||||
*/
|
||||
53
extra/examples/pcsc2_example.fun
Executable file
53
extra/examples/pcsc2_example.fun
Executable file
|
|
@ -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 <you@hanez.org>
|
||||
* 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 <io/pcsc2.fun>
|
||||
|
||||
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")
|
||||
106
lib/io/pcsc2.fun
Normal file
106
lib/io/pcsc2.fun
Normal file
|
|
@ -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 <you@hanez.org>
|
||||
* 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 <hex.fun>
|
||||
|
||||
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
|
||||
13
src/fun.c
13
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] <script.fun>\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;
|
||||
}
|
||||
|
|
|
|||
51
src/repl.c
51
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 : "<entry>";
|
||||
const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : "<unknown>";
|
||||
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 : "<entry>";
|
||||
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;
|
||||
|
|
|
|||
37
src/vm.c
37
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 <setjmp.h>
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
|||
8
src/vm.h
8
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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue