From 605a27c1bd332fba491601c533e22a6a6603acbf Mon Sep 17 00:00:00 2001 From: Theodore Cooper Date: Fri, 3 Jul 2026 16:41:16 +0800 Subject: [PATCH 1/6] feat: add minimal C embedding API --- CMakeLists.txt | 68 +++++++++++--- api/pico.c | 70 +++++++++++++++ examples/embedding/basic.c | 34 +++++++ include/pico.h | 59 ++++++++++++ tests/run_tests.ps1 | 180 +++++++++++++++++++++++++++++++++++++ 5 files changed, 401 insertions(+), 10 deletions(-) create mode 100644 api/pico.c create mode 100644 examples/embedding/basic.c create mode 100644 include/pico.h create mode 100644 tests/run_tests.ps1 diff --git a/CMakeLists.txt b/CMakeLists.txt index a9af212..51d040c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,18 +46,61 @@ add_compile_options( $<$:-O3> ) -add_executable(pico - ${CORE_SRC} - ${VM_SRC} - ${COMPILER_SRC} - ${STD_SRC} +set(PICO_API_SRC + api/pico.c +) + +set(PICO_RUNTIME_SRC + ${PICO_API_SRC} + ${CORE_SRC} + ${VM_SRC} + ${COMPILER_SRC} + ${STD_SRC} +) + +add_library(picort STATIC + ${PICO_RUNTIME_SRC} +) + +target_include_directories(picort + PUBLIC + $ + $ +) + +set_target_properties(picort PROPERTIES + OUTPUT_NAME pico +) + +target_link_libraries(picort + PRIVATE + xxhash_lib + m +) + +add_executable(pico ${CMD_SRC} ) -if(WIN32) - target_link_libraries(pico PRIVATE xxhash_lib m) -else() - target_link_libraries(pico PRIVATE xxhash_lib linenoise_lib m) +add_executable(pico_embed_basic + examples/embedding/basic.c +) + +target_link_libraries(pico_embed_basic + PRIVATE + picort +) + +target_link_libraries(pico + PRIVATE + picort +) + +if(NOT WIN32) + target_link_libraries(pico + PRIVATE + linenoise_lib + ) endif() include(CTest) @@ -71,11 +114,16 @@ if(BUILD_TESTING) bash run_tests.sh WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests ) + + add_test( + NAME pico_embedding_basic + COMMAND $ + ) endif() add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - DEPENDS pico + DEPENDS pico pico_embed_basic WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) diff --git a/api/pico.c b/api/pico.c new file mode 100644 index 0000000..5945662 --- /dev/null +++ b/api/pico.c @@ -0,0 +1,70 @@ +#include "pico.h" + +#include + +#include "vm.h" + +static PicoStatus mapInterpreterStatus(InterpreterStatus status){ + switch(status){ + case VM_OK: + return PICO_STATUS_OK; + case VM_COMPILE_ERROR: + return PICO_STATUS_COMPILE_ERROR; + case VM_RUNTIME_ERROR: + return PICO_STATUS_RUNTIME_ERROR; + } + + return PICO_STATUS_RUNTIME_ERROR; +} + +PicoVM* pico_vm_create(void){ + PicoVM* vm = malloc(sizeof(*vm)); + + if(vm == NULL){ + return NULL; + } + + initVM(vm, 0, NULL); + return vm; +} + +void pico_vm_destroy(PicoVM* vm){ + if(vm == NULL){ + return; + } + + freeVM(vm); + free(vm); +} + +PicoStatus pico_vm_eval( + PicoVM* vm, + const char* source, + const char* source_name +){ + if(vm == NULL || source == NULL){ + return PICO_STATUS_INVALID_ARGUMENT; + } + + if(source_name == NULL){ + source_name = ""; + } + + InterpreterStatus status = interpret(vm, source, source_name); + return mapInterpreterStatus(status); +} + +const char* pico_status_string(PicoStatus status){ + switch(status){ + case PICO_STATUS_OK: + return "OK"; + case PICO_STATUS_COMPILE_ERROR: + return "compile error"; + case PICO_STATUS_RUNTIME_ERROR: + return "runtime error"; + case PICO_STATUS_INVALID_ARGUMENT: + return "invalid argument"; + } + + return "unknown status"; +} \ No newline at end of file diff --git a/examples/embedding/basic.c b/examples/embedding/basic.c new file mode 100644 index 0000000..1ef3a15 --- /dev/null +++ b/examples/embedding/basic.c @@ -0,0 +1,34 @@ +#include + +#include + +int main(void) { + PicoVM* vm = pico_vm_create(); + + if (vm == NULL) { + fprintf(stderr, "Could not create PiCo VM.\n"); + return 1; + } + + const char* source = + "var project = \"PiCo\";\n" + "print \"Hello from embedded ${project}!\";\n"; + + PicoStatus status = pico_vm_eval( + vm, + source, + "embed_basic.pcs" + ); + + if (status != PICO_STATUS_OK) { + fprintf( + stderr, + "PiCo execution failed: %s\n", + pico_status_string(status) + ); + } + + pico_vm_destroy(vm); + + return status == PICO_STATUS_OK ? 0 : 1; +} \ No newline at end of file diff --git a/include/pico.h b/include/pico.h new file mode 100644 index 0000000..938ad7a --- /dev/null +++ b/include/pico.h @@ -0,0 +1,59 @@ +#ifndef PICO_H +#define PICO_H + +#ifdef __cplusplus +extern "C"{ +#endif + +/* + * Public opaque handle for a PiCo +*/ + +typedef struct VM PicoVM; + +/* + * Result of a public PiCo API operation +*/ + +typedef enum PicoStatus{ + PICO_STATUS_OK = 0, + PICO_STATUS_COMPILE_ERROR, + PICO_STATUS_RUNTIME_ERROR, + PICO_STATUS_INVALID_ARGUMENT +} PicoStatus; + +/* + * Creates a new PiCo virtual machine. + * The VM is created without script command-line arguments. + * Returns NULL when storage for the VM cannot be allocated. +*/ + +PicoVM* pico_vm_create(void); + +/* + * Destroys a VM created by pico_vm_create(). + * Passing NULL is allowed and has no effect. +*/ +void pico_vm_destroy(PicoVM* vm); + +/* + * Compiles and executes a null-terminated PiCo source string. + * source_name is used in diagnostics. It may be NULL, in which case "" is used. +*/ +PicoStatus pico_vm_eval( + PicoVM* vm, + const char* source, + const char* source_name +); + +/* + * Returns a static human-readable name for a status value. + * The returned string must not be freed. +*/ +const char* pico_status_string(PicoStatus status); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/tests/run_tests.ps1 b/tests/run_tests.ps1 new file mode 100644 index 0000000..3b2caac --- /dev/null +++ b/tests/run_tests.ps1 @@ -0,0 +1,180 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$timeoutSec = 5 + +if (-not [string]::IsNullOrWhiteSpace($env:TIMEOUT_SEC)) { + $timeoutSec = [int]$env:TIMEOUT_SEC +} + +$picoExec = $env:PICO_EXEC + +if ([string]::IsNullOrWhiteSpace($picoExec)) { + $candidates = @( + (Join-Path $PSScriptRoot "..\build\debug\pico.exe"), + (Join-Path $PSScriptRoot "..\build\release\pico.exe") + ) + + foreach ($candidate in $candidates) { + if (Test-Path -LiteralPath $candidate) { + $picoExec = $candidate + break + } + } +} + +if ([string]::IsNullOrWhiteSpace($picoExec) -or + -not (Test-Path -LiteralPath $picoExec)) { + Write-Error @" +PiCo executable was not found. + +Set PICO_EXEC explicitly, for example: + + `$env:PICO_EXEC = "..\build\debug\pico.exe" + .\run_tests.ps1 +"@ + exit 1 +} + +$picoExec = (Resolve-Path -LiteralPath $picoExec).Path + +function Show-CapturedOutput { + param( + [string]$StdoutPath, + [string]$StderrPath + ) + + $lines = @() + + if (Test-Path -LiteralPath $StdoutPath) { + $lines += @(Get-Content -LiteralPath $StdoutPath) + } + + if (Test-Path -LiteralPath $StderrPath) { + $lines += @(Get-Content -LiteralPath $StderrPath) + } + + if ($lines.Count -eq 0) { + Write-Host "No output captured." + return + } + + $lines | + Select-Object -First 120 | + ForEach-Object { + Write-Host $_ + } +} + +$testFiles = Get-ChildItem ` + -LiteralPath $PSScriptRoot ` + -Filter "test_*.pcs" | + Where-Object { + -not $_.PSIsContainer + } | + Sort-Object Name + +$passed = 0 +$failed = 0 +$timeouts = 0 + +Write-Host "Starting tests..." +Write-Host "PICO_EXEC: $picoExec" +Write-Host "TIMEOUT_SEC: $timeoutSec" +Write-Host "" + +foreach ($test in $testFiles) { + Write-Host ("Running {0,-35} " -f $test.Name) -NoNewline + + $temporaryBase = Join-Path ` + ([System.IO.Path]::GetTempPath()) ` + ("pico-test-{0}-{1}" -f $PID, [Guid]::NewGuid().ToString("N")) + + $stdoutFile = "$temporaryBase.stdout.log" + $stderrFile = "$temporaryBase.stderr.log" + + try { + $process = Start-Process ` + -FilePath $picoExec ` + -ArgumentList @($test.Name) ` + -WorkingDirectory $PSScriptRoot ` + -RedirectStandardOutput $stdoutFile ` + -RedirectStandardError $stderrFile ` + -NoNewWindow ` + -PassThru + + $finished = $process.WaitForExit($timeoutSec * 1000) + + if (-not $finished) { + try { + $process.Kill() + } + catch { + # The process may already have exited. + } + + $process.WaitForExit() + + Write-Host "[TIMEOUT]" + Write-Host "Timed out after $timeoutSec seconds." + + Show-CapturedOutput ` + -StdoutPath $stdoutFile ` + -StderrPath $stderrFile + + $timeouts++ + $failed++ + continue + } + + # Wait once more so redirected output is completely flushed. + $process.WaitForExit() + + $exitCode = $process.ExitCode + + if ($exitCode -eq 0) { + Write-Host "[PASS]" + $passed++ + } + elseif ($exitCode -lt 0) { + Write-Host "[CRASH: exit $exitCode]" + + Show-CapturedOutput ` + -StdoutPath $stdoutFile ` + -StderrPath $stderrFile + + $failed++ + } + else { + Write-Host "[FAIL: exit $exitCode]" + + Show-CapturedOutput ` + -StdoutPath $stdoutFile ` + -StderrPath $stderrFile + + $failed++ + } + } + catch { + Write-Host "[ERROR]" + Write-Host $_.Exception.Message + $failed++ + } + finally { + Remove-Item ` + -LiteralPath $stdoutFile, $stderrFile ` + -Force ` + -ErrorAction SilentlyContinue + } +} + +Write-Host "" +Write-Host "========================================" +Write-Host "Summary: $passed Passed, $failed Failed ($timeouts Timeouts)" +Write-Host "========================================" + +if ($failed -eq 0) { + exit 0 +} + +exit 1 From 18496d99e1feb922e13b9febdd078df55436f602 Mon Sep 17 00:00:00 2001 From: Theodore Cooper Date: Sat, 4 Jul 2026 15:24:32 +0800 Subject: [PATCH 2/6] feat: implement embedding exit policy --- .gitattributes | 1 + CMakeLists.txt | 52 +++++++++++++++++++++++++++++++---- api/pico.c | 16 +++++++++++ include/pico.h | 8 ++++++ std/modules/os.c | 6 ++++ tests/embedding_exit_policy.c | 52 +++++++++++++++++++++++++++++++++++ tests/run_tests.sh | 28 +++++++------------ vm/vm.c | 18 ++++++++++-- vm/vm.h | 14 ++++++++++ 9 files changed, 168 insertions(+), 27 deletions(-) create mode 100644 .gitattributes create mode 100644 tests/embedding_exit_policy.c diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..526c8a3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 51d040c..c5922e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,15 @@ target_link_libraries(pico_embed_basic picort ) +add_executable(pico_embed_exit_policy + tests/embedding_exit_policy.c +) + +target_link_libraries(pico_embed_exit_policy + PRIVATE + picort +) + target_link_libraries(pico PRIVATE picort @@ -106,24 +115,55 @@ endif() include(CTest) if(BUILD_TESTING) + if(WIN32) + find_program( + PICO_BASH_EXECUTABLE + NAMES bash.exe + PATHS + "$ENV{ProgramFiles}/Git/bin" + "$ENV{ProgramFiles}/Git/usr/bin" + "C:/Program Files/Git/bin" + "C:/Program Files/Git/usr/bin" + "C:/msys64/usr/bin" + NO_DEFAULT_PATH + ) + else() + find_program( + PICO_BASH_EXECUTABLE + NAMES bash + ) + endif() + + if(NOT PICO_BASH_EXECUTABLE) + message(FATAL_ERROR + "Bash was not found. On Windows, install Git for Windows or MSYS2." + ) + endif() + add_test( NAME pico_script_tests - COMMAND ${CMAKE_COMMAND} -E env - "PICO_EXEC=$" - "TIMEOUT_SEC=60" - bash run_tests.sh - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + COMMAND + "${PICO_BASH_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/tests/run_tests.sh" + "$" + "60" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests" ) add_test( NAME pico_embedding_basic COMMAND $ ) + + add_test( + NAME pico_embedding_exit_policy + COMMAND $ + ) endif() add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - DEPENDS pico pico_embed_basic + DEPENDS pico pico_embed_basic pico_embed_exit_policy WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) diff --git a/api/pico.c b/api/pico.c index 5945662..b1ad6d6 100644 --- a/api/pico.c +++ b/api/pico.c @@ -25,6 +25,14 @@ PicoVM* pico_vm_create(void){ } initVM(vm, 0, NULL); + + /* + * A script embedded must not be allowed to terminate host process through os.exit(). + * This overrides the initialization of initVM(). + */ + + vm->allowProcessExit = false; + return vm; } @@ -54,6 +62,14 @@ PicoStatus pico_vm_eval( return mapInterpreterStatus(status); } +const char* pico_vm_last_error(const PicoVM* vm){ + if(vm == NULL || vm->lastError[0] == '\0'){ + return NULL; + } + + return vm->lastError; +} + const char* pico_status_string(PicoStatus status){ switch(status){ case PICO_STATUS_OK: diff --git a/include/pico.h b/include/pico.h index 938ad7a..8575abb 100644 --- a/include/pico.h +++ b/include/pico.h @@ -46,6 +46,14 @@ PicoStatus pico_vm_eval( const char* source_name ); +/* + * Returns the most recent compilation or runtime error message. + * Returns NULL if vm is NULL or no error message is currently available. + * The returned pointer is owned by the VM and must not be freed. + * It remains valid until the next pico_vm_eval() call or until the VM is destroyed. +*/ +const char* pico_vm_last_error(const PicoVM* vm); + /* * Returns a static human-readable name for a status value. * The returned string must not be freed. diff --git a/std/modules/os.c b/std/modules/os.c index 5efbd21..e560fe0 100644 --- a/std/modules/os.c +++ b/std/modules/os.c @@ -134,6 +134,12 @@ static Value os_exit(VM* vm, int argCount, Value* args){ if(argCount > 0 && IS_NUM(args[0])){ exitCode = (int)AS_NUM(args[0]); } + + if(!vm->allowProcessExit){ + runtimeError(vm, "os.exit() is disabled when PiCo is embedded."); + return NULL_VAL; + } + exit(exitCode); return NULL_VAL; // Unreachable } diff --git a/tests/embedding_exit_policy.c b/tests/embedding_exit_policy.c new file mode 100644 index 0000000..71c278d --- /dev/null +++ b/tests/embedding_exit_policy.c @@ -0,0 +1,52 @@ +#include +#include + +#include + +int main(void){ + PicoVM* vm = pico_vm_create(); + + if(vm == NULL){ + fprintf(stderr, "Could not create PiCo VM.\n"); + return 1; + } + + const char* source = + "import \"os\";\n" + "os.exit(7);\n" + "print \"This line must not execute.\";\n"; + + PicoStatus status = pico_vm_eval( + vm, + source, + "embedding_exit_policy.pcs" + ); + + if(status != PICO_STATUS_RUNTIME_ERROR){ + fprintf(stderr, "Expected runtime error, got: %s\n", pico_status_string(status)); + + pico_vm_destroy(vm); + return 1; + } + + const char* error = pico_vm_last_error(vm); + + if(error == NULL){ + fprintf(stderr, "Expected an error message.\n"); + pico_vm_destroy(vm); + return 1; + } + + if(strstr(error, "disabled") == NULL){ + fprintf(stderr, "Unexpected error message: %s\n", error); + + pico_vm_destroy(vm); + return 1; + } + + printf("Host survived os.exit().\n"); + printf("Captured error: %s\n", error); + + pico_vm_destroy(vm); + return 0; +} \ No newline at end of file diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 6b7787f..a5ce1c3 100644 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -2,29 +2,21 @@ set -u -TIMEOUT_SEC="${TIMEOUT_SEC:-5}" +PICO_EXEC="${1:-${PICO_EXEC:-}}" +TIMEOUT_SEC="${2:-${TIMEOUT_SEC:-5}}" LOG_FILE="${LOG_FILE:-test_output.log}" -if [ -z "${PICO_EXEC:-}" ]; then - if [ -f "../build/pico.exe" ]; then - PICO_EXEC="../build/pico.exe" - elif [ -f "../build/pico" ]; then - PICO_EXEC="../build/pico" - else - PICO_EXEC="$(find ../build -name "pico*" -type f 2>/dev/null | head -n 1)" - fi -fi - -if [ -z "${PICO_EXEC:-}" ] || [ ! -f "$PICO_EXEC" ]; then - echo "Error: pico executable not found." - echo "Set it manually, for example:" - echo " PICO_EXEC=\"../build/pico\" bash run_tests.sh" - echo " PICO_EXEC=\"../build-stress/pico\" TIMEOUT_SEC=60 bash run_tests.sh" +if [ -z "$PICO_EXEC" ]; then + echo "Error: PiCo executable path was not provided." + echo "Usage:" + echo " bash run_tests.sh /path/to/pico [timeout_seconds]" exit 1 fi -if [ ! -x "$PICO_EXEC" ]; then - chmod +x "$PICO_EXEC" 2>/dev/null || true +if [ ! -f "$PICO_EXEC" ]; then + echo "Error: PiCo executable not found:" + echo " $PICO_EXEC" + exit 1 fi PASSED=0 diff --git a/vm/vm.c b/vm/vm.c index ebbca3f..8ff8f73 100644 --- a/vm/vm.c +++ b/vm/vm.c @@ -101,6 +101,13 @@ void initVM(VM* vm, int argc, const char* argv[]){ vm->hadRuntimeError = false; + /* + * initVM() is also used directly by the CLI. The embedding API overrides this after initialization. + */ + + vm->allowProcessExit = true; + vm->lastError[0] = '\0'; + registerPrelude(vm); } @@ -204,8 +211,13 @@ static void closeUpvalues(VM* vm, Value* last){ } InterpreterStatus interpret(VM* vm, const char* code, const char* srcName){ + vm->lastError[0] = '\0'; + ObjectFunc* func = compile(vm, code, srcName); - if(func == NULL) return VM_COMPILE_ERROR; + if(func == NULL){ + snprintf(vm->lastError, sizeof(vm->lastError), "Compilation failed."); + return VM_COMPILE_ERROR; + } push(vm, OBJECT_VAL(func)); @@ -1559,9 +1571,9 @@ static bool callValue(VM* vm, Value callee, int argCnt){ void runtimeError(VM* vm, const char* format, ...){ va_list args; va_start(args, format); - vfprintf(stderr, format, args); + vsnprintf(vm->lastError, sizeof(vm->lastError), format, args); va_end(args); - fputs("\n", stderr); + fprintf(stderr, "%s\n", vm->lastError); #ifdef DEBUG_TRACE if(vm->frameCount > 0){ diff --git a/vm/vm.h b/vm/vm.h index c919217..3389022 100644 --- a/vm/vm.h +++ b/vm/vm.h @@ -17,6 +17,7 @@ typedef struct GCPolicy GCPolicy; #define STACK_MAX (FRAMES_MAX * 256) #define GLOBAL_STATCK_MAX 64 #define MAX_DEFERS 255 +#define VM_ERROR_MESSAGE_MAX 512 typedef struct CallFrame{ ObjectClosure* closure; @@ -79,6 +80,19 @@ typedef struct VM{ const char** argv; bool hadRuntimeError; + + /* + * Whether script code may terminate the host process through os.exit(). + * The CLI enables this to preserve its existing behavior. + * The public embedding API disables it. + */ + bool allowProcessExit; + + /* + * Most recent compilation or runtime error. + * The message remains available after recover(). + */ + char lastError[VM_ERROR_MESSAGE_MAX]; }VM; typedef enum{ From 3baf78fd785cce34bf329ff4aaa1e5a8e2d89518 Mon Sep 17 00:00:00 2001 From: Theodore Cooper Date: Sat, 4 Jul 2026 16:44:22 +0800 Subject: [PATCH 3/6] feat: implement native function --- CMakeLists.txt | 16 ++- api/pico.c | 208 ++++++++++++++++++++++++++++++ core/object.c | 14 +- core/object.h | 9 ++ include/pico.h | 81 ++++++++++++ tests/embedding_native_function.c | 162 +++++++++++++++++++++++ 6 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 tests/embedding_native_function.c diff --git a/CMakeLists.txt b/CMakeLists.txt index c5922e3..9add40f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,15 @@ target_link_libraries(pico_embed_exit_policy picort ) +add_executable(pico_embed_native_function + tests/embedding_native_function.c +) + +target_link_libraries(pico_embed_native_function + PRIVATE + picort +) + target_link_libraries(pico PRIVATE picort @@ -159,11 +168,16 @@ if(BUILD_TESTING) NAME pico_embedding_exit_policy COMMAND $ ) + + add_test( + NAME pico_embedding_native_function + COMMAND $ + ) endif() add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - DEPENDS pico pico_embed_basic pico_embed_exit_policy + DEPENDS pico pico_embed_basic pico_embed_exit_policy pico_embed_native_function WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) diff --git a/api/pico.c b/api/pico.c index b1ad6d6..2a75004 100644 --- a/api/pico.c +++ b/api/pico.c @@ -1,9 +1,21 @@ #include "pico.h" #include +#include +#include +#include "global_env.h" +#include "object.h" +#include "value.h" #include "vm.h" +struct PicoCall{ + VM* vm; + int argCount; + Value* args; + Value result; +}; + static PicoStatus mapInterpreterStatus(InterpreterStatus status){ switch(status){ case VM_OK: @@ -17,6 +29,46 @@ static PicoStatus mapInterpreterStatus(InterpreterStatus status){ return PICO_STATUS_RUNTIME_ERROR; } +static Value callHostFunc(VM* vm, int argCount, Value* args){ + Value callee = args[-1]; + // the callee is stored immediately before the first arg + + if(!IS_CFUNC(callee)){ + runtimeError(vm, "Invalid host function call."); + return NULL_VAL; + } + + ObjectCFunc* func = AS_CFUNC_OBJECT(callee); + + if(func->hostFunc == NULL){ + runtimeError(vm, "Host function callback is not available."); + return NULL_VAL; + } + + PicoCall call = { + .vm = vm, + .argCount = argCount, + .args = args, + .result = NULL_VAL + }; + + func->hostFunc(&call, func->userData); + + return call.result; +} + +static bool isValidArgIndex(const PicoCall* call, int index){ + return call != NULL && index >= 0 && index < call->argCount; +} + +static void setCallResult(PicoCall* call, Value value){ + if(call == NULL){ + return; + } + + call->result = value; +} + PicoVM* pico_vm_create(void){ PicoVM* vm = malloc(sizeof(*vm)); @@ -62,6 +114,162 @@ PicoStatus pico_vm_eval( return mapInterpreterStatus(status); } +int pico_call_arg_count(const PicoCall* call){ + if(call == NULL){ + return 0; + } + + return call->argCount; +} + +PicoValueType pico_call_arg_type(const PicoCall* call, int index){ + if(!isValidArgIndex(call, index)){ + return PICO_VALUE_OTHER; + } + + Value value = call->args[index]; + + if(IS_NULL(value)){ + return PICO_VALUE_NULL; + } + + if(IS_BOOL(value)){ + return PICO_VALUE_BOOL; + } + + if(IS_NUM(value)){ + return PICO_VALUE_NUMBER; + } + + if(IS_STRING(value)){ + return PICO_VALUE_STRING; + } + + return PICO_VALUE_OTHER; +} + +bool pico_call_get_bool(const PicoCall* call, int index, bool* result){ + if(result == NULL || !isValidArgIndex(call, index)){ + return false; + } + + Value value = call->args[index]; + + if(!IS_BOOL(value)){ + return false; + } + + *result = AS_BOOL(value); + return true; +} + +bool pico_call_get_number(const PicoCall* call, int index, double* result){ + if(result == NULL || !isValidArgIndex(call, index)){ + return false; + } + + Value value = call->args[index]; + + if(!IS_NUM(value)){ + return false; + } + + *result = AS_NUM(value); + return true; +} + +const char* pico_call_get_string(const PicoCall* call, int index, size_t* length){ + if(!isValidArgIndex(call, index)){ + return NULL; + } + + Value value = call->args[index]; + + if(!IS_STRING(value)){ + return NULL; + } + + ObjectString* string = AS_STRING(value); + + if(length != NULL){ + *length = string->length; + } + + return string->chars; +} + +void pico_call_return_null(PicoCall* call){ + setCallResult(call, NULL_VAL); +} + +void pico_call_return_bool(PicoCall* call, bool value){ + setCallResult(call, BOOL_VAL(value)); +} + +void pico_call_return_number(PicoCall* call, double value){ + setCallResult(call, NUM_VAL(value)); +} + +void pico_call_return_string(PicoCall* call, const char* value, size_t length){ + if(call == NULL){ + return; + } + + if(value == NULL){ + pico_call_error(call, "Host function returned a null string pointer."); + return; + } + + if(length > INT_MAX){ + pico_call_error(call, "Host function returned a string that is too large."); + return; + } + + ObjectString* string = copyString(call->vm, value, (int)length); + + setCallResult(call, OBJECT_VAL(string)); +} + +void pico_call_error(PicoCall* call, const char* message){ + if(call == NULL){ + return; + } + + if(message == NULL){ + message = "Host function failed."; + } + + runtimeError(call->vm, "%s", message); + + setCallResult(call, NULL_VAL); +} + +PicoStatus pico_vm_register_native(PicoVM* vm, const char* name, PicoNativeFn function, void* user_data){ + if(vm == NULL || name == NULL || name[0] == '\0' || function == NULL){ + return PICO_STATUS_INVALID_ARGUMENT; + } + + ObjectString* key = copyString(vm, name, (int)strlen(name)); + + //Root the key while the function object and global table may allocate. + push(vm, OBJECT_VAL(key)); + + ObjectCFunc* native = newHostCFunc(vm, callHostFunc, function, user_data); + + push(vm, OBJECT_VAL(native)); + + bool success = globalSetName(vm, &vm->globals, key, OBJECT_VAL(native)); + + pop(vm); + pop(vm); + + if(!success){ + return PICO_STATUS_RUNTIME_ERROR; + } + + return PICO_STATUS_OK; +} + const char* pico_vm_last_error(const PicoVM* vm){ if(vm == NULL || vm->lastError[0] == '\0'){ return NULL; diff --git a/core/object.c b/core/object.c index 6a692a5..3adb79b 100644 --- a/core/object.c +++ b/core/object.c @@ -174,18 +174,26 @@ ObjectFunc* newFunction(VM* vm){ return func; } -ObjectCFunc* newCFunc(VM* vm, CFunc func){ +ObjectCFunc* newHostCFunc(VM* vm, CFunc adapter, HostCFunc hostFunc, void* userData){ ObjectCFunc* cfunc = (ObjectCFunc*)reallocate(vm, NULL, 0, sizeof(ObjectCFunc)); + cfunc->obj.type = OBJECT_CFUNC; cfunc->obj.isMarked = false; - cfunc->func = func; - cfunc->obj.isMarked = false; + + cfunc->func = adapter; + cfunc->hostFunc = hostFunc; + cfunc->userData = userData; cfunc->obj.next = vm->objects; vm->objects = (Object*)cfunc; + return cfunc; } +ObjectCFunc* newCFunc(VM* vm, CFunc func){ + return newHostCFunc(vm, func, NULL, NULL); +} + ObjectModule* newModule(VM* vm, ObjectString* name, ObjectString* path, ModuleKind kind){ ObjectModule* module = (ObjectModule*)reallocate(vm, NULL, 0, sizeof(ObjectModule)); module->obj.type = OBJECT_MODULE; diff --git a/core/object.h b/core/object.h index ad44b43..d05364f 100644 --- a/core/object.h +++ b/core/object.h @@ -12,6 +12,10 @@ typedef struct VM VM; typedef Value (*CFunc)(VM* vm, int argCount, Value* args); +typedef struct PicoCall PicoCall; + +typedef void (*HostCFunc)(PicoCall* call, void* userData); + #define OBJECT_TYPE(value) (AS_OBJECT(value)->type) #define IS_STRING(value) (IS_OBJECT(value) && OBJECT_TYPE(value) == OBJECT_STRING) @@ -28,6 +32,7 @@ typedef Value (*CFunc)(VM* vm, int argCount, Value* args); #define AS_FUNC(value) ((ObjectFunc*)AS_OBJECT(value)) #define IS_CFUNC(value) (IS_OBJECT(value) && OBJECT_TYPE(value) == OBJECT_CFUNC) +#define AS_CFUNC_OBJECT(value) ((ObjectCFunc*)AS_OBJECT(value)) #define AS_CFUNC(value) (((ObjectCFunc*)AS_OBJECT(value))->func) #define IS_MODULE(value) (IS_OBJECT(value) && OBJECT_TYPE(value) == OBJECT_MODULE) @@ -129,10 +134,14 @@ ObjectFunc* newFunction(VM* vm); typedef struct ObjectCFunc{ Object obj; CFunc func; + HostCFunc hostFunc; + void* userData; }ObjectCFunc; ObjectCFunc* newCFunc(VM* vm, CFunc func); +ObjectCFunc* newHostCFunc(VM* vm, CFunc adapter, HostCFunc hostFunc, void* userData); + typedef enum{ MODULE_NATIVE, MODULE_SCRIPT diff --git a/include/pico.h b/include/pico.h index 8575abb..6b06862 100644 --- a/include/pico.h +++ b/include/pico.h @@ -1,6 +1,9 @@ #ifndef PICO_H #define PICO_H +#include +#include + #ifdef __cplusplus extern "C"{ #endif @@ -11,6 +14,18 @@ extern "C"{ typedef struct VM PicoVM; +typedef struct PicoCall PicoCall; + +typedef enum PicoValueType{ + PICO_VALUE_NULL = 0, + PICO_VALUE_BOOL, + PICO_VALUE_NUMBER, + PICO_VALUE_STRING, + PICO_VALUE_OTHER +} PicoValueType; + +typedef void (*PicoNativeFn)(PicoCall* call, void* user_data); + /* * Result of a public PiCo API operation */ @@ -46,6 +61,72 @@ PicoStatus pico_vm_eval( const char* source_name ); +/* + * Registers a host-provided native function as a PiCo global. + * The user_data pointer is borrowed. PiCo does not free it, so it must remain valid + * Register native functions before evaluating scripts that use them. +*/ +PicoStatus pico_vm_register_native( + PicoVM* vm, + const char* name, + PicoNativeFn function, + void* user_data +); + +/* + * Returns the number of arguments passed by the PiCo script. +*/ +int pico_call_arg_count(const PicoCall* call); + +/* + * Returns the public type of an argument. + * Invalid indexes return PICO_VALUE_OTHER. +*/ +PicoValueType pico_call_arg_type(const PicoCall* call, int index); + +bool pico_call_get_bool( + const PicoCall* call, + int index, + bool* result +); + +bool pico_call_get_number( + const PicoCall* call, + int index, + double* result +); + +/* + * Returns a borrowed string pointer. + * The returned pointer belongs to the VM and must not be freed. + * It is valid only during the native function call. +*/ +const char* pico_call_get_string( + const PicoCall* call, + int index, + size_t* length +); + +void pico_call_return_null(PicoCall* call); + +void pico_call_return_bool(PicoCall* call, bool value); + +void pico_call_return_number(PicoCall* call, double value); + +/* + * Copies the supplied string into the PiCo VM. +*/ +void pico_call_return_string( + PicoCall* call, + const char* value, + size_t length +); + +/* + * Stops the current PiCo call with a runtime error. + */ +void pico_call_error(PicoCall* call, const char* message); + /* * Returns the most recent compilation or runtime error message. * Returns NULL if vm is NULL or no error message is currently available. diff --git a/tests/embedding_native_function.c b/tests/embedding_native_function.c new file mode 100644 index 0000000..394104a --- /dev/null +++ b/tests/embedding_native_function.c @@ -0,0 +1,162 @@ +#include +#include + +#include + +typedef struct HostState { + int addCalls; + int recordCalls; + double recordedValue; +} HostState; + +static void hostAdd(PicoCall* call, void* userData) { + HostState* state = (HostState*)userData; + + if(pico_call_arg_count(call) != 2){ + pico_call_error(call, "hostAdd() expects exactly two arguments."); + return; + } + + double left; + double right; + + if(!pico_call_get_number(call, 0, &left) || !pico_call_get_number(call, 1, &right)){ + pico_call_error(call, "hostAdd() expects two numbers."); + return; + } + + state->addCalls++; + pico_call_return_number(call, left + right); +} + +static void hostRecord(PicoCall* call, void* userData) { + HostState* state = (HostState*)userData; + + if(pico_call_arg_count(call) != 1){ + pico_call_error(call, "hostRecord() expects exactly one argument."); + return; + } + + double value; + + if(!pico_call_get_number(call, 0, &value)){ + pico_call_error(call, "hostRecord() expects a number."); + return; + } + + state->recordCalls++; + state->recordedValue = value; + + pico_call_return_bool(call, true); +} + +static int reportStatus(PicoVM* vm, const char* operation, PicoStatus status) { + const char* error = pico_vm_last_error(vm); + + fprintf(stderr, "%s failed: %s\n", operation, error != NULL ? error : pico_status_string(status)); + + return 1; +} + +int main(void) { + PicoVM* vm = pico_vm_create(); + + if(vm == NULL){ + fprintf(stderr, "Could not create PiCo VM.\n"); + return 1; + } + + HostState state = {0}; + + PicoStatus status = pico_vm_register_native(vm, "hostAdd", hostAdd, &state); + + if(status != PICO_STATUS_OK){ + int result = reportStatus(vm, "Registering hostAdd", status); + pico_vm_destroy(vm); + return result; + } + + status = pico_vm_register_native(vm, "hostRecord", hostRecord, &state); + + if(status != PICO_STATUS_OK){ + int result = reportStatus(vm, "Registering hostRecord", status); + pico_vm_destroy(vm); + return result; + } + + const char* source = + "var result = hostAdd(20, 22);\n" + "hostRecord(result);\n"; + + status = pico_vm_eval(vm, source, "embedding_native_function.pcs"); + + if(status != PICO_STATUS_OK){ + int result = reportStatus(vm, "Execution", status); + pico_vm_destroy(vm); + return result; + } + + if(state.addCalls != 1){ + fprintf(stderr, "Expected hostAdd() to be called once, got %d calls.\n", state.addCalls); + pico_vm_destroy(vm); + return 1; + } + + if(state.recordCalls != 1){ + fprintf(stderr, "Expected hostRecord() to be called once, got %d calls.\n", state.recordCalls); + pico_vm_destroy(vm); + return 1; + } + + if(state.recordedValue != 42.0){ + fprintf(stderr, "Expected recorded value 42, got %.14g.\n", state.recordedValue); + pico_vm_destroy(vm); + return 1; + } + + printf("Host function result: %.14g\n", state.recordedValue); + + status = pico_vm_eval(vm, + "hostAdd(\"invalid\", 1);\n", + "embedding_native_error.pcs" + ); + + if(status != PICO_STATUS_RUNTIME_ERROR){ + fprintf(stderr, "Expected runtime error, got %s.\n", pico_status_string(status)); + pico_vm_destroy(vm); + return 1; + } + + const char* error = pico_vm_last_error(vm); + + if(error == NULL || strstr(error, "expects two numbers") == NULL){ + fprintf(stderr, "Unexpected callback error: %s\n", error != NULL ? error : ""); + pico_vm_destroy(vm); + return 1; + } + + printf("Captured callback error: %s\n", error); + + status = pico_vm_eval(vm, + "var nestedResult = hostAdd(10, 5);\n" + "hostRecord(hostAdd(nestedResult, 27));\n", + "embedding_nested_native_function.pcs" + ); + + if(status != PICO_STATUS_OK){ + int result = reportStatus(vm, "Nested execution", status); + pico_vm_destroy(vm); + return result; + } + + if(state.recordedValue != 42.0){ + fprintf(stderr, "Expected nested result 42, got %.14g.\n", state.recordedValue); + pico_vm_destroy(vm); + return 1; + } + + printf("Nested host function result: %.14g\n", state.recordedValue); + + pico_vm_destroy(vm); + return 0; +} \ No newline at end of file From 6285777dbe18d675fe350c651d85aa3e41299908 Mon Sep 17 00:00:00 2001 From: Theodore Cooper Date: Sun, 5 Jul 2026 19:17:57 +0800 Subject: [PATCH 4/6] feat: add embedding API to eval and call PiCo functions --- CMakeLists.txt | 21 +++- api/pico.c | 163 ++++++++++++++++++++++++++ examples/embedding/call_script.c | 58 ++++++++++ include/pico.h | 29 ++++- tests/embedding_call_script.c | 193 +++++++++++++++++++++++++++++++ vm/vm.c | 74 +++++++++++- vm/vm.h | 1 + 7 files changed, 534 insertions(+), 5 deletions(-) create mode 100644 examples/embedding/call_script.c create mode 100644 tests/embedding_call_script.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9add40f..3c99e70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,15 @@ add_executable(pico_embed_native_function tests/embedding_native_function.c ) +add_executable(pico_embed_call_script + tests/embedding_call_script.c +) + +target_link_libraries(pico_embed_call_script + PRIVATE + picort +) + target_link_libraries(pico_embed_native_function PRIVATE picort @@ -173,11 +182,21 @@ if(BUILD_TESTING) NAME pico_embedding_native_function COMMAND $ ) + + add_test( + NAME pico_embedding_call_script + COMMAND $ + ) endif() add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - DEPENDS pico pico_embed_basic pico_embed_exit_policy pico_embed_native_function + DEPENDS + pico + pico_embed_basic + pico_embed_exit_policy + pico_embed_native_function + pico_embed_call_script WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) diff --git a/api/pico.c b/api/pico.c index 2a75004..cf87e53 100644 --- a/api/pico.c +++ b/api/pico.c @@ -69,6 +69,88 @@ static void setCallResult(PicoCall* call, Value value){ call->result = value; } +static bool toInternalValue(const PicoValue* publicValue, Value* internalValue){ + if(publicValue == NULL || internalValue == NULL){ + return false; + } + + switch(publicValue->type){ + case PICO_VALUE_NULL: + *internalValue = NULL_VAL; + return true; + case PICO_VALUE_BOOL: + *internalValue = BOOL_VAL(publicValue->as.boolean); + return true; + case PICO_VALUE_NUMBER: + *internalValue = NUM_VAL(publicValue->as.number); + return true; + case PICO_VALUE_STRING: + case PICO_VALUE_OTHER: + default: + return false; + } + + return false; +} + +static bool fromInternalValue(Value internalValue, PicoValue* publicValue){ + if(publicValue == NULL){ + return false; + } + + if(IS_NULL(internalValue)){ + *publicValue = pico_value_null(); + return true; + } + + if(IS_BOOL(internalValue)){ + *publicValue = pico_value_bool(AS_BOOL(internalValue)); + return true; + } + + if(IS_NUM(internalValue)){ + *publicValue = pico_value_number(AS_NUM(internalValue)); + return true; + } + + publicValue->type = PICO_VALUE_OTHER; + return false; +} + +static PicoStatus setApiError(PicoVM* vm, PicoStatus status, const char* message){ + if(vm != NULL && message != NULL){ + snprintf(vm->lastError, sizeof(vm->lastError), "%s", message); + } + + return status; +} + +PicoValue pico_value_null(void){ + PicoValue value = { + .type = PICO_VALUE_NULL + }; + + return value; +} + +PicoValue pico_value_bool(bool value){ + PicoValue result = { + .type = PICO_VALUE_BOOL, + .as.boolean = value + }; + + return result; +} + +PicoValue pico_value_number(double value){ + PicoValue result = { + .type = PICO_VALUE_NUMBER, + .as.number = value + }; + + return result; +} + PicoVM* pico_vm_create(void){ PicoVM* vm = malloc(sizeof(*vm)); @@ -270,6 +352,83 @@ PicoStatus pico_vm_register_native(PicoVM* vm, const char* name, PicoNativeFn fu return PICO_STATUS_OK; } +PicoStatus pico_vm_call( + PicoVM* vm, + const char* name, + int argCount, + const PicoValue* args, + PicoValue* result +){ + if(vm == NULL || name == NULL || name[0] == '\0' || argCount < 0){ + return PICO_STATUS_INVALID_ARGUMENT; + } + + if(argCount > 0 && args == NULL){ + return PICO_STATUS_INVALID_ARGUMENT; + } + + if(vm->frameCount != 0){ + return setApiError(vm, PICO_STATUS_RUNTIME_ERROR, "PiCo VM calls are not reentrant."); + } + + size_t nameLength = strlen(name); + + if(nameLength > INT_MAX){ + return setApiError(vm, PICO_STATUS_INVALID_ARGUMENT, "PiCo function name is too long."); + } + + vm->lastError[0] = '\0'; + + ObjectString* key = copyString(vm, name, (int)nameLength); + push(vm, OBJECT_VAL(key)); + + Value callee; + bool found = globalGetName(&vm->globals, key, &callee); + + pop(vm); + + if(!found){ + snprintf(vm->lastError, sizeof(vm->lastError), "Global function '%s' is not defined.", name); + return PICO_STATUS_RUNTIME_ERROR; + } + + Value* internalArgs = NULL; + + if(argCount > 0){ + internalArgs = malloc(sizeof(Value) * (size_t)argCount); + + if(internalArgs == NULL){ + return setApiError(vm, PICO_STATUS_OUT_OF_MEMORY, "Could not allocate PiCo call arguments."); + } + } + + for(int i = 0; i < argCount; i++){ + if(!toInternalValue(&args[i], &internalArgs[i])){ + free(internalArgs); + return setApiError(vm, PICO_STATUS_UNSUPPORTED_TYPE, "Arguments contain unsupported value types."); + } + } + + Value internalResult = NULL_VAL; + InterpreterStatus status = vmCallValue(vm, callee, argCount, internalArgs, &internalResult); + + free(internalArgs); + + if(status != VM_OK){ + return mapInterpreterStatus(status); + } + + if(result == NULL){ + return PICO_STATUS_OK; + } + + if(!fromInternalValue(internalResult, result)){ + return setApiError(vm, PICO_STATUS_UNSUPPORTED_TYPE, "PiCo function returned an unsupported value type."); + } + + return PICO_STATUS_OK; +} + const char* pico_vm_last_error(const PicoVM* vm){ if(vm == NULL || vm->lastError[0] == '\0'){ return NULL; @@ -288,6 +447,10 @@ const char* pico_status_string(PicoStatus status){ return "runtime error"; case PICO_STATUS_INVALID_ARGUMENT: return "invalid argument"; + case PICO_STATUS_OUT_OF_MEMORY: + return "out of memory"; + case PICO_STATUS_UNSUPPORTED_TYPE: + return "unsupported type"; } return "unknown status"; diff --git a/examples/embedding/call_script.c b/examples/embedding/call_script.c new file mode 100644 index 0000000..9696230 --- /dev/null +++ b/examples/embedding/call_script.c @@ -0,0 +1,58 @@ +/* + * This example demonstrates how to embed PiCo into a host application and call a PiCo function from the host. + * It defines a native host function hostAdd() that adds two numbers, and registers it as a global PiCo function. + * The PiCo script defines a function calculate() that calls hostAdd() and multiplies the result by 2. + * The host application calls calculate() and prints the result. +*/ + +#include + +#include + +static void hostAdd(PicoCall* call, void* userData){ + (void)userData; + + double left; + double right; + + if(!pico_call_get_number(call, 0, &left) || !pico_call_get_number(call, 1, &right)){ + pico_call_error(call, "hostAdd() expects two numbers."); + return; + } + + pico_call_return_number(call, left + right); +} + +int main(void){ + PicoVM* vm = pico_vm_create(); + pico_vm_register_native(vm, "hostAdd", hostAdd, NULL); + + const char* source = + "func calculate(a, b){\n" + " return hostAdd(a, b) * 2;\n" + "}\n"; + + if(pico_vm_eval(vm, source, "") != PICO_STATUS_OK){ + fprintf(stderr, "%s\n", pico_vm_last_error(vm)); + pico_vm_destroy(vm); + return 1; + } + + PicoValue args[] = { + pico_value_number(10), + pico_value_number(11) + }; + + PicoValue result; + + if(pico_vm_call(vm, "calculate", 2, args, &result) != PICO_STATUS_OK){ + fprintf(stderr, "%s\n", pico_vm_last_error(vm)); + pico_vm_destroy(vm); + return 1; + } + + printf("Result: %.14g\n", result.as.number); + + pico_vm_destroy(vm); + return 0; +} \ No newline at end of file diff --git a/include/pico.h b/include/pico.h index 6b06862..0b407c3 100644 --- a/include/pico.h +++ b/include/pico.h @@ -24,6 +24,15 @@ typedef enum PicoValueType{ PICO_VALUE_OTHER } PicoValueType; +typedef struct PicoValue{ + PicoValueType type; + + union{ + bool boolean; + double number; + }as; +} PicoValue; + typedef void (*PicoNativeFn)(PicoCall* call, void* user_data); /* @@ -34,9 +43,15 @@ typedef enum PicoStatus{ PICO_STATUS_OK = 0, PICO_STATUS_COMPILE_ERROR, PICO_STATUS_RUNTIME_ERROR, - PICO_STATUS_INVALID_ARGUMENT + PICO_STATUS_INVALID_ARGUMENT, + PICO_STATUS_OUT_OF_MEMORY, + PICO_STATUS_UNSUPPORTED_TYPE } PicoStatus; +PicoValue pico_value_null(void); +PicoValue pico_value_bool(bool value); +PicoValue pico_value_number(double value); + /* * Creates a new PiCo virtual machine. * The VM is created without script command-line arguments. @@ -73,6 +88,18 @@ PicoStatus pico_vm_register_native( void* user_data ); +/* + * Calls a global PiCo function. + * result may be NULL if the caller does not need the return value. +*/ +PicoStatus pico_vm_call( + PicoVM* vm, + const char* name, + int argCount, + const PicoValue* args, + PicoValue* result +); + /* * Returns the number of arguments passed by the PiCo script. */ diff --git a/tests/embedding_call_script.c b/tests/embedding_call_script.c new file mode 100644 index 0000000..4dc6e61 --- /dev/null +++ b/tests/embedding_call_script.c @@ -0,0 +1,193 @@ +#include +#include + +#include + +static void hostAdd(PicoCall* call, void* userData) { + (void)userData; + + if(pico_call_arg_count(call) != 2){ + pico_call_error(call, "hostAdd() expects exactly two arguments."); + return; + } + + double left; + double right; + + if(!pico_call_get_number(call, 0, &left) || + !pico_call_get_number(call, 1, &right)){ + pico_call_error(call, "hostAdd() expects two numbers."); + return; + } + + pico_call_return_number(call, left + right); +} + +static int reportFailure(PicoVM* vm, const char* operation, + PicoStatus status) { + const char* error = pico_vm_last_error(vm); + + fprintf(stderr, "%s failed: %s\n", operation, + error != NULL ? error : pico_status_string(status)); + + return 1; +} + +int main(void) { + PicoVM* vm = pico_vm_create(); + + if(vm == NULL){ + fprintf(stderr, "Could not create PiCo VM.\n"); + return 1; + } + + PicoStatus status = + pico_vm_register_native(vm, "hostAdd", hostAdd, NULL); + + if(status != PICO_STATUS_OK){ + int exitCode = reportFailure(vm, "Registering hostAdd", status); + pico_vm_destroy(vm); + return exitCode; + } + + const char* source = + "func add(a, b) {\n" + " return a + b;\n" + "}\n" + "\n" + "func calculate(a, b) {\n" + " return hostAdd(a, b) * 2;\n" + "}\n" + "\n" + "func failCall() {\n" + " return hostAdd(\"invalid\", 1);\n" + "}\n" + "\n" + "var notCallable = 42;\n"; + + status = pico_vm_eval(vm, source, "embedding_call_script.pcs"); + + if(status != PICO_STATUS_OK){ + int exitCode = reportFailure(vm, "Loading PiCo functions", status); + pico_vm_destroy(vm); + return exitCode; + } + + PicoValue addArgs[] = { + pico_value_number(20), + pico_value_number(22) + }; + + PicoValue result; + status = pico_vm_call(vm, "add", 2, addArgs, &result); + + if(status != PICO_STATUS_OK){ + int exitCode = reportFailure(vm, "Calling add", status); + pico_vm_destroy(vm); + return exitCode; + } + + if(result.type != PICO_VALUE_NUMBER || result.as.number != 42.0){ + fprintf(stderr, "Expected add() to return 42.\n"); + pico_vm_destroy(vm); + return 1; + } + + printf("PiCo add result: %.14g\n", result.as.number); + + PicoValue calculateArgs[] = { + pico_value_number(10), + pico_value_number(11) + }; + + status = pico_vm_call(vm, "calculate", 2, calculateArgs, &result); + + if(status != PICO_STATUS_OK){ + int exitCode = reportFailure(vm, "Calling calculate", status); + pico_vm_destroy(vm); + return exitCode; + } + + if(result.type != PICO_VALUE_NUMBER || result.as.number != 42.0){ + fprintf(stderr, "Expected calculate() to return 42.\n"); + pico_vm_destroy(vm); + return 1; + } + + printf("C -> PiCo -> C result: %.14g\n", result.as.number); + + status = pico_vm_call(vm, "missingFunction", 0, NULL, &result); + + if(status != PICO_STATUS_RUNTIME_ERROR){ + fprintf(stderr, "Expected missing function call to fail.\n"); + pico_vm_destroy(vm); + return 1; + } + + const char* error = pico_vm_last_error(vm); + + if(error == NULL || strstr(error, "not defined") == NULL){ + fprintf(stderr, "Unexpected missing-function error: %s\n", + error != NULL ? error : ""); + pico_vm_destroy(vm); + return 1; + } + + printf("Captured missing function error: %s\n", error); + + status = pico_vm_call(vm, "notCallable", 0, NULL, &result); + + if(status != PICO_STATUS_RUNTIME_ERROR){ + fprintf(stderr, "Expected non-callable global to fail.\n"); + pico_vm_destroy(vm); + return 1; + } + + error = pico_vm_last_error(vm); + + if(error == NULL || strstr(error, "Can only call") == NULL){ + fprintf(stderr, "Unexpected non-callable error: %s\n", + error != NULL ? error : ""); + pico_vm_destroy(vm); + return 1; + } + + printf("Captured non-callable error: %s\n", error); + + status = pico_vm_call(vm, "failCall", 0, NULL, &result); + + if(status != PICO_STATUS_RUNTIME_ERROR){ + fprintf(stderr, "Expected failCall() to produce a runtime error.\n"); + pico_vm_destroy(vm); + return 1; + } + + error = pico_vm_last_error(vm); + + if(error == NULL || strstr(error, "expects two numbers") == NULL){ + fprintf(stderr, "Unexpected nested callback error: %s\n", + error != NULL ? error : ""); + pico_vm_destroy(vm); + return 1; + } + + printf("Captured nested callback error: %s\n", error); + + /* + * Verify that the VM remains usable after a failed host-triggered call. + */ + status = pico_vm_call(vm, "add", 2, addArgs, &result); + + if(status != PICO_STATUS_OK || + result.type != PICO_VALUE_NUMBER || + result.as.number != 42.0){ + int exitCode = reportFailure(vm, "Calling add after recovery", status); + pico_vm_destroy(vm); + return exitCode; + } + + printf("VM recovered after failed call: %.14g\n", result.as.number); + + pico_vm_destroy(vm); + return 0; +} \ No newline at end of file diff --git a/vm/vm.c b/vm/vm.c index 8ff8f73..72544b9 100644 --- a/vm/vm.c +++ b/vm/vm.c @@ -213,7 +213,9 @@ static void closeUpvalues(VM* vm, Value* last){ InterpreterStatus interpret(VM* vm, const char* code, const char* srcName){ vm->lastError[0] = '\0'; + Value* stackBase = vm->stackTop; ObjectFunc* func = compile(vm, code, srcName); + if(func == NULL){ snprintf(vm->lastError, sizeof(vm->lastError), "Compilation failed."); return VM_COMPILE_ERROR; @@ -234,7 +236,9 @@ InterpreterStatus interpret(VM* vm, const char* code, const char* srcName){ if(status == VM_RUNTIME_ERROR){ recover(vm); - } + }else{ + vm->stackTop = stackBase; + } // restore stack top to the base before the call return status; } @@ -1462,9 +1466,10 @@ static InterpreterStatus run(VM* vm){ vm->frameCount--; if(vm->frameCount == 0){ - pop(vm); + vm->stackTop = calleeBase; + push(vm, result); return VM_OK; - } + } // save the result frame = &vm->frames[vm->frameCount - 1]; @@ -1568,6 +1573,69 @@ static bool callValue(VM* vm, Value callee, int argCnt){ return false; } +InterpreterStatus vmCallValue( + VM* vm, + Value callee, + int argCnt, + const Value* args, + Value* result +){ + if(vm->frameCount != 0){ + runtimeError(vm, "PiCo VM calls are not reentrant."); + return VM_RUNTIME_ERROR; + } + + if(argCnt < 0 || (argCnt > 0 && args == NULL)){ + runtimeError(vm, "Invalid argument count or null arguments."); + return VM_RUNTIME_ERROR; + } + + Value* stackBase = vm->stackTop; + + push(vm, callee); + + for(int i = 0; i < argCnt; i++){ + push(vm, args[i]); + } + + if(!callValue(vm, callee, argCnt)){ + recover(vm); + return VM_RUNTIME_ERROR; + } + + InterpreterStatus status = VM_OK; + + /* + * Native C functions complete immediately, so we can return the result directly. + * PiCo closures push a new frame onto the stack, and we need to run the VM loop to execute it. + */ + + if(vm->frameCount > 0){ + status = run(vm); + } + + if(status == VM_RUNTIME_ERROR){ + recover(vm); + return status; + } + + if(vm->stackTop <= stackBase){ + runtimeError(vm, "Stack underflow after PiCo function call."); + recover(vm); + return VM_RUNTIME_ERROR; + } + + Value returnValue = pop(vm); + vm->stackTop = stackBase; + // restore stack top to the base before the call + + if(result != NULL){ + *result = returnValue; + } + + return VM_OK; +} + void runtimeError(VM* vm, const char* format, ...){ va_list args; va_start(args, format); diff --git a/vm/vm.h b/vm/vm.h index 3389022..e88b468 100644 --- a/vm/vm.h +++ b/vm/vm.h @@ -113,6 +113,7 @@ static bool isTruthy(Value value); static bool checkAccess(VM* vm, ObjectClass* instanceKlass, ObjectString* fieldName); InterpreterStatus interpret(VM* vm, const char* code, const char* srcName); +InterpreterStatus vmCallValue(VM* vm, Value callee, int argCount, const Value* args, Value* result); static InterpreterStatus run(VM* vm); static bool call(VM* vm, ObjectClosure* closure, int argCnt); From bc7847ea71816d68357e8e8851a0426818ae7df2 Mon Sep 17 00:00:00 2001 From: Theodore Cooper Date: Sat, 11 Jul 2026 16:27:50 +0800 Subject: [PATCH 5/6] feat: implement writer interface and callback --- api/pico.c | 20 ++++++++++++++++- core/object.c | 39 ++++++++++++++++---------------- core/object.h | 3 +++ core/value.c | 14 +++++++----- core/value.h | 4 ++-- core/writer.c | 44 ++++++++++++++++++++++++++++++++++++ core/writer.h | 17 ++++++++++++++ include/pico.h | 17 ++++++++++++-- vm/debug.c | 14 ++++++++++-- vm/vm.c | 61 +++++++++++++++++++++++++++++++++++++++++++++++--- vm/vm.h | 12 ++++++++++ 11 files changed, 210 insertions(+), 35 deletions(-) create mode 100644 core/writer.c create mode 100644 core/writer.h diff --git a/api/pico.c b/api/pico.c index cf87e53..25a6e1e 100644 --- a/api/pico.c +++ b/api/pico.c @@ -204,6 +204,24 @@ int pico_call_arg_count(const PicoCall* call){ return call->argCount; } +void pico_vm_set_output(PicoVM* vm, PicoWriteFunc func, void* userData){ + if(vm == NULL){ + return; + } + + vm->output.write = func; + vm->output.userData = userData; +} + +void pico_vm_set_error_output(PicoVM* vm, PicoWriteFunc func, void* userData){ + if(vm == NULL){ + return; + } + + vm->errOutput.write = func; + vm->errOutput.userData = userData; +} + PicoValueType pico_call_arg_type(const PicoCall* call, int index){ if(!isValidArgIndex(call, index)){ return PICO_VALUE_OTHER; @@ -326,7 +344,7 @@ void pico_call_error(PicoCall* call, const char* message){ setCallResult(call, NULL_VAL); } -PicoStatus pico_vm_register_native(PicoVM* vm, const char* name, PicoNativeFn function, void* user_data){ +PicoStatus pico_vm_register_native(PicoVM* vm, const char* name, PicoNativeFunc function, void* user_data){ if(vm == NULL || name == NULL || name[0] == '\0' || function == NULL){ return PICO_STATUS_INVALID_ARGUMENT; } diff --git a/core/object.c b/core/object.c index 3adb79b..f778029 100644 --- a/core/object.c +++ b/core/object.c @@ -399,66 +399,67 @@ void freeObjects(VM* vm){ } } -void printObject(Value value){ +void objectWrite(Value value, Writer* writer){ switch(OBJECT_TYPE(value)){ case OBJECT_STRING: - printf("%s", AS_CSTRING(value)); + writerWCString(writer, AS_CSTRING(value)); break; case OBJECT_LIST: { ObjectList* list = AS_LIST(value); printf("["); for(int i = 0; i < list->count; i++){ - printValue(list->items[i]); - if(i < list->count - 1) printf(", "); + objectWrite(list->items[i], writer); + if(i < list->count - 1) + writerWCString(writer, ", "); } - printf("]"); + writerWCString(writer, "]"); break; } case OBJECT_MAP: - printf("{map}"); + writerWCString(writer, "{map}"); break; case OBJECT_FUNC: if(AS_FUNC(value)->name == NULL) - printf("