diff --git a/.gitignore b/.gitignore index ceee666..48a69a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,7 @@ json.xml lib/*.so out/ src/*.o +src/rust/Cargo.lock +src/rust/target *.swp tmp* diff --git a/CMakeLists.txt b/CMakeLists.txt index a6448c9..fbfca89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.62 LANGUAGES C) +project(fun VERSION 0.38.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -11,11 +11,114 @@ include(${CMAKE_SOURCE_DIR}/cmake/Dependencies.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/Extensions.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Targets.cmake) +# --- Always show key build toggles --- +# Normalize to ENABLED/DISABLED like the "Fun extension summary" +if(FUN_DEBUG) + set(_FUN_DEBUG_STATE "ENABLED") +else() + set(_FUN_DEBUG_STATE "DISABLED") +endif() + +if(FUN_WITH_RUST) + set(_FUN_WITH_RUST_STATE "ENABLED") +else() + set(_FUN_WITH_RUST_STATE "DISABLED") +endif() + +message(STATUS "==== Fun build options ====") +message(STATUS " FUN_DEBUG: ${_FUN_DEBUG_STATE}") +message(STATUS " FUN_WITH_RUST: ${_FUN_WITH_RUST_STATE}") +message(STATUS "===========================") + # Convenience aggregate target (like 'build' in Makefile) add_custom_target(build DEPENDS fun fun_test test_opcodes ) +# --- Rust (Cargo) integration: optionally build and link a staticlib with opcode examples --- +option(FUN_WITH_RUST "Build and link Rust opcode library" OFF) + +if(FUN_WITH_RUST) + find_program(CARGO_EXECUTABLE cargo) + if(NOT CARGO_EXECUTABLE) + message(FATAL_ERROR "FUN_WITH_RUST=ON but 'cargo' not found in PATH") + endif() + + set(RUST_CRATE_DIR ${CMAKE_SOURCE_DIR}/src/rust) + + # Map CMake build type to Cargo profile and output path + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(RUST_PROFILE "debug") + set(RUST_BUILD_ARGS) + else() + set(RUST_PROFILE "release") + set(RUST_BUILD_ARGS --release) + endif() + + # Crate name as per Cargo.toml + # Crate name on-disk replaces '-' with '_' in artifact names + set(RUST_CRATE_NAME hello-c-world) + string(REPLACE "-" "_" RUST_CRATE_BASENAME "${RUST_CRATE_NAME}") + set(RUST_LIB_NAME lib${RUST_CRATE_BASENAME}.a) + set(RUST_LIB_PATH ${RUST_CRATE_DIR}/target/${RUST_PROFILE}/${RUST_LIB_NAME}) + + add_custom_command( + OUTPUT ${RUST_LIB_PATH} + COMMAND ${CARGO_EXECUTABLE} build ${RUST_BUILD_ARGS} + WORKING_DIRECTORY ${RUST_CRATE_DIR} + COMMENT "Building Rust static library (${RUST_PROFILE})" + VERBATIM + ) + + add_custom_target(rust_ops_build DEPENDS ${RUST_LIB_PATH}) + + add_library(fun_ops STATIC IMPORTED GLOBAL) + set_target_properties(fun_ops PROPERTIES + IMPORTED_LOCATION ${RUST_LIB_PATH} + IMPORTED_LINK_INTERFACE_LANGUAGES C + ) + add_dependencies(fun_ops rust_ops_build) + + # Link Rust ops into the core so executables can call them + if(TARGET fun_core) + add_dependencies(fun_core rust_ops_build) + target_link_libraries(fun_core PRIVATE fun_ops) + target_compile_definitions(fun_core PRIVATE FUN_WITH_RUST) + endif() + + # Propagate define to test targets that might call Rust + if(TARGET test_opcodes) + add_dependencies(test_opcodes rust_ops_build) + target_link_libraries(test_opcodes PRIVATE fun_ops) + target_compile_definitions(test_opcodes PRIVATE FUN_WITH_RUST) + endif() + + if(TARGET fun) + target_compile_definitions(fun PRIVATE FUN_WITH_RUST) + endif() +endif() + +# --- Size optimization for final binaries (Release) --- +# Enable function/data sectioning for better GC; safe for all configs. +add_compile_options(-ffunction-sections -fdata-sections) + +# Link-time garbage collection and stripping for the main executable in Release +if(TARGET fun) + # Garbage-collect unused sections; also strip symbols (-s) in Release + target_link_options(fun PRIVATE + $<$:-Wl,--gc-sections -s> + ) + # Prefer enabling LTO/IPO for Release builds + set_property(TARGET fun PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) +endif() + +if(TARGET fun_core) + # LTO/IPO for the core library helps the final link DCE more Rust/C glue + set_property(TARGET fun_core PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + # Ensure objects compiled with sectioning (added globally above) benefit from GC + target_link_options(fun_core PRIVATE $<$:-Wl,--gc-sections>) +endif() + # Convenience targets: repl, run, threads-demo, ops, examples set(FUN_RUN_SCRIPT "" CACHE STRING "Script to run with the 'run' target, e.g. -DFUN_RUN_SCRIPT=examples/strings_test.fun") diff --git a/examples/rust_hello.fun b/examples/rust_hello.fun new file mode 100644 index 0000000..5cd23d2 --- /dev/null +++ b/examples/rust_hello.fun @@ -0,0 +1,32 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-27 + */ + +/* + * Rust-backed opcode demo: rust_hello() + * + * Build instructions: + * - Default builds disable Rust integration. + * - Enable it via: cmake -S . -B build_debug -DFUN_WITH_RUST=ON + * - Then: cmake --build build_debug --target fun + * + * Run: + * build_debug/fun examples/rust_hello.fun + * + * Expected output (with FUN_WITH_RUST=ON): + * Hello from Rust ops! + * + * If built without Rust, calling rust_hello() will raise a runtime error + * explaining that Rust integration is disabled. + */ + +print(rust_hello()) diff --git a/src/bytecode.c b/src/bytecode.c index 80a4ce7..58b62a8 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -206,6 +206,7 @@ static const char *opcode_name(OpCode op) { case OP_SIGN: return "SIGN"; case OP_FMIN: return "FMIN"; case OP_FMAX: return "FMAX"; + case OP_RUST_HELLO: return "RUST_HELLO"; default: return "???"; } } diff --git a/src/bytecode.h b/src/bytecode.h index c1b9434..0cb4908 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -263,6 +263,9 @@ typedef enum { OP_FMIN, // pops b, a (int/float); pushes fmin(a,b) (NaN handling per C99) OP_FMAX, // pops b, a (int/float); pushes fmax(a,b) (NaN handling per C99) + // Rust FFI demo opcode(s) + OP_RUST_HELLO, // pushes string returned from Rust (hello world) + /* Notcurses TUI (optional) */ OP_NC_INIT, // initializes Notcurses; returns 1 on success, 0 on failure OP_NC_SHUTDOWN, // shuts down Notcurses; returns 0 diff --git a/src/parser.c b/src/parser.c index e110f68..78da293 100644 --- a/src/parser.c +++ b/src/parser.c @@ -786,6 +786,13 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + if (strcmp(name, "rust_hello") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "rust_hello expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_RUST_HELLO, 0); + free(name); + return 1; + } if (strcmp(name, "os_list_dir") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "os_list_dir expects (path)"); free(name); return 0; } diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000..a6c7500 --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "hello-c-world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[profile.release] +panic = "abort" +# Optimize for minimal size +opt-level = "z" +codegen-units = 1 +lto = true +# If Cargo is new enough, this strips symbols from Rust objects +strip = "symbols" diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs new file mode 100644 index 0000000..12121ed --- /dev/null +++ b/src/rust/src/lib.rs @@ -0,0 +1,44 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-27 + */ + +#![no_std] + +#[repr(C)] +pub struct Vm; + +extern "C" { + fn vm_pop_i64(vm: *mut Vm) -> i64; + fn vm_push_i64(vm: *mut Vm, v: i64); +} + +// Submodule with additional Rust VM math ops (exported via C ABI) +pub mod vm; + +#[no_mangle] +pub extern "C" fn fun_op_radd(vm: *mut Vm) -> i32 { + unsafe { + let b = vm_pop_i64(vm); + let a = vm_pop_i64(vm); + vm_push_i64(vm, a + b); + } + 0 +} + +#[no_mangle] +pub extern "C" fn fun_rust_get_string() -> *const core::ffi::c_char { + b"Hello from Rust ops!\0".as_ptr() as *const _ +} + +// Minimal panic handler for no_std; abort behavior requested via Cargo profile +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/src/test_opcodes.c b/src/test_opcodes.c index c6c6e3a..db79283 100644 --- a/src/test_opcodes.c +++ b/src/test_opcodes.c @@ -46,6 +46,29 @@ int main() { } vm_clear_output(&vm); + + /* --- Rust FFI demo: call a Rust opcode and string function --- */ +#ifdef FUN_WITH_RUST + extern int fun_op_radd(VM *vm); + extern const char *fun_rust_get_string(void); + + printf("=== Rust FFI demo ===\n"); + const char *rs = fun_rust_get_string(); + if (rs) { + printf("Rust says: %s\n", rs); + } + + /* prepare stack: push 10 and 32, then call Rust add -> expect 42 */ + vm_push_i64(&vm, 10); + vm_push_i64(&vm, 32); + int rc = fun_op_radd(&vm); + printf("fun_op_radd rc=%d\n", rc); + long long sum = (long long)vm_pop_i64(&vm); + printf("Rust op result: %lld\n", sum); +#else + printf("=== Rust FFI demo (disabled; build with -DFUN_WITH_RUST=ON) ===\n"); +#endif + vm_free(&vm); bytecode_free(bc); return 0; diff --git a/src/vm.c b/src/vm.c index 16ac20b..4c78db9 100644 --- a/src/vm.c +++ b/src/vm.c @@ -466,6 +466,28 @@ static Value pop_value(VM *vm) { return vm->stack[vm->sp--]; /* caller owns returned Value */ } +/* --- C ABI helpers for Rust FFI --- */ +int64_t vm_pop_i64(VM *vm) { + Value v = pop_value(vm); + int64_t out = 0; + if (v.type == VAL_INT) { + out = v.i; + } else if (v.type == VAL_FLOAT) { + out = (int64_t) v.d; + } else { + fprintf(stderr, "Runtime type error: expected int/float on stack, got %s\n", value_type_name(v.type)); + free_value(v); + exit(1); + } + /* free any dynamic payload (no-op for int/float) */ + free_value(v); + return out; +} + +void vm_push_i64(VM *vm, int64_t v) { + push_value(vm, make_int(v)); +} + static void frame_init(Frame *f) { f->fn = NULL; f->ip = 0; @@ -734,6 +756,9 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/math/isqrt.c" #include "vm/math/sign.c" + /* Rust FFI demo opcode(s) */ + #include "vm/rust/hello.c" + #include "vm/os/env.c" #include "vm/os/env_all.c" #include "vm/os/fun_version.c" diff --git a/src/vm.h b/src/vm.h index a08a891..0e1d528 100644 --- a/src/vm.h +++ b/src/vm.h @@ -54,6 +54,9 @@ static const char *opcode_names[] = { "SERIAL_OPEN","SERIAL_CONFIG","SERIAL_SEND","SERIAL_RECV","SERIAL_CLOSE", "TK_EVAL","TK_RESULT","TK_LOOP","TK_WM_TITLE","TK_LABEL","TK_BUTTON","TK_PACK", "TRY_PUSH","TRY_POP","THROW", + "FMIN","FMAX", + /* Rust FFI demo */ + "RUST_HELLO", /* Notcurses TUI (optional) */ "NC_INIT","NC_SHUTDOWN","NC_CLEAR","NC_DRAW_TEXT","NC_GETCH" }; @@ -143,4 +146,16 @@ static inline int opcode_is_valid(int op) { return op >= OP_NOP && op <= OP_NC_GETCH; // all current opcodes (including optional NC_*) } +/* --- Minimal C ABI helpers for FFI (Rust opcode experiments) --- */ +/* Pop an int64 from VM stack (errors if not an int/float); returns integer-converted value. */ +int64_t vm_pop_i64(VM *vm); +/* Push an int64 onto VM stack. */ +void vm_push_i64(VM *vm, int64_t v); + +/* Example Rust-implemented opcode (adds top two ints on stack) */ +int fun_op_radd(VM *vm); + +/* Example Rust function returning a demo C string (null-terminated). */ +const char *fun_rust_get_string(void); + #endif diff --git a/src/vm/rust/hello.c b/src/vm/rust/hello.c new file mode 100644 index 0000000..709d8c6 --- /dev/null +++ b/src/vm/rust/hello.c @@ -0,0 +1,27 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-27 + */ + + /** + * Rust FFI demo opcode: OP_RUST_HELLO + * When executed, it pushes a hello string returned by Rust onto the VM stack. + */ + +case OP_RUST_HELLO: { +#ifdef FUN_WITH_RUST + const char *s = fun_rust_get_string(); + if (!s) s = ""; + push_value(vm, make_string(s)); +#else + vm_raise_error(vm, "RUST_HELLO requires FUN_WITH_RUST=ON at build time"); + push_value(vm, make_nil()); +#endif + break; +}