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 a9af212..ca7c5d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,24 +4,24 @@ project(PiCo VERSION 0.1.0 LANGUAGES C) include(GNUInstallDirs) -set(CMAKE_C_COMPILER "gcc") - if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU") - message(FATAL_ERROR "FATAL: This project requires the GCC compiler due to specific GNU C extensions. " - "The attempt to set GCC as the compiler failed. Please ensure 'gcc' is installed and in your PATH.") + message(FATAL_ERROR + "PiCo requires GCC because it uses GNU C extensions. " + "Configure CMake with a GCC-based toolchain or preset." + ) endif() set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS ON) -file(GLOB_RECURSE CORE_SRC "core/*.c") -file(GLOB_RECURSE VM_SRC "vm/*.c") -file(GLOB_RECURSE COMPILER_SRC "compiler/*.c") +file(GLOB_RECURSE CORE_SRC CONFIGURE_DEPENDS "core/*.c") +file(GLOB_RECURSE VM_SRC CONFIGURE_DEPENDS "vm/*.c") +file(GLOB_RECURSE COMPILER_SRC CONFIGURE_DEPENDS "compiler/*.c") file(GLOB_RECURSE STD_SRC CONFIGURE_DEPENDS "std/*.c") -file(GLOB_RECURSE CMD_SRC "cmd/*.c") +file(GLOB_RECURSE CMD_SRC CONFIGURE_DEPENDS "cmd/*.c") -include_directories( +set(PICO_INTERNAL_INCLUDE_DIRS cmd core vm @@ -30,15 +30,22 @@ include_directories( vendor/xxhash ) -add_library(xxhash_lib STATIC vendor/xxhash/xxhash.c) -target_include_directories(xxhash_lib PUBLIC vendor/xxhash) +set(PICO_API_SRC + api/pico.c +) -if(NOT WIN32) - add_library(linenoise_lib STATIC vendor/linenoise/linenoise.c) - target_include_directories(linenoise_lib PUBLIC vendor/linenoise) - add_definitions(-DUSE_LINENOISE) -else() -endif() +set(PICO_VENDOR_SRC + vendor/xxhash/xxhash.c +) + +set(PICO_RUNTIME_SRC + ${PICO_API_SRC} + ${PICO_VENDOR_SRC} + ${CORE_SRC} + ${VM_SRC} + ${COMPILER_SRC} + ${STD_SRC} +) add_compile_options( $<$:-g> @@ -46,41 +53,191 @@ add_compile_options( $<$:-O3> ) -add_executable(pico - ${CORE_SRC} - ${VM_SRC} - ${COMPILER_SRC} - ${STD_SRC} +add_library(picort STATIC + ${PICO_RUNTIME_SRC} +) + +target_include_directories(picort + PUBLIC + $ + $ + PRIVATE + ${PICO_INTERNAL_INCLUDE_DIRS} +) + +set_target_properties(picort PROPERTIES + OUTPUT_NAME pico +) + +target_link_libraries(picort + PRIVATE + 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) +target_include_directories(pico + PRIVATE + ${PICO_INTERNAL_INCLUDE_DIRS} +) + +target_link_libraries(pico + PRIVATE + picort +) + +if(NOT WIN32) + add_library(linenoise_lib STATIC + vendor/linenoise/linenoise.c + ) + + target_include_directories(linenoise_lib + PUBLIC + vendor/linenoise + ) + + target_compile_definitions(pico + PRIVATE + USE_LINENOISE + ) + + target_link_libraries(pico + PRIVATE + linenoise_lib + ) endif() +add_executable(pico_embed_basic + examples/embedding/basic.c +) + +target_link_libraries(pico_embed_basic + PRIVATE + picort +) + +add_executable(pico_embed_exit_policy + tests/embedding_exit_policy.c +) + +target_link_libraries(pico_embed_exit_policy + PRIVATE + picort +) + +add_executable(pico_embed_native_function + tests/embedding_native_function.c +) + +target_link_libraries(pico_embed_native_function + PRIVATE + picort +) + +add_executable(pico_embed_output_callback + tests/embedding_output_callback.c +) + +target_link_libraries(pico_embed_output_callback + PRIVATE + picort +) + +add_executable(pico_embed_call_script + tests/embedding_call_script.c +) + +target_link_libraries(pico_embed_call_script + PRIVATE + picort +) + 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 $ + ) + + add_test( + NAME pico_embedding_native_function + COMMAND $ + ) + + add_test( + NAME pico_embedding_call_script + COMMAND $ + ) + + add_test( + NAME pico_embedding_output_callback + COMMAND $ ) endif() add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - DEPENDS pico + DEPENDS + pico + pico_embed_basic + pico_embed_exit_policy + pico_embed_native_function + pico_embed_call_script + pico_embed_output_callback WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) -install(TARGETS pico +install(TARGETS pico picort RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +install(FILES include/pico.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) install(FILES README.md manual.md LICENSE gpl-3.0.txt @@ -89,7 +246,10 @@ install(FILES README.md manual.md LICENSE gpl-3.0.txt install(DIRECTORY examples/ DESTINATION ${CMAKE_INSTALL_DOCDIR}/examples - FILES_MATCHING PATTERN "*.pcs" + FILES_MATCHING + PATTERN "*.pcs" + PATTERN "*.c" + PATTERN "README.md" ) -message(STATUS "Project 'PiCo' configured to use C Compiler: ${CMAKE_C_COMPILER}") +message(STATUS "Project 'PiCo' configured to use C Compiler: ${CMAKE_C_COMPILER}") \ No newline at end of file diff --git a/README.md b/README.md index 4ec1a64..e7bcf60 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,83 @@ To install system-wide: sudo cmake --install build/release ``` +## Embedding PiCo in C + +PiCo can also be used as an embedded scripting VM inside a C program. The host program owns a `PicoVM`, loads PiCo source code, registers native C functions, calls PiCo functions, and can capture script output and runtime errors. + +A minimal embedded program looks like this: + +```c +#include +#include + +int main(void){ + PicoVM* vm = pico_vm_create(); + + if(vm == NULL){ + return 1; + } + + PicoStatus status = pico_vm_eval(vm, + "func add(a, b){ return a + b; }\n", + "" + ); + + if(status != PICO_STATUS_OK){ + fprintf(stderr, "%s\n", pico_vm_last_error(vm)); + pico_vm_destroy(vm); + return 1; + } + + PicoValue args[] = { + pico_value_number(20), + pico_value_number(22) + }; + + PicoValue result; + status = pico_vm_call(vm, "add", 2, args, &result); + + if(status == PICO_STATUS_OK && result.type == PICO_VALUE_NUMBER){ + printf("%.14g\n", result.as.number); + } + + pico_vm_destroy(vm); + return status == PICO_STATUS_OK ? 0 : 1; +} +``` + +The third argument of `pico_vm_eval()` is a diagnostic source name. It does not need to be a real file path; names like `""` or `""` are useful for error messages. + +The embedding API currently supports these core operations: + +- `pico_vm_create()` / `pico_vm_destroy()` for VM lifetime management +- `pico_vm_eval()` for loading PiCo source code +- `pico_vm_register_native()` for exposing C functions to PiCo +- `pico_vm_call()` for calling global PiCo functions from C +- `pico_vm_set_output()` and `pico_vm_set_error_output()` for capturing `print` output and runtime error output +- `pico_vm_last_error()` for reading the latest compile or runtime error + +More complete examples are in `examples/embedding/`. + +### Linking against an installed libpico + +After installation, an external C program can include `pico.h` and link against `libpico.a`: + +```sh +gcc main.c -I /path/to/pico/include -L /path/to/pico/lib -lpico -lm -o embed_app +``` + +On Windows with MinGW, the command is the same except paths usually point to the local install prefix, for example: + +```powershell +gcc .\main.c ` + -I .\install-debug\include ` + -L .\install-debug\lib ` + -lpico ` + -lm ` + -o .\embed_app.exe +``` + ## Testing Run the test suite with *CTest*: diff --git a/api/pico.c b/api/pico.c new file mode 100644 index 0000000..25a6e1e --- /dev/null +++ b/api/pico.c @@ -0,0 +1,475 @@ +#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: + 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; +} + +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; +} + +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)); + + if(vm == NULL){ + return NULL; + } + + 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; +} + +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); +} + +int pico_call_arg_count(const PicoCall* call){ + if(call == NULL){ + return 0; + } + + 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; + } + + 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, PicoNativeFunc 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; +} + +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; + } + + return vm->lastError; +} + +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"; + case PICO_STATUS_OUT_OF_MEMORY: + return "out of memory"; + case PICO_STATUS_UNSUPPORTED_TYPE: + return "unsupported type"; + } + + return "unknown status"; +} \ No newline at end of file diff --git a/core/object.c b/core/object.c index 6a692a5..b9341a3 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; @@ -391,66 +399,88 @@ 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)); + writerW(writer, AS_STRING(value)->chars, (size_t)AS_STRING(value)->length); break; - case OBJECT_LIST: - { + + case OBJECT_LIST:{ ObjectList* list = AS_LIST(value); - printf("["); + + writerWCString(writer, "["); + for(int i = 0; i < list->count; i++){ - printValue(list->items[i]); - if(i < list->count - 1) printf(", "); + valueWrite(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("