diff --git a/.gitignore b/.gitignore index a28170a..ceee666 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ CMakeCache.txt !.gitignore .venv build* -cmake* +cmake_install.cmake database.sqlite demo_* dist/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ffd71d0..a8847f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,543 +1,15 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.48 LANGUAGES C) +project(fun VERSION 0.37.57 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) include(GNUInstallDirs) -# Defaults and options migrated from Makefile -# Convenience: default path to bundled stdlib -set(FUN_LIB "${CMAKE_SOURCE_DIR}/lib" CACHE PATH "Path to bundled Fun stdlib") - -# Optional: override default library directory for #include <...> lookups -# -DDEFAULT_LIB_DIR=/custom/fun/lib (trailing slash added automatically) -if(NOT DEFINED DEFAULT_LIB_DIR OR DEFAULT_LIB_DIR STREQUAL "") - if(WIN32) - set(_FUN_DEFAULT_LIB_DIR "C:/Users/Public/fun/lib") - elseif(APPLE) - set(_FUN_DEFAULT_LIB_DIR "/Library/Application Support/fun/lib") - else() - set(_FUN_DEFAULT_LIB_DIR "/usr/share/fun/lib") - endif() - set(DEFAULT_LIB_DIR "${_FUN_DEFAULT_LIB_DIR}" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) -else() - set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) -endif() - -# Optional: build using musl libc instead of glibc on Linux (OFF by default) -# Note: Switching CMAKE_C_COMPILER must happen before the first project() call. -option(FUN_USE_MUSL "Use musl libc toolchain when available (Linux only)" OFF) - -if(FUN_USE_MUSL AND UNIX AND NOT APPLE) - # Try to find a musl toolchain wrapper - find_program(_FUN_MUSL_CC NAMES musl-gcc musl-clang) - if(_FUN_MUSL_CC) - message(STATUS "FUN_USE_MUSL=ON: using musl toolchain: ${_FUN_MUSL_CC}") - # Force C compiler to musl wrapper before project() so the whole toolchain is configured accordingly - set(CMAKE_C_COMPILER "${_FUN_MUSL_CC}" CACHE FILEPATH "C compiler" FORCE) - set(FUN_LIBC "musl" CACHE STRING "Selected C library") - else() - message(WARNING "FUN_USE_MUSL=ON but no musl toolchain (musl-gcc or musl-clang) found. Falling back to default compiler (likely glibc).") - set(FUN_LIBC "glibc" CACHE STRING "Selected C library") - endif() -else() - # Default remains the system toolchain (typically glibc on Linux) - set(FUN_LIBC "glibc" CACHE STRING "Selected C library") -endif() - -# Expose a preprocessor macro indicating selected C library -if(FUN_LIBC STREQUAL "musl") - add_definitions(-DFUN_LIBC_MUSL) -else() - add_definitions(-DFUN_LIBC_GLIBC) -endif() - -# Optional: build statically linked executables -# When enabled, prefer static libraries for all dependencies and request -# fully static linking for executables where the platform/toolchain allows it. -option(FUN_LINK_STATIC "Link fun and tests fully static where possible" OFF) - -if(FUN_LINK_STATIC) - message(STATUS "Building Fun statically linked") - # Prefer static libraries for all add_library without explicit type - set(BUILD_SHARED_LIBS OFF) - - # Make pkg-config choose static libs - set(PKG_CONFIG_USE_STATIC_LIBS ON) - - # Hint find_library to choose static archives first on Unix (not macOS) - # Prefer .a but still allow falling back to shared if static is unavailable. - if(UNIX AND NOT APPLE) - set(CMAKE_FIND_LIBRARY_SUFFIXES .a;.so;.so.0;.so.1) - endif() - - # Toolchain-specific static link flags - # Use a "mostly static" approach by default to avoid requiring static - # variants of every system/third-party library. This keeps libgcc and - # libstdc++ static while allowing shared deps when needed. - if(UNIX AND NOT APPLE AND CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") - set(_FUN_STATIC_LINK_FLAGS -static-libgcc -static-libstdc++) - endif() - - # On MSVC, prefer the static runtime - if(MSVC) - foreach(flag_var CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE - CMAKE_C_FLAGS_RELWITHDEBINFO CMAKE_C_FLAGS_MINSIZEREL) - if(DEFINED ${flag_var}) - string(REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") - endif() - endforeach() - endif() -endif() - -# Optional Tcl/Tk GUI support (embedded interpreter) -option(FUN_WITH_TCLTK "Enable Tcl/Tk GUI support" OFF) -set(TCL_INCLUDE_DIRS "") -set(TCL_LINK_LIBS "") -if(FUN_WITH_TCLTK) - message(STATUS "Building with Tcl/Tk support") - add_definitions(-DFUN_WITH_TCLTK) - # Try pkg-config first - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(TCL QUIET tcl) - pkg_check_modules(TK QUIET tk) - endif() - if(TCL_FOUND OR TK_FOUND) - if(TCL_FOUND) - list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIRS} ${TCL_INCLUDE_DIRS}) - list(APPEND TCL_LINK_LIBS ${TCL_LINK_LIBS} ${TCL_LIBRARIES}) - endif() - if(TK_FOUND) - list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIRS} ${TK_INCLUDE_DIRS}) - list(APPEND TCL_LINK_LIBS ${TCL_LINK_LIBS} ${TK_LIBRARIES}) - endif() - if(TCL_INCLUDE_DIRS) - include_directories(${TCL_INCLUDE_DIRS}) - endif() - else() - # Fallbacks by platform (best-effort) - find_path(TCL_INCLUDE_DIR tcl.h PATH_SUFFIXES tcl8.7 tcl8.6 include) - find_library(TCL_LIB NAMES tcl8.7 tcl8.6 tcl) - find_library(TK_LIB NAMES tk8.7 tk8.6 tk) - if(TCL_INCLUDE_DIR) - list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIR}) - include_directories(${TCL_INCLUDE_DIR}) - endif() - if(TCL_LIB) - list(APPEND TCL_LINK_LIBS ${TCL_LIB}) - endif() - if(TK_LIB) - list(APPEND TCL_LINK_LIBS ${TK_LIB}) - endif() - if(APPLE) - # On macOS additional frameworks are usually not required; Homebrew libs suffice - elseif(WIN32) - # Typical Windows GUI libs - list(APPEND TCL_LINK_LIBS user32 gdi32 comctl32) - else() - # X11 may be required on some Linux setups - find_package(X11 QUIET) - if(X11_FOUND) - include_directories(${X11_INCLUDE_DIR}) - list(APPEND TCL_LINK_LIBS ${X11_LIBRARIES}) - endif() - endif() - if(NOT TCL_LINK_LIBS) - message(WARNING "Tcl/Tk libraries not found via pkg-config or fallbacks. FUN_WITH_TCLTK is enabled, but linking may fail.") - endif() - endif() -endif() -# Ensure trailing slash -if(NOT DEFAULT_LIB_DIR MATCHES "/$") - set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}/") -endif() - -# Optional SQLite support -option(FUN_WITH_SQLITE "Enable SQLite (sqlite3) support" OFF) -set(SQLITE3_INCLUDE_DIRS "") -set(SQLITE3_LINK_LIBS "") -if(FUN_WITH_SQLITE) - message(STATUS "Building with SQLite support") - add_definitions(-DFUN_WITH_SQLITE) - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(SQLITE3 QUIET sqlite3) - endif() - if(SQLITE3_FOUND) - list(APPEND SQLITE3_INCLUDE_DIRS ${SQLITE3_INCLUDE_DIRS} ${SQLITE3_INCLUDE_DIRS}) - list(APPEND SQLITE3_LINK_LIBS ${SQLITE3_LINK_LIBS} ${SQLITE3_LIBRARIES}) - include_directories(${SQLITE3_INCLUDE_DIRS}) - else() - find_library(SQLITE3_LIB sqlite3) - if(SQLITE3_LIB) - list(APPEND SQLITE3_LINK_LIBS ${SQLITE3_LIB}) - else() - message(FATAL_ERROR "sqlite3 not found. Install sqlite3 (dev headers) or disable FUN_WITH_SQLITE.") - endif() - endif() -endif() - -# Optional PCSC support (enabled via -DFUN_WITH_PCSC=ON) -option(FUN_WITH_PCSC "Enable PCSC (pcsclite) support" OFF) -set(PCSC_LINK_LIBS "") -set(PCSC_INCLUDE_DIRS "") -if(FUN_WITH_PCSC) - message(STATUS "Building with PCSC support") - add_definitions(-DFUN_WITH_PCSC) - if(APPLE) - list(APPEND PCSC_LINK_LIBS "-framework PCSC") - else() - list(APPEND PCSC_INCLUDE_DIRS "/usr/include/PCSC") - list(APPEND PCSC_LINK_LIBS pcsclite) - endif() -endif() - -# Optional JSON (json-c) support -option(FUN_WITH_JSON "Enable JSON (json-c) support" OFF) -set(JSONC_INCLUDE_DIRS "") -set(JSONC_LINK_LIBS "") -if(FUN_WITH_JSON) - message(STATUS "Building with JSON (json-c) support") - add_definitions(-DFUN_WITH_JSON) - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(JSONC QUIET json-c) - endif() - if(JSONC_FOUND) - list(APPEND JSONC_INCLUDE_DIRS ${JSONC_INCLUDE_DIRS} ${JSONC_INCLUDE_DIRS}) - list(APPEND JSONC_LINK_LIBS ${JSONC_LINK_LIBS} ${JSONC_LIBRARIES}) - include_directories(${JSONC_INCLUDE_DIRS}) - else() - # Fallback: try plain -ljson-c - list(APPEND JSONC_LINK_LIBS json-c) - endif() -endif() - -# Optional INI (iniparser 4.2.6) support -option(FUN_WITH_INI "Enable INI (iniparser) support" OFF) -set(INIPARSER_INCLUDE_DIRS "") -set(INIPARSER_LINK_LIBS "") -if(FUN_WITH_INI) - message(STATUS "Building with INI (iniparser) support") - add_definitions(-DFUN_WITH_INI) - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(INIPARSER QUIET iniparser) - endif() - if(INIPARSER_FOUND) - list(APPEND INIPARSER_INCLUDE_DIRS ${INIPARSER_INCLUDE_DIRS} ${INIPARSER_INCLUDE_DIRS}) - list(APPEND INIPARSER_LINK_LIBS ${INIPARSER_LINK_LIBS} ${INIPARSER_LIBRARIES}) - else() - # Fallback: try to locate headers and library manually - find_path(INIPARSER_INCLUDE_DIR iniparser.h) - find_library(INIPARSER_LIB NAMES iniparser) - if(INIPARSER_INCLUDE_DIR) - list(APPEND INIPARSER_INCLUDE_DIRS ${INIPARSER_INCLUDE_DIR}) - endif() - if(INIPARSER_LIB) - list(APPEND INIPARSER_LINK_LIBS ${INIPARSER_LIB}) - endif() - if(NOT INIPARSER_INCLUDE_DIRS OR NOT INIPARSER_LINK_LIBS) - message(FATAL_ERROR "iniparser not found. Install iniparser (>=4.2.6) or disable FUN_WITH_INI.") - endif() - endif() -endif() - -# Optional XML (libxml2) support -option(FUN_WITH_XML2 "Enable XML (libxml2) support" OFF) -set(LIBXML2_INCLUDE_DIRS "") -set(LIBXML2_LINK_LIBS "") -if(FUN_WITH_XML2) - message(STATUS "Building with XML (libxml2) support") - add_definitions(-DFUN_WITH_XML2) - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(LIBXML2 QUIET libxml-2.0) - endif() - if(LIBXML2_FOUND) - list(APPEND LIBXML2_INCLUDE_DIRS ${LIBXML2_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIRS}) - list(APPEND LIBXML2_LINK_LIBS ${LIBXML2_LINK_LIBS} ${LIBXML2_LIBRARIES}) - include_directories(${LIBXML2_INCLUDE_DIRS}) - else() - find_library(LIBXML2_LIB NAMES xml2 libxml2) - if(LIBXML2_LIB) - list(APPEND LIBXML2_LINK_LIBS ${LIBXML2_LIB}) - # Common system include location for libxml2 headers - if(EXISTS "/usr/include/libxml2") - list(APPEND LIBXML2_INCLUDE_DIRS "/usr/include/libxml2") - include_directories(${LIBXML2_INCLUDE_DIRS}) - endif() - else() - message(FATAL_ERROR "libxml2 not found. Install libxml2 (dev headers) or disable FUN_WITH_XML2.") - endif() - endif() - # As a robust fallback, add standard system include path for libxml2 if present - if(EXISTS "/usr/include/libxml2") - include_directories("/usr/include/libxml2") - endif() -endif() - -# Optional libsql support (independent from SQLite) -option(FUN_WITH_LIBSQL "Enable libsql (Turso) client support" OFF) -set(LIBSQL_INCLUDE_DIRS "") -set(LIBSQL_LINK_LIBS "") -if(FUN_WITH_LIBSQL) - message(STATUS "Building with libSQL support") - add_definitions(-DFUN_WITH_LIBSQL) - # Try pkg-config for libsql first; some systems expose libsql-client - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(LIBSQL QUIET libsql) - if(NOT LIBSQL_FOUND) - pkg_check_modules(LIBSQL_CLIENT QUIET libsql-client) - if(LIBSQL_CLIENT_FOUND) - set(LIBSQL_FOUND TRUE) - set(LIBSQL_INCLUDE_DIRS ${LIBSQL_CLIENT_INCLUDE_DIRS}) - set(LIBSQL_LINK_LIBS ${LIBSQL_CLIENT_LIBRARIES}) - endif() - endif() - endif() - if(LIBSQL_FOUND) - include_directories(${LIBSQL_INCLUDE_DIRS}) - else() - # Fallback: many libsql deployments provide a sqlite3-compatible client lib - # so try linking against sqlite3 as a compatibility layer. - find_library(LIBSQL_LIB sqlite3) - if(LIBSQL_LIB) - list(APPEND LIBSQL_LINK_LIBS ${LIBSQL_LIB}) - else() - message(FATAL_ERROR "libsql not found. Install libsql (or compatible sqlite3 client) or disable FUN_WITH_LIBSQL.") - endif() - endif() -endif() - -# Optional PCRE2 support -option(FUN_WITH_PCRE2 "Enable PCRE2 (Perl Compatible Regular Expressions) support" OFF) -set(PCRE2_INCLUDE_DIRS "") -set(PCRE2_LINK_LIBS "") -if(FUN_WITH_PCRE2) - message(STATUS "Building with PCRE2 support") - add_definitions(-DFUN_WITH_PCRE2) - # Try pkg-config first - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(PCRE2 QUIET libpcre2-8) - endif() - if(PCRE2_FOUND) - list(APPEND PCRE2_INCLUDE_DIRS ${PCRE2_INCLUDE_DIRS} ${PCRE2_INCLUDE_DIRS}) - list(APPEND PCRE2_LINK_LIBS ${PCRE2_LINK_LIBS} ${PCRE2_LIBRARIES}) - include_directories(${PCRE2_INCLUDE_DIRS}) - else() - # Fallback: common defaults - list(APPEND PCRE2_LINK_LIBS pcre2-8) - endif() -endif() - -# Optional CURL (libcurl) support -option(FUN_WITH_CURL "Enable libcurl HTTP client support" OFF) -set(CURL_INCLUDE_DIRS "") -set(CURL_LINK_LIBS "") -if(FUN_WITH_CURL) - message(STATUS "Building with libcurl support") - add_definitions(-DFUN_WITH_CURL) - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(LIBCURL QUIET libcurl) - endif() - if(LIBCURL_FOUND) - list(APPEND CURL_INCLUDE_DIRS ${LIBCURL_INCLUDE_DIRS}) - list(APPEND CURL_LINK_LIBS ${LIBCURL_LIBRARIES}) - include_directories(${CURL_INCLUDE_DIRS}) - else() - list(APPEND CURL_LINK_LIBS curl) - endif() -endif() - -# Debug option to enable verbose parser/VM logging -option(FUN_DEBUG "Enable extra debug logging in Fun" OFF) - -# Threads (used by demos and optionally by the runtime) -if(UNIX) - find_package(Threads QUIET) -endif() - -# Core VM/library sources -add_library(fun_core - src/bytecode.c - src/parser.c - src/value.c - src/vm.c -) - -target_include_directories(fun_core PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src -) - -# Apply options to core -if(FUN_DEBUG) - message(STATUS "FUN_DEBUG enabled: building with verbose debug logging") - target_compile_definitions(fun_core PUBLIC FUN_VERSION="${PROJECT_VERSION}") - target_compile_definitions(fun_core PUBLIC FUN_DEBUG=1) -endif() - -# Provide default stdlib directory to the runtime -target_compile_definitions(fun_core PUBLIC FUN_VERSION="${PROJECT_VERSION}") -target_compile_definitions(fun_core PUBLIC DEFAULT_LIB_DIR="${DEFAULT_LIB_DIR}") - -# PCSC include and link (if enabled) -if(PCSC_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${PCSC_INCLUDE_DIRS}) -endif() -if(PCSC_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${PCSC_LINK_LIBS}) -endif() - -# json-c include and link (if enabled) -if(JSONC_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${JSONC_INCLUDE_DIRS}) -endif() -if(JSONC_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${JSONC_LINK_LIBS}) -endif() - -# pcre2 include and link (if enabled) -if(PCRE2_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${PCRE2_INCLUDE_DIRS}) -endif() -if(PCRE2_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${PCRE2_LINK_LIBS}) -endif() - -# libcurl include and link (if enabled) -if(CURL_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${CURL_INCLUDE_DIRS}) -endif() -if(CURL_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${CURL_LINK_LIBS}) -endif() - -# sqlite3 include and link (if enabled) -if(SQLITE3_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${SQLITE3_INCLUDE_DIRS}) -endif() -if(SQLITE3_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${SQLITE3_LINK_LIBS}) -endif() - -# iniparser include and link (if enabled) -if(INIPARSER_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${INIPARSER_INCLUDE_DIRS}) -endif() -if(INIPARSER_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${INIPARSER_LINK_LIBS}) -endif() - -# libsql include and link (if enabled) -if(LIBSQL_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${LIBSQL_INCLUDE_DIRS}) -endif() -if(LIBSQL_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${LIBSQL_LINK_LIBS}) -endif() - -# libxml2 include and link (if enabled) -if(LIBXML2_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${LIBXML2_INCLUDE_DIRS}) -endif() -if(LIBXML2_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${LIBXML2_LINK_LIBS}) -endif() -if(FUN_WITH_XML2) - if(EXISTS "/usr/include/libxml2") - target_include_directories(fun_core PRIVATE "/usr/include/libxml2") - endif() -endif() - -# Tcl/Tk include and link (if enabled) -if(TCL_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${TCL_INCLUDE_DIRS}) -endif() -if(TCL_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${TCL_LINK_LIBS}) -endif() - -# Link threads if available on UNIX -if(Threads_FOUND) - target_link_libraries(fun_core PUBLIC Threads::Threads) -endif() - -# Link libm for C99 math functions if available -find_library(M_LIB m) -if(M_LIB) - target_link_libraries(fun_core PUBLIC ${M_LIB}) -endif() - -# Optional Notcurses TUI support (per-opcode files under src/vm/notcurses) -option(FUN_WITH_NOTCURSES "Enable Notcurses TUI support" OFF) -set(NOTCURSES_INCLUDE_DIRS "") -set(NOTCURSES_LINK_LIBS "") -if(FUN_WITH_NOTCURSES) - message(STATUS "FUN_WITH_NOTCURSES=ON: enabling Notcurses TUI ops") - add_definitions(-DFUN_WITH_NOTCURSES) - find_package(PkgConfig QUIET) - if(PkgConfig_FOUND) - # Prefer full 'notcurses', fall back to 'notcurses-core' - pkg_check_modules(NOTCURSES QUIET notcurses) - if(NOT NOTCURSES_FOUND) - pkg_check_modules(NOTCURSES QUIET notcurses-core) - endif() - endif() - if(NOTCURSES_FOUND) - list(APPEND NOTCURSES_INCLUDE_DIRS ${NOTCURSES_INCLUDE_DIRS} ${NOTCURSES_INCLUDE_DIRS}) - list(APPEND NOTCURSES_LINK_LIBS ${NOTCURSES_LINK_LIBS} ${NOTCURSES_LIBRARIES}) - include_directories(${NOTCURSES_INCLUDE_DIRS}) - else() - message(FATAL_ERROR "FUN_WITH_NOTCURSES=ON but notcurses was not found via pkg-config (tried notcurses and notcurses-core). Install notcurses or configure with -DFUN_WITH_NOTCURSES=OFF") - endif() -endif() - -# notcurses include and link (if enabled) -if(NOTCURSES_INCLUDE_DIRS) - target_include_directories(fun_core PRIVATE ${NOTCURSES_INCLUDE_DIRS}) -endif() -if(NOTCURSES_LINK_LIBS) - target_link_libraries(fun_core PUBLIC ${NOTCURSES_LINK_LIBS}) -endif() - -# Interpreter /usr/bin/fun -option(FUN_WITH_REPL "Enable interactive REPL in the fun CLI" OFF) -add_executable(fun - src/fun.c -) -# Provide version string to the CLI -if(FUN_WITH_REPL) - # Enable REPL compilation path and add the REPL source - target_compile_definitions(fun PRIVATE FUN_WITH_REPL=1) - target_sources(fun PRIVATE src/repl.c) -endif() -target_link_libraries(fun PRIVATE fun_core) - -# Internal test programs -add_executable(fun_test - src/fun_test.c -) -target_link_libraries(fun_test PRIVATE fun_core) - -add_executable(test_opcodes - src/test_opcodes.c -) -target_link_libraries(test_opcodes PRIVATE fun_core) - -# If static linking is requested, apply linker flags to executables -if(FUN_LINK_STATIC AND DEFINED _FUN_STATIC_LINK_FLAGS) - foreach(tgt fun fun_test test_opcodes) - if(TARGET ${tgt}) - target_link_options(${tgt} PRIVATE ${_FUN_STATIC_LINK_FLAGS}) - endif() - endforeach() -endif() +include(${CMAKE_SOURCE_DIR}/cmake/Options.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Dependencies.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/Extensions.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Targets.cmake) # Convenience aggregate target (like 'build' in Makefile) add_custom_target(build @@ -549,11 +21,11 @@ set(FUN_RUN_SCRIPT "" CACHE STRING "Script to run with the 'run' target, e.g. -D if(FUN_WITH_REPL) add_custom_target(repl - COMMAND $ - DEPENDS fun - USES_TERMINAL - COMMENT "Run Fun REPL" -) + COMMAND $ + DEPENDS fun + USES_TERMINAL + COMMENT "Run Fun REPL" + ) endif() add_custom_target(run @@ -563,21 +35,6 @@ add_custom_target(run COMMENT "Run Fun with script: ${FUN_RUN_SCRIPT}" ) -add_custom_target(threads-demo - COMMAND ${CMAKE_COMMAND} -E echo "Running Thread class demo with FUN_LIB_DIR=${FUN_LIB}" - COMMAND ${CMAKE_COMMAND} -E env FUN_LIB_DIR="${FUN_LIB}" $ ${CMAKE_SOURCE_DIR}/examples/threads_demo.fun - DEPENDS fun - USES_TERMINAL -) - -# Python for helper scripts -find_package(Python3 QUIET COMPONENTS Interpreter) -if(Python3_Interpreter_FOUND) - set(_FUN_PY "${Python3_EXECUTABLE}") -else() - set(_FUN_PY "python3") -endif() - add_custom_target(ops COMMAND ${_FUN_PY} ${CMAKE_SOURCE_DIR}/scripts/check_op_includes.py --verbose WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} @@ -592,20 +49,6 @@ add_custom_target(ops-quiet COMMENT "Opcode include check" ) -add_custom_target(examples - COMMAND ${CMAKE_COMMAND} -E env FUN_LIB_DIR="${FUN_LIB}" FUN_WITH_INI=$,1,0> ${CMAKE_SOURCE_DIR}/scripts/run_examples.sh - DEPENDS fun - USES_TERMINAL - COMMENT "Build and run all examples" -) - -add_custom_target(run-examples - COMMAND ${CMAKE_COMMAND} -E env FUN_LIB_DIR="${FUN_LIB}" FUN_WITH_INI=$,1,0> ${CMAKE_SOURCE_DIR}/scripts/run_examples.sh - DEPENDS fun - USES_TERMINAL - COMMENT "Build and run all examples" -) - # Clean and distclean add_custom_target(fun_clean COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target clean diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake new file mode 100644 index 0000000..6546c4c --- /dev/null +++ b/cmake/Dependencies.cmake @@ -0,0 +1,14 @@ +# Common dependency discovery used across the project + +# Threads (used by demos and optionally by the runtime) +if(UNIX) + find_package(Threads QUIET) +endif() + +# Python for helper scripts +find_package(Python3 QUIET COMPONENTS Interpreter) +if(Python3_Interpreter_FOUND) + set(_FUN_PY "${Python3_EXECUTABLE}") +else() + set(_FUN_PY "python3") +endif() diff --git a/cmake/Extensions/CURL.cmake b/cmake/Extensions/CURL.cmake new file mode 100644 index 0000000..bbce9da --- /dev/null +++ b/cmake/Extensions/CURL.cmake @@ -0,0 +1,17 @@ +# CURL +option(FUN_WITH_CURL "Enable libcurl HTTP client support" OFF) +set(CURL_INCLUDE_DIRS "") +set(CURL_LINK_LIBS "") +if(FUN_WITH_CURL) + add_definitions(-DFUN_WITH_CURL) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBCURL QUIET libcurl) + endif() + if(LIBCURL_FOUND) + list(APPEND CURL_INCLUDE_DIRS ${LIBCURL_INCLUDE_DIRS}) + list(APPEND CURL_LINK_LIBS ${LIBCURL_LIBRARIES}) + else() + list(APPEND CURL_LINK_LIBS curl) + endif() +endif() diff --git a/cmake/Extensions/Extensions.cmake b/cmake/Extensions/Extensions.cmake new file mode 100644 index 0000000..070f410 --- /dev/null +++ b/cmake/Extensions/Extensions.cmake @@ -0,0 +1,39 @@ +# Toggleable optional features live in per-file modules. These populate *_INCLUDE_DIRS and *_LINK_LIBS +list(PREPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules") + +# Helper to report feature status nicely +function(_fun_print_feature name flag) + if(${flag}) + message(STATUS " ${name}: ENABLED") + else() + message(STATUS " ${name}: DISABLED") + endif() +endfunction() + +# Include each extension module +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/TCLTK.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/SQLITE.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/PCSC.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/JSON.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/INI.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/XML2.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/LIBSQL.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/PCRE2.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/CURL.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/NOTCURSES.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/REPL.cmake) + +# Summary of extension toggles +message(STATUS "---- Fun extension summary ----") +_fun_print_feature("Tcl/Tk (FUN_WITH_TCLTK)" FUN_WITH_TCLTK) +_fun_print_feature("SQLite (FUN_WITH_SQLITE)" FUN_WITH_SQLITE) +_fun_print_feature("PCSC (FUN_WITH_PCSC)" FUN_WITH_PCSC) +_fun_print_feature("JSON-C (FUN_WITH_JSON)" FUN_WITH_JSON) +_fun_print_feature("INI/iniparser (FUN_WITH_INI)" FUN_WITH_INI) +_fun_print_feature("libxml2 (FUN_WITH_XML2)" FUN_WITH_XML2) +_fun_print_feature("libsql (FUN_WITH_LIBSQL)" FUN_WITH_LIBSQL) +_fun_print_feature("PCRE2 (FUN_WITH_PCRE2)" FUN_WITH_PCRE2) +_fun_print_feature("libcurl (FUN_WITH_CURL)" FUN_WITH_CURL) +_fun_print_feature("Notcurses (FUN_WITH_NOTCURSES)" FUN_WITH_NOTCURSES) +_fun_print_feature("REPL (FUN_WITH_REPL)" FUN_WITH_REPL) +message(STATUS "--------------------------------") diff --git a/cmake/Extensions/INI.cmake b/cmake/Extensions/INI.cmake new file mode 100644 index 0000000..1fe6a4c --- /dev/null +++ b/cmake/Extensions/INI.cmake @@ -0,0 +1,27 @@ +# INI (iniparser) +option(FUN_WITH_INI "Enable INI (iniparser) support" OFF) +set(INIPARSER_INCLUDE_DIRS "") +set(INIPARSER_LINK_LIBS "") +if(FUN_WITH_INI) + add_definitions(-DFUN_WITH_INI) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(INIPARSER QUIET iniparser) + endif() + if(INIPARSER_FOUND) + list(APPEND INIPARSER_INCLUDE_DIRS ${INIPARSER_INCLUDE_DIRS} ${INIPARSER_INCLUDE_DIRS}) + list(APPEND INIPARSER_LINK_LIBS ${INIPARSER_LINK_LIBS} ${INIPARSER_LIBRARIES}) + else() + find_path(INIPARSER_INCLUDE_DIR iniparser.h) + find_library(INIPARSER_LIB NAMES iniparser) + if(INIPARSER_INCLUDE_DIR) + list(APPEND INIPARSER_INCLUDE_DIRS ${INIPARSER_INCLUDE_DIR}) + endif() + if(INIPARSER_LIB) + list(APPEND INIPARSER_LINK_LIBS ${INIPARSER_LIB}) + endif() + if(NOT INIPARSER_INCLUDE_DIRS OR NOT INIPARSER_LINK_LIBS) + message(FATAL_ERROR "iniparser not found. Install iniparser (>=4.2.6) or disable FUN_WITH_INI.") + endif() + endif() +endif() diff --git a/cmake/Extensions/JSON.cmake b/cmake/Extensions/JSON.cmake new file mode 100644 index 0000000..b640793 --- /dev/null +++ b/cmake/Extensions/JSON.cmake @@ -0,0 +1,17 @@ +# JSON (json-c) +option(FUN_WITH_JSON "Enable JSON (json-c) support" OFF) +set(JSONC_INCLUDE_DIRS "") +set(JSONC_LINK_LIBS "") +if(FUN_WITH_JSON) + add_definitions(-DFUN_WITH_JSON) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(JSONC QUIET json-c) + endif() + if(JSONC_FOUND) + list(APPEND JSONC_INCLUDE_DIRS ${JSONC_INCLUDE_DIRS} ${JSONC_INCLUDE_DIRS}) + list(APPEND JSONC_LINK_LIBS ${JSONC_LINK_LIBS} ${JSONC_LIBRARIES}) + else() + list(APPEND JSONC_LINK_LIBS json-c) + endif() +endif() diff --git a/cmake/Extensions/LIBSQL.cmake b/cmake/Extensions/LIBSQL.cmake new file mode 100644 index 0000000..4272d02 --- /dev/null +++ b/cmake/Extensions/LIBSQL.cmake @@ -0,0 +1,27 @@ +# libsql +option(FUN_WITH_LIBSQL "Enable libsql (Turso) client support" OFF) +set(LIBSQL_INCLUDE_DIRS "") +set(LIBSQL_LINK_LIBS "") +if(FUN_WITH_LIBSQL) + add_definitions(-DFUN_WITH_LIBSQL) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBSQL QUIET libsql) + if(NOT LIBSQL_FOUND) + pkg_check_modules(LIBSQL_CLIENT QUIET libsql-client) + if(LIBSQL_CLIENT_FOUND) + set(LIBSQL_FOUND TRUE) + set(LIBSQL_INCLUDE_DIRS ${LIBSQL_CLIENT_INCLUDE_DIRS}) + set(LIBSQL_LINK_LIBS ${LIBSQL_CLIENT_LIBRARIES}) + endif() + endif() + endif() + if(NOT LIBSQL_FOUND) + find_library(LIBSQL_LIB sqlite3) + if(LIBSQL_LIB) + list(APPEND LIBSQL_LINK_LIBS ${LIBSQL_LIB}) + else() + message(FATAL_ERROR "libsql not found. Install libsql (or compatible sqlite3 client) or disable FUN_WITH_LIBSQL.") + endif() + endif() +endif() diff --git a/cmake/Extensions/NOTCURSES.cmake b/cmake/Extensions/NOTCURSES.cmake new file mode 100644 index 0000000..84abbee --- /dev/null +++ b/cmake/Extensions/NOTCURSES.cmake @@ -0,0 +1,20 @@ +# Notcurses +option(FUN_WITH_NOTCURSES "Enable Notcurses TUI support" OFF) +set(NOTCURSES_INCLUDE_DIRS "") +set(NOTCURSES_LINK_LIBS "") +if(FUN_WITH_NOTCURSES) + add_definitions(-DFUN_WITH_NOTCURSES) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(NOTCURSES QUIET notcurses) + if(NOT NOTCURSES_FOUND) + pkg_check_modules(NOTCURSES QUIET notcurses-core) + endif() + endif() + if(NOT NOTCURSES_FOUND) + message(FATAL_ERROR "FUN_WITH_NOTCURSES=ON but notcurses was not found via pkg-config (tried notcurses and notcurses-core). Install notcurses or configure with -DFUN_WITH_NOTCURSES=OFF") + else() + list(APPEND NOTCURSES_INCLUDE_DIRS ${NOTCURSES_INCLUDE_DIRS} ${NOTCURSES_INCLUDE_DIRS}) + list(APPEND NOTCURSES_LINK_LIBS ${NOTCURSES_LINK_LIBS} ${NOTCURSES_LIBRARIES}) + endif() +endif() diff --git a/cmake/Extensions/PCRE2.cmake b/cmake/Extensions/PCRE2.cmake new file mode 100644 index 0000000..618a481 --- /dev/null +++ b/cmake/Extensions/PCRE2.cmake @@ -0,0 +1,17 @@ +# PCRE2 +option(FUN_WITH_PCRE2 "Enable PCRE2 (Perl Compatible Regular Expressions) support" OFF) +set(PCRE2_INCLUDE_DIRS "") +set(PCRE2_LINK_LIBS "") +if(FUN_WITH_PCRE2) + add_definitions(-DFUN_WITH_PCRE2) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(PCRE2 QUIET libpcre2-8) + endif() + if(PCRE2_FOUND) + list(APPEND PCRE2_INCLUDE_DIRS ${PCRE2_INCLUDE_DIRS} ${PCRE2_INCLUDE_DIRS}) + list(APPEND PCRE2_LINK_LIBS ${PCRE2_LINK_LIBS} ${PCRE2_LIBRARIES}) + else() + list(APPEND PCRE2_LINK_LIBS pcre2-8) + endif() +endif() diff --git a/cmake/Extensions/PCSC.cmake b/cmake/Extensions/PCSC.cmake new file mode 100644 index 0000000..fa94f75 --- /dev/null +++ b/cmake/Extensions/PCSC.cmake @@ -0,0 +1,13 @@ +# PCSC +option(FUN_WITH_PCSC "Enable PCSC (pcsclite) support" OFF) +set(PCSC_LINK_LIBS "") +set(PCSC_INCLUDE_DIRS "") +if(FUN_WITH_PCSC) + add_definitions(-DFUN_WITH_PCSC) + if(APPLE) + list(APPEND PCSC_LINK_LIBS "-framework PCSC") + else() + list(APPEND PCSC_INCLUDE_DIRS "/usr/include/PCSC") + list(APPEND PCSC_LINK_LIBS pcsclite) + endif() +endif() diff --git a/cmake/Extensions/REPL.cmake b/cmake/Extensions/REPL.cmake new file mode 100644 index 0000000..9ac79f4 --- /dev/null +++ b/cmake/Extensions/REPL.cmake @@ -0,0 +1,2 @@ +# REPL toggle (affects sources of the fun executable) +option(FUN_WITH_REPL "Enable interactive REPL in the fun CLI" OFF) diff --git a/cmake/Extensions/SQLITE.cmake b/cmake/Extensions/SQLITE.cmake new file mode 100644 index 0000000..a92475e --- /dev/null +++ b/cmake/Extensions/SQLITE.cmake @@ -0,0 +1,22 @@ +# SQLite +option(FUN_WITH_SQLITE "Enable SQLite (sqlite3) support" OFF) +set(SQLITE3_INCLUDE_DIRS "") +set(SQLITE3_LINK_LIBS "") +if(FUN_WITH_SQLITE) + add_definitions(-DFUN_WITH_SQLITE) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(SQLITE3 QUIET sqlite3) + endif() + if(SQLITE3_FOUND) + list(APPEND SQLITE3_INCLUDE_DIRS ${SQLITE3_INCLUDE_DIRS} ${SQLITE3_INCLUDE_DIRS}) + list(APPEND SQLITE3_LINK_LIBS ${SQLITE3_LINK_LIBS} ${SQLITE3_LIBRARIES}) + else() + find_library(SQLITE3_LIB sqlite3) + if(SQLITE3_LIB) + list(APPEND SQLITE3_LINK_LIBS ${SQLITE3_LIB}) + else() + message(FATAL_ERROR "sqlite3 not found. Install sqlite3 (dev headers) or disable FUN_WITH_SQLITE.") + endif() + endif() +endif() diff --git a/cmake/Extensions/TCLTK.cmake b/cmake/Extensions/TCLTK.cmake new file mode 100644 index 0000000..471c4f6 --- /dev/null +++ b/cmake/Extensions/TCLTK.cmake @@ -0,0 +1,46 @@ +# Tcl/Tk GUI support +option(FUN_WITH_TCLTK "Enable Tcl/Tk GUI support" OFF) +set(TCL_INCLUDE_DIRS "") +set(TCL_LINK_LIBS "") +if(FUN_WITH_TCLTK) + add_definitions(-DFUN_WITH_TCLTK) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(TCL QUIET tcl) + pkg_check_modules(TK QUIET tk) + endif() + if(TCL_FOUND OR TK_FOUND) + if(TCL_FOUND) + list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIRS} ${TCL_INCLUDE_DIRS}) + list(APPEND TCL_LINK_LIBS ${TCL_LINK_LIBS} ${TCL_LIBRARIES}) + endif() + if(TK_FOUND) + list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIRS} ${TK_INCLUDE_DIRS}) + list(APPEND TCL_LINK_LIBS ${TCL_LINK_LIBS} ${TK_LIBRARIES}) + endif() + else() + find_path(TCL_INCLUDE_DIR tcl.h PATH_SUFFIXES tcl8.7 tcl8.6 include) + find_library(TCL_LIB NAMES tcl8.7 tcl8.6 tcl) + find_library(TK_LIB NAMES tk8.7 tk8.6 tk) + if(TCL_INCLUDE_DIR) + list(APPEND TCL_INCLUDE_DIRS ${TCL_INCLUDE_DIR}) + endif() + if(TCL_LIB) + list(APPEND TCL_LINK_LIBS ${TCL_LIB}) + endif() + if(TK_LIB) + list(APPEND TCL_LINK_LIBS ${TK_LIB}) + endif() + if(APPLE) + # macOS frameworks often unnecessary when using Homebrew tcl/tk + elseif(WIN32) + list(APPEND TCL_LINK_LIBS user32 gdi32 comctl32) + else() + find_package(X11 QUIET) + if(X11_FOUND) + list(APPEND TCL_INCLUDE_DIRS ${X11_INCLUDE_DIR}) + list(APPEND TCL_LINK_LIBS ${X11_LIBRARIES}) + endif() + endif() + endif() +endif() diff --git a/cmake/Extensions/XML2.cmake b/cmake/Extensions/XML2.cmake new file mode 100644 index 0000000..01858b1 --- /dev/null +++ b/cmake/Extensions/XML2.cmake @@ -0,0 +1,25 @@ +# XML (libxml2) +option(FUN_WITH_XML2 "Enable XML (libxml2) support" OFF) +set(LIBXML2_INCLUDE_DIRS "") +set(LIBXML2_LINK_LIBS "") +if(FUN_WITH_XML2) + add_definitions(-DFUN_WITH_XML2) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBXML2 QUIET libxml-2.0) + endif() + if(LIBXML2_FOUND) + list(APPEND LIBXML2_INCLUDE_DIRS ${LIBXML2_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIRS}) + list(APPEND LIBXML2_LINK_LIBS ${LIBXML2_LINK_LIBS} ${LIBXML2_LIBRARIES}) + else() + find_library(LIBXML2_LIB NAMES xml2 libxml2) + if(LIBXML2_LIB) + list(APPEND LIBXML2_LINK_LIBS ${LIBXML2_LIB}) + if(EXISTS "/usr/include/libxml2") + list(APPEND LIBXML2_INCLUDE_DIRS "/usr/include/libxml2") + endif() + else() + message(FATAL_ERROR "libxml2 not found. Install libxml2 (dev headers) or disable FUN_WITH_XML2.") + endif() + endif() +endif() diff --git a/cmake/Options.cmake b/cmake/Options.cmake new file mode 100644 index 0000000..92a5fb8 --- /dev/null +++ b/cmake/Options.cmake @@ -0,0 +1,35 @@ +# Global options and defaults + +# Convenience: default path to bundled stdlib +set(FUN_LIB "${CMAKE_SOURCE_DIR}/lib" CACHE PATH "Path to bundled Fun stdlib") + +# Optional: build using musl libc instead of glibc on Linux (OFF by default) +option(FUN_USE_MUSL "Use musl libc toolchain when available (Linux only)" OFF) + +# Platform-specific configuration (DEFAULT_LIB_DIR defaults, libc defines/toolchain tweaks) +if(WIN32) + include(${CMAKE_SOURCE_DIR}/cmake/Platform/Windows.cmake) +elseif(APPLE) + include(${CMAKE_SOURCE_DIR}/cmake/Platform/macOS.cmake) +elseif(UNIX) + include(${CMAKE_SOURCE_DIR}/cmake/Platform/Linux.cmake) +endif() + +# If user provided DEFAULT_LIB_DIR, keep it; otherwise it should be set by platform include above +set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) + +# Ensure trailing slash for DEFAULT_LIB_DIR if it is defined +if(DEFINED DEFAULT_LIB_DIR AND NOT DEFAULT_LIB_DIR STREQUAL "") + if(NOT DEFAULT_LIB_DIR MATCHES "/$") + set(DEFAULT_LIB_DIR "${DEFAULT_LIB_DIR}/") + endif() +endif() + +# Static linking is deprecated/unsupported: always build dynamically +option(FUN_LINK_STATIC "(Deprecated) Attempt to link statically — ignored; dynamic linking is enforced" OFF) +if(FUN_LINK_STATIC) + message(WARNING "FUN_LINK_STATIC is deprecated and ignored. Building with dynamic linking.") +endif() + +# Debug option to enable verbose parser/VM logging +option(FUN_DEBUG "Enable extra debug logging in Fun" OFF) diff --git a/cmake/Platform/Linux.cmake b/cmake/Platform/Linux.cmake new file mode 100644 index 0000000..0c4147e --- /dev/null +++ b/cmake/Platform/Linux.cmake @@ -0,0 +1,24 @@ +# Linux-specific configuration for Fun + +# Default library directory (only set if not provided by user) +if(NOT DEFINED DEFAULT_LIB_DIR OR DEFAULT_LIB_DIR STREQUAL "") + set(DEFAULT_LIB_DIR "/usr/share/fun/lib" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) +endif() + +# Handle musl toolchain selection when requested +if(FUN_USE_MUSL) + find_program(_FUN_MUSL_CC NAMES musl-gcc musl-clang) + if(_FUN_MUSL_CC) + message(STATUS "FUN_USE_MUSL=ON: using musl toolchain: ${_FUN_MUSL_CC}") + set(CMAKE_C_COMPILER "${_FUN_MUSL_CC}" CACHE FILEPATH "C compiler" FORCE) + set(FUN_LIBC "musl" CACHE STRING "Selected C library") + add_compile_definitions(FUN_LIBC_MUSL) + else() + message(WARNING "FUN_USE_MUSL=ON but no musl toolchain (musl-gcc or musl-clang) found. Falling back to default compiler (likely glibc).") + set(FUN_LIBC "glibc" CACHE STRING "Selected C library") + add_compile_definitions(FUN_LIBC_GLIBC) + endif() +else() + set(FUN_LIBC "glibc" CACHE STRING "Selected C library") + add_compile_definitions(FUN_LIBC_GLIBC) +endif() diff --git a/cmake/Platform/Windows.cmake b/cmake/Platform/Windows.cmake new file mode 100644 index 0000000..2be11ec --- /dev/null +++ b/cmake/Platform/Windows.cmake @@ -0,0 +1,10 @@ +# Windows-specific configuration for Fun + +# Default library directory (only set if not provided by user) +if(NOT DEFINED DEFAULT_LIB_DIR OR DEFAULT_LIB_DIR STREQUAL "") + set(DEFAULT_LIB_DIR "C:/Users/Public/fun/lib" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) +endif() + +# libc macro (non-musl path) +set(FUN_LIBC "glibc" CACHE STRING "Selected C library") +add_compile_definitions(FUN_LIBC_GLIBC) diff --git a/cmake/Platform/macOS.cmake b/cmake/Platform/macOS.cmake new file mode 100644 index 0000000..eabaf85 --- /dev/null +++ b/cmake/Platform/macOS.cmake @@ -0,0 +1,10 @@ +# macOS-specific configuration for Fun + +# Default library directory (only set if not provided by user) +if(NOT DEFINED DEFAULT_LIB_DIR OR DEFAULT_LIB_DIR STREQUAL "") + set(DEFAULT_LIB_DIR "/Library/Application Support/fun/lib" CACHE PATH "Default library directory for Fun stdlib (override with -DDEFAULT_LIB_DIR=...)" FORCE) +endif() + +# libc macro (non-musl path) +set(FUN_LIBC "glibc" CACHE STRING "Selected C library") +add_compile_definitions(FUN_LIBC_GLIBC) diff --git a/cmake/Targets.cmake b/cmake/Targets.cmake new file mode 100644 index 0000000..c1db251 --- /dev/null +++ b/cmake/Targets.cmake @@ -0,0 +1,130 @@ +# Targets for Fun project (moved from src/CMakeLists.txt) + +# Core VM/library sources +add_library(fun_core + ${CMAKE_SOURCE_DIR}/src/bytecode.c + ${CMAKE_SOURCE_DIR}/src/parser.c + ${CMAKE_SOURCE_DIR}/src/value.c + ${CMAKE_SOURCE_DIR}/src/vm.c +) + +# NOTE: Do not compile src/vm/*/*.c as independent units. +# Those files are meant to be included by src/vm.c inside a big switch. + +target_include_directories(fun_core PUBLIC + ${CMAKE_SOURCE_DIR}/src +) + +# Apply options to core +if(FUN_DEBUG) + message(STATUS "FUN_DEBUG enabled: building with verbose debug logging") + target_compile_definitions(fun_core PUBLIC FUN_VERSION="${PROJECT_VERSION}") + target_compile_definitions(fun_core PUBLIC FUN_DEBUG=1) +endif() + +# Provide default stdlib directory and version to the runtime +target_compile_definitions(fun_core PUBLIC FUN_VERSION="${PROJECT_VERSION}") +target_compile_definitions(fun_core PUBLIC DEFAULT_LIB_DIR="${DEFAULT_LIB_DIR}") + +# Apply extension include/link variables discovered in cmake/Extensions +foreach(var_pair + PCSC + JSONC + PCRE2 + CURL + SQLITE3 + INIPARSER + LIBSQL + LIBXML2 + TCL + NOTCURSES) + if(${var_pair}_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${${var_pair}_INCLUDE_DIRS}) + endif() + if(${var_pair}_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${${var_pair}_LINK_LIBS}) + endif() +endforeach() + +# Ensure feature compile definitions are applied to fun_core so +# conditional code blocks (#ifdef FUN_WITH_*) are compiled as expected. + +# Feature-specific sources and compile definitions +if(FUN_WITH_PCSC) + target_compile_definitions(fun_core PUBLIC FUN_WITH_PCSC=1) +endif() + +if(FUN_WITH_JSON) + target_compile_definitions(fun_core PUBLIC FUN_WITH_JSON=1) +endif() + +if(FUN_WITH_INI) + target_compile_definitions(fun_core PUBLIC FUN_WITH_INI=1) +endif() + +if(FUN_WITH_XML2) + target_compile_definitions(fun_core PUBLIC FUN_WITH_XML2=1) +endif() + +if(FUN_WITH_LIBSQL) + target_compile_definitions(fun_core PUBLIC FUN_WITH_LIBSQL=1) +endif() + +if(FUN_WITH_PCRE2) + target_compile_definitions(fun_core PUBLIC FUN_WITH_PCRE2=1) +endif() + +if(FUN_WITH_CURL) + target_compile_definitions(fun_core PUBLIC FUN_WITH_CURL=1) +endif() + +if(FUN_WITH_NOTCURSES) + target_compile_definitions(fun_core PUBLIC FUN_WITH_NOTCURSES=1) +endif() + +if(FUN_WITH_SQLITE) + target_compile_definitions(fun_core PUBLIC FUN_WITH_SQLITE=1) +endif() + +if(FUN_WITH_TCLTK) + target_compile_definitions(fun_core PUBLIC FUN_WITH_TCLTK=1) +endif() + +# Special case for libxml2 system include path if enabled +if(FUN_WITH_XML2) + if(EXISTS "/usr/include/libxml2") + target_include_directories(fun_core PRIVATE "/usr/include/libxml2") + endif() +endif() + +# Link threads if available on UNIX +if(Threads_FOUND) + target_link_libraries(fun_core PUBLIC Threads::Threads) +endif() + +# Link libm for C99 math functions if available +find_library(M_LIB m) +if(M_LIB) + target_link_libraries(fun_core PUBLIC ${M_LIB}) +endif() + +# Executable: fun (CLI) +add_executable(fun + ${CMAKE_SOURCE_DIR}/src/fun.c +) +if(FUN_WITH_REPL) + target_compile_definitions(fun PRIVATE FUN_WITH_REPL=1) + target_sources(fun PRIVATE ${CMAKE_SOURCE_DIR}/src/repl.c) +endif() +target_link_libraries(fun PRIVATE fun_core) + +# Internal test programs +add_executable(fun_test + ${CMAKE_SOURCE_DIR}/src/fun_test.c) +target_link_libraries(fun_test PRIVATE fun_core) + +add_executable(test_opcodes + ${CMAKE_SOURCE_DIR}/src/test_opcodes.c) +target_link_libraries(test_opcodes PRIVATE fun_core) + +# Static linking flags are intentionally not applied (deprecated behavior) diff --git a/examples/base64_demo.fun b/examples/base64_demo.fun new file mode 100755 index 0000000..d80f87b --- /dev/null +++ b/examples/base64_demo.fun @@ -0,0 +1,51 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-13 + */ + +/* + * Base64 usage demo (RFC 4648, standard alphabet) + * + * Run without installing: + * FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/base64_demo.fun + */ + +#include + +print("=== Base64 demo ===") + +// Bytes for the ASCII string "Hello" +bytes = [0x48, 0x65, 0x6c, 0x6c, 0x6f] + +// Encode bytes -> Base64 string +b64 = b64_encode_bytes(bytes) +print("b64(Hello) = " + b64) // Expected: SGVsbG8= + +// Decode Base64 -> bytes array (uncomment to try) +// decoded = b64_decode_to_bytes(b64) +// // Build a comma-separated list of byte values as strings +// parts = [] +// for i in range(0, len(decoded)) +// push(parts, to_string(decoded[i])) +// print("bytes = " + join(parts, ",")) // Expected: 72,101,108,108,111 + +// Another example: padding with '=' +bytes2 = [0x46, 0x75, 0x6e] // "Fun" +print("b64(Fun) = " + b64_encode_bytes(bytes2)) // Expected: RnVu + +print("=== done ===") + +/* Expected output: +=== Base64 demo === +b64(Hello) = SGVsbG8= +b64(Fun) = RnVu +=== done === +*/ diff --git a/examples/notcurses_clock.fun b/examples/broken/notcurses_clock.fun similarity index 100% rename from examples/notcurses_clock.fun rename to examples/broken/notcurses_clock.fun diff --git a/examples/notcurses_typing.fun b/examples/broken/notcurses_typing.fun similarity index 100% rename from examples/notcurses_typing.fun rename to examples/broken/notcurses_typing.fun diff --git a/examples/broken/ripemd160.fun b/examples/broken/ripemd160.fun new file mode 100755 index 0000000..c23fc7b --- /dev/null +++ b/examples/broken/ripemd160.fun @@ -0,0 +1,314 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-02 + */ + +// lib/crypt/ripemd160.fun +// Pure Fun implementation of RIPEMD-160 operating on hex-string or ASCII inputs. +// +// Public API (class): +// rip = RIPEMD160() +// rip.ripemd160_hex(hexStr) -> digest hex string (lowercase) +// rip.ripemd160_str("abc") -> digest hex string of ASCII bytes + +#include + +class RIPEMD160() + // 32-bit helpers (match style from md5.fun) + fun u32(this, x) + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + + fun add32(this, a, b) + return this.u32(a + b) + + fun add32_5(this, a, b, c, d, e) + return this.u32(this.u32(this.u32(this.u32(a + b) + c) + d) + e) + + fun rol32(this, x, s) + return rol(this.u32(x), s) + + fun and32(this, a, b) + return band(this.u32(a), this.u32(b)) + + fun or32(this, a, b) + return bor(this.u32(a), this.u32(b)) + + fun xor32(this, a, b) + return bxor(this.u32(a), this.u32(b)) + + fun not32(this, x) + return bnot(this.u32(x)) + + // hex helpers (same as md5/sha files) + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + hi = this.hex_val(substr(hh, 0, 1)) + lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + arr = [] + i = 0 + n = len(hex) + while i + 1 < n + b = this.byte_from_hex_pair(substr(hex, i, 2)) + push(arr, b) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + d = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] + hi = n / 16 + lo = n % 16 + parts = [d[hi], d[lo]] + return join(parts, "") + + fun bytes_to_hex(this, arr) + i = 0 + out = [] + while i < len(arr) + push(out, this.two_hex(arr[i])) + i = i + 1 + return join(out, "") + + // Padding (RIPEMD-160): like MD4/MD5 — append 0x80, zeros to 56 mod 64, then 64-bit length in little-endian + fun pad_bytes(this, bytes) + L = len(bytes) + out = [] + i = 0 + while i < L + push(out, bytes[i]) + i = i + 1 + push(out, 128) + while (len(out) % 64) != 56 + push(out, 0) + len_bits = L * 8 + j = 0 + while j < 8 + b = (len_bits / pow(2, 8 * j)) % 256 + push(out, b) + j = j + 1 + return out + + fun word32_le(this, b0, b1, b2, b3) + return this.u32(b0 + b1 * 256 + b2 * 65536 + b3 * 16777216) + + // RIPEMD-160 boolean functions + fun f1(this, x, y, z) + return this.xor32(this.xor32(x, y), z) + + fun f2(this, x, y, z) + return this.or32(this.and32(x, y), this.and32(this.not32(x), z)) + + fun f3(this, x, y, z) + return this.xor32(this.or32(x, this.not32(y)), z) + + fun f4(this, x, y, z) + return this.or32(this.and32(x, z), this.and32(y, this.not32(z))) + + fun f5(this, x, y, z) + return this.xor32(x, this.or32(y, this.not32(z))) + + // Process a 512-bit block + fun process_block(this, H, block) + // message words X[16] (little-endian) + X = [] + i = 0 + while i < 16 + j = i * 4 + push(X, this.word32_le(block[j], block[j+1], block[j+2], block[j+3])) + i = i + 1 + + // R and S (left line) + R = [ + 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15, + 7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8, + 3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12, + 1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2, + 4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13 + ] + S = [ + 11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8, + 7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12, + 11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5, + 11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12, + 9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6 + ] + + // R' and S' (right line) + Rp = [ + 5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12, + 6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2, + 15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13, + 8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14, + 12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11 + ] + Sp = [ + 8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6, + 9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11, + 9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5, + 15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8, + 8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11 + ] + + // Constants + KL = [0, 1518500249, 1859775393, 2400959708, 2840853838] // 0x00, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E + KR = [1352829926, 1548603684, 1836072691, 2053994217, 0] // 0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000 + + al = H[0]; bl = H[1]; cl = H[2]; dl = H[3]; el = H[4] + ar = H[0]; br = H[1]; cr = H[2]; dr = H[3]; er = H[4] + + j = 0 + while j < 80 + sL = S[j] + sR = Sp[j] + + // select function and constant per round for left lane + if (j < 16) + fL = this.f1(bl, cl, dl) + kL = KL[0] + else if (j < 32) + fL = this.f2(bl, cl, dl) + kL = KL[1] + else if (j < 48) + fL = this.f3(bl, cl, dl) + kL = KL[2] + else if (j < 64) + fL = this.f4(bl, cl, dl) + kL = KL[3] + else + fL = this.f5(bl, cl, dl) + kL = KL[4] + + // right lane + if (j < 16) + fR = this.f5(br, cr, dr) + kR = KR[0] + else if (j < 32) + fR = this.f4(br, cr, dr) + kR = KR[1] + else if (j < 48) + fR = this.f3(br, cr, dr) + kR = KR[2] + else if (j < 64) + fR = this.f2(br, cr, dr) + kR = KR[3] + else + fR = this.f1(br, cr, dr) + kR = KR[4] + + // left step + tl = this.add32_5(al, fL, X[R[j]], kL, 0) + tl = this.rol32(tl, sL) + tl = this.add32(tl, el) + al = el + el = dl + dl = this.rol32(cl, 10) + cl = bl + bl = tl + + // right step + tr = this.add32_5(ar, fR, X[Rp[j]], kR, 0) + tr = this.rol32(tr, sR) + tr = this.add32(tr, er) + ar = er + er = dr + dr = this.rol32(cr, 10) + cr = br + br = tr + + j = j + 1 + + // combine using originals + h0 = H[0]; h1 = H[1]; h2 = H[2]; h3 = H[3]; h4 = H[4] + tt = this.add32(h0, this.add32(bl, cr)) + H[0] = this.add32(h1, this.add32(cl, dr)) + H[1] = this.add32(h2, this.add32(dl, er)) + H[2] = this.add32(h3, this.add32(el, ar)) + H[3] = this.add32(h4, this.add32(al, br)) + H[4] = tt + return H + + fun ripemd160_bytes(this, bytes) + // initialize h0..h4 + H = [1732584193, 4023233417, 2562383102, 271733878, 3285377520] + data = this.pad_bytes(bytes) + off = 0 + N = len(data) + while off < N + block = [] + i = 0 + while i < 64 + push(block, data[off + i]) + i = i + 1 + H = this.process_block(H, block) + off = off + 64 + + // output as little-endian of h0..h4 (20 bytes) + out = [] + j = 0 + while j < 5 + v = H[j] + push(out, v % 256) + push(out, (v / 256) % 256) + push(out, (v / 65536) % 256) + push(out, (v / 16777216) % 256) + j = j + 1 + return out + + fun ripemd160_hex(this, hexStr) + bytes = this.from_hex(hexStr) + digest = this.ripemd160_bytes(bytes) + return this.bytes_to_hex(digest) + + fun ripemd160_str(this, str) + bytes = string_to_bytes_ascii(str) + digest = this.ripemd160_bytes(bytes) + return this.bytes_to_hex(digest) diff --git a/examples/ripemd160_debug.fun b/examples/broken/ripemd160_debug.fun similarity index 100% rename from examples/ripemd160_debug.fun rename to examples/broken/ripemd160_debug.fun diff --git a/examples/ripemd160_demo.fun b/examples/broken/ripemd160_demo.fun similarity index 100% rename from examples/ripemd160_demo.fun rename to examples/broken/ripemd160_demo.fun diff --git a/examples/ripemd160_example.fun b/examples/broken/ripemd160_example.fun similarity index 100% rename from examples/ripemd160_example.fun rename to examples/broken/ripemd160_example.fun diff --git a/examples/ripemd160_test.fun b/examples/broken/ripemd160_test.fun similarity index 100% rename from examples/ripemd160_test.fun rename to examples/broken/ripemd160_test.fun diff --git a/examples/crc32_example.fun b/examples/crypto/crc32_example.fun similarity index 100% rename from examples/crc32_example.fun rename to examples/crypto/crc32_example.fun diff --git a/examples/crc32c_example.fun b/examples/crypto/crc32c_example.fun similarity index 100% rename from examples/crc32c_example.fun rename to examples/crypto/crc32c_example.fun diff --git a/examples/md5_demo.fun b/examples/crypto/md5_demo.fun similarity index 100% rename from examples/md5_demo.fun rename to examples/crypto/md5_demo.fun diff --git a/examples/sha1_demo.fun b/examples/crypto/sha1_demo.fun similarity index 100% rename from examples/sha1_demo.fun rename to examples/crypto/sha1_demo.fun diff --git a/examples/sha256_demo.fun b/examples/crypto/sha256_demo.fun similarity index 100% rename from examples/sha256_demo.fun rename to examples/crypto/sha256_demo.fun diff --git a/examples/sha256_str_demo.fun b/examples/crypto/sha256_str_demo.fun similarity index 100% rename from examples/sha256_str_demo.fun rename to examples/crypto/sha256_str_demo.fun diff --git a/examples/sha384_example.fun b/examples/crypto/sha384_example.fun similarity index 100% rename from examples/sha384_example.fun rename to examples/crypto/sha384_example.fun diff --git a/examples/sha512_demo.fun b/examples/crypto/sha512_demo.fun similarity index 100% rename from examples/sha512_demo.fun rename to examples/crypto/sha512_demo.fun diff --git a/examples/sha512_str_demo.fun b/examples/crypto/sha512_str_demo.fun similarity index 100% rename from examples/sha512_str_demo.fun rename to examples/crypto/sha512_str_demo.fun diff --git a/examples/data/complex.ini b/examples/data/complex.ini index 2d73052..8a379f1 100644 --- a/examples/data/complex.ini +++ b/examples/data/complex.ini @@ -1,29 +1,37 @@ + [auth] -user = "hanez" -token = "abcd1234" +user = "hanez" +token = "abcd1234" + [app] -name = "FunApp" -version = "1.2.3" -debug = "1" +name = "FunApp" +version = "1.2.3" +debug = "1" + [database] -host = "localhost" -port = "5432" -user = "fun" -pass = "secret" -pool_size = "8" -timeout = "2.5" +host = "localhost" +port = "5432" +user = "fun" +pass = "secret" +pool_size = "8" +timeout = "2.5" + [network] -ssl = "yes" -retries = "3" -base_url = "https://api.example.com" +ssl = "yes" +retries = "3" +base_url = "https://api.example.com" + [features] -feature_x = "on" -feature_y = "off" +feature_x = "on" +feature_y = "off" + [paths] -data_dir = "./data" -log_file = "./logs/app.log" +data_dir = "./data" +log_file = "./logs/app.log" + + diff --git a/examples/curl_download.fun b/examples/extra/curl_download.fun similarity index 100% rename from examples/curl_download.fun rename to examples/extra/curl_download.fun diff --git a/examples/curl_get_json.fun b/examples/extra/curl_get_json.fun similarity index 100% rename from examples/curl_get_json.fun rename to examples/extra/curl_get_json.fun diff --git a/examples/curl_post.fun b/examples/extra/curl_post.fun similarity index 56% rename from examples/curl_post.fun rename to examples/extra/curl_post.fun index 802ea56..d5a70bb 100755 --- a/examples/curl_post.fun +++ b/examples/extra/curl_post.fun @@ -31,29 +31,27 @@ if obj != nil if obj != nil print("Origin: " + to_string(obj["origin"])) -/* Possible output: -Response: { - "args": {}, - "data": "", - "files": {}, - "form": { - "lang": "fun", - "name": "Fun" - }, - "headers": { - "Accept": "*/*", - "Content-Length": "17", - "Content-Type": "application/x-www-form-urlencoded", - "Host": "httpbin.org", - "X-Amzn-Trace-Id": "Root=1-6944834b-74f499e251c322713e7dd9a8" - }, - "json": null, - "origin": "5.252.226.107", - "url": "https://httpbin.org/post" -} - -Content-Type: application/x-www-form-urlencoded -Host: httpbin.org -Origin: 5.252.226.107 -*/ - +// Possible output: +// Response: { +// "args": {}, +// "data": "", +// "files": {}, +// "form": { +// "lang": "fun", +// "name": "Fun" +// }, +// "headers": { +// "Accept": "*/*", +// "Content-Length": "17", +// "Content-Type": "application/x-www-form-urlencoded", +// "Host": "httpbin.org", +// "X-Amzn-Trace-Id": "Root=1-6944834b-74f499e251c322713e7dd9a8" +// }, +// "json": nil, +// "origin": "5.252.226.107", +// "url": "https://httpbin.org/post" +// } +// +// Content-Type: application/x-www-form-urlencoded +// Host: httpbin.org +// Origin: 5.252.226.107 diff --git a/examples/ini_class_demo.fun b/examples/extra/ini_class_demo.fun similarity index 100% rename from examples/ini_class_demo.fun rename to examples/extra/ini_class_demo.fun diff --git a/examples/ini_complex.fun b/examples/extra/ini_complex.fun similarity index 100% rename from examples/ini_complex.fun rename to examples/extra/ini_complex.fun diff --git a/examples/ini_demo.fun b/examples/extra/ini_demo.fun similarity index 100% rename from examples/ini_demo.fun rename to examples/extra/ini_demo.fun diff --git a/examples/ini_diag.fun b/examples/extra/ini_diag.fun similarity index 100% rename from examples/ini_diag.fun rename to examples/extra/ini_diag.fun diff --git a/examples/ini_subsections.fun b/examples/extra/ini_subsections.fun similarity index 100% rename from examples/ini_subsections.fun rename to examples/extra/ini_subsections.fun diff --git a/examples/json_showcase.fun b/examples/extra/json_showcase.fun similarity index 100% rename from examples/json_showcase.fun rename to examples/extra/json_showcase.fun diff --git a/examples/libsql_example.fun b/examples/extra/libsql_example.fun similarity index 100% rename from examples/libsql_example.fun rename to examples/extra/libsql_example.fun diff --git a/examples/notcurses_hello.fun b/examples/extra/notcurses_hello.fun similarity index 100% rename from examples/notcurses_hello.fun rename to examples/extra/notcurses_hello.fun diff --git a/examples/pcre2_opcodes.fun b/examples/extra/pcre2_opcodes.fun similarity index 100% rename from examples/pcre2_opcodes.fun rename to examples/extra/pcre2_opcodes.fun diff --git a/examples/pcre2_showcase.fun b/examples/extra/pcre2_showcase.fun similarity index 100% rename from examples/pcre2_showcase.fun rename to examples/extra/pcre2_showcase.fun diff --git a/examples/pcsc_example.fun b/examples/extra/pcsc_example.fun similarity index 100% rename from examples/pcsc_example.fun rename to examples/extra/pcsc_example.fun diff --git a/examples/sqlite_example.fun b/examples/extra/sqlite_example.fun similarity index 100% rename from examples/sqlite_example.fun rename to examples/extra/sqlite_example.fun diff --git a/examples/tk_file_manager.fun b/examples/extra/tk_file_manager.fun similarity index 100% rename from examples/tk_file_manager.fun rename to examples/extra/tk_file_manager.fun diff --git a/examples/tk_hello.fun b/examples/extra/tk_hello.fun similarity index 100% rename from examples/tk_hello.fun rename to examples/extra/tk_hello.fun diff --git a/examples/tk_testing.fun b/examples/extra/tk_testing.fun similarity index 100% rename from examples/tk_testing.fun rename to examples/extra/tk_testing.fun diff --git a/examples/xml_access_catalog.fun b/examples/extra/xml_access_catalog.fun similarity index 100% rename from examples/xml_access_catalog.fun rename to examples/extra/xml_access_catalog.fun diff --git a/examples/xml_access_employees.fun b/examples/extra/xml_access_employees.fun similarity index 100% rename from examples/xml_access_employees.fun rename to examples/extra/xml_access_employees.fun diff --git a/examples/xml_access_ns.fun b/examples/extra/xml_access_ns.fun similarity index 100% rename from examples/xml_access_ns.fun rename to examples/extra/xml_access_ns.fun diff --git a/examples/xml_class_example.fun b/examples/extra/xml_class_example.fun similarity index 100% rename from examples/xml_class_example.fun rename to examples/extra/xml_class_example.fun diff --git a/examples/xml_minimal.fun b/examples/extra/xml_minimal.fun similarity index 100% rename from examples/xml_minimal.fun rename to examples/extra/xml_minimal.fun diff --git a/examples/console_prompt.fun b/examples/interactive/console_prompt.fun similarity index 100% rename from examples/console_prompt.fun rename to examples/interactive/console_prompt.fun diff --git a/examples/input_example.fun b/examples/interactive/input_example.fun similarity index 100% rename from examples/input_example.fun rename to examples/interactive/input_example.fun diff --git a/examples/input_hidden_example.fun b/examples/interactive/input_hidden_example.fun similarity index 100% rename from examples/input_hidden_example.fun rename to examples/interactive/input_hidden_example.fun diff --git a/examples/input_hidden_pam_auth.fun b/examples/interactive/input_hidden_pam_auth.fun similarity index 91% rename from examples/input_hidden_pam_auth.fun rename to examples/interactive/input_hidden_pam_auth.fun index 56d5fb7..e1138dd 100755 --- a/examples/input_hidden_pam_auth.fun +++ b/examples/interactive/input_hidden_pam_auth.fun @@ -29,6 +29,9 @@ if (res["code"] == 0) else print("Login failed!") +// Propagate the child process exit status to the Fun program's exit code +exit res["code"] + /* Possible output: Username: hanez Password: diff --git a/examples/math_ceil.fun b/examples/math/math_ceil.fun similarity index 100% rename from examples/math_ceil.fun rename to examples/math/math_ceil.fun diff --git a/examples/math_cos.fun b/examples/math/math_cos.fun similarity index 100% rename from examples/math_cos.fun rename to examples/math/math_cos.fun diff --git a/examples/math_exp_log.fun b/examples/math/math_exp_log.fun similarity index 100% rename from examples/math_exp_log.fun rename to examples/math/math_exp_log.fun diff --git a/examples/math_floor.fun b/examples/math/math_floor.fun similarity index 100% rename from examples/math_floor.fun rename to examples/math/math_floor.fun diff --git a/examples/math_fmin_fmax.fun b/examples/math/math_fmin_fmax.fun similarity index 100% rename from examples/math_fmin_fmax.fun rename to examples/math/math_fmin_fmax.fun diff --git a/examples/math_gcd_lcm.fun b/examples/math/math_gcd_lcm.fun similarity index 100% rename from examples/math_gcd_lcm.fun rename to examples/math/math_gcd_lcm.fun diff --git a/examples/math_isqrt.fun b/examples/math/math_isqrt.fun similarity index 100% rename from examples/math_isqrt.fun rename to examples/math/math_isqrt.fun diff --git a/examples/math_round.fun b/examples/math/math_round.fun similarity index 100% rename from examples/math_round.fun rename to examples/math/math_round.fun diff --git a/examples/math_sign.fun b/examples/math/math_sign.fun similarity index 100% rename from examples/math_sign.fun rename to examples/math/math_sign.fun diff --git a/examples/math_sin.fun b/examples/math/math_sin.fun similarity index 100% rename from examples/math_sin.fun rename to examples/math/math_sin.fun diff --git a/examples/math_sqrt.fun b/examples/math/math_sqrt.fun similarity index 100% rename from examples/math_sqrt.fun rename to examples/math/math_sqrt.fun diff --git a/examples/math_tan.fun b/examples/math/math_tan.fun similarity index 100% rename from examples/math_tan.fun rename to examples/math/math_tan.fun diff --git a/examples/math_trunc.fun b/examples/math/math_trunc.fun similarity index 100% rename from examples/math_trunc.fun rename to examples/math/math_trunc.fun diff --git a/make b/make index a6a0205..b0d587a 100755 --- a/make +++ b/make @@ -30,9 +30,7 @@ if [ "$target" = "all" ]; then -DFUN_WITH_JSON=ON \ -DFUN_WITH_TCLTK=ON \ -DFUN_WITH_INI=ON \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_USE_MUSL=OFF \ - -DFUN_DEBUG=OFF \ + -DFUN_WITH_NOTCURSES=ON \ && cmake --build build --target fun elif [ "$target" = "all_debug" ]; then rm -rf build \ @@ -47,14 +45,12 @@ elif [ "$target" = "all_debug" ]; then -DFUN_WITH_JSON=ON \ -DFUN_WITH_TCLTK=ON \ -DFUN_WITH_INI=ON \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_USE_MUSL=OFF \ + -DFUN_WITH_NOTCURSES=ON \ -DFUN_DEBUG=ON \ && cmake --build build --target fun elif [ "$target" = "alpine" ]; then rm -rf build \ && cmake -S . -B build \ - -DFUN_WITH_PCSC=OFF \ -DFUN_WITH_REPL=ON \ -DFUN_WITH_LIBSQL=ON \ -DFUN_WITH_SQLITE=ON \ @@ -62,96 +58,35 @@ elif [ "$target" = "alpine" ]; then -DFUN_WITH_PCRE2=ON \ -DFUN_WITH_XML2=ON \ -DFUN_WITH_JSON=ON \ - -DFUN_WITH_TCLTK=OFF \ -DFUN_WITH_INI=ON \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_DEBUG=OFF \ && cmake --build build --target fun elif [ "$target" = "debug" ]; then rm -rf build \ && cmake -S . -B build \ - -DFUN_WITH_PCSC=OFF \ -DFUN_WITH_REPL=ON \ - -DFUN_WITH_LIBSQL=OFF \ - -DFUN_WITH_SQLITE=OFF \ - -DFUN_WITH_CURL=OFF \ - -DFUN_WITH_PCRE2=OFF \ - -DFUN_WITH_XML2=OFF \ - -DFUN_WITH_JSON=OFF \ - -DFUN_WITH_TCLTK=OFF \ - -DFUN_WITH_INI=OFF \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_USE_MUSL=OFF \ -DFUN_DEBUG=ON \ && cmake --build build --target fun elif [ "$target" = "freebsd" ]; then rm -rf build \ && cmake -S . -B build \ - -DFUN_WITH_PCSC=OFF \ -DFUN_WITH_REPL=ON \ - -DFUN_WITH_LIBSQL=OFF \ - -DFUN_WITH_SQLITE=OFF \ - -DFUN_WITH_CURL=OFF \ - -DFUN_WITH_PCRE2=OFF \ - -DFUN_WITH_XML2=OFF \ - -DFUN_WITH_JSON=OFF \ - -DFUN_WITH_TCLTK=OFF \ - -DFUN_WITH_INI=OFF \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_DEBUG=OFF \ && cmake --build build --target fun elif [ "$target" = "install" ]; then sudo cmake --build build --target install elif [ "$target" = "minimal" ]; then rm -rf build \ && cmake -S . -B build \ - -DFUN_WITH_PCSC=OFF \ - -DFUN_WITH_REPL=OFF \ - -DFUN_WITH_LIBSQL=OFF \ - -DFUN_WITH_SQLITE=OFF \ - -DFUN_WITH_CURL=OFF \ - -DFUN_WITH_PCRE2=OFF \ - -DFUN_WITH_XML2=OFF \ - -DFUN_WITH_JSON=OFF \ - -DFUN_WITH_TCLTK=OFF \ - -DFUN_WITH_INI=OFF \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_USE_MUSL=OFF \ - -DFUN_DEBUG=OFF \ && cmake --build build --target fun elif [ "$target" = "musl" ]; then rm -rf build \ && cmake -S . -B build \ - -DFUN_WITH_PCSC=OFF \ -DFUN_WITH_REPL=ON \ - -DFUN_WITH_LIBSQL=OFF \ - -DFUN_WITH_SQLITE=OFF \ - -DFUN_WITH_CURL=OFF \ - -DFUN_WITH_PCRE2=OFF \ - -DFUN_WITH_XML2=OFF \ - -DFUN_WITH_JSON=OFF \ - -DFUN_WITH_TCLTK=OFF \ - -DFUN_WITH_INI=OFF \ - -DFUN_LINK_STATIC=OFF \ -DFUN_USE_MUSL=ON \ - -DFUN_DEBUG=OFF \ && cmake --build build --target fun elif [ "$target" = "repl" ]; then rm -rf build \ && cmake -S . -B build \ - -DFUN_WITH_PCSC=OFF \ -DFUN_WITH_REPL=ON \ - -DFUN_WITH_LIBSQL=OFF \ - -DFUN_WITH_SQLITE=OFF \ - -DFUN_WITH_CURL=OFF \ - -DFUN_WITH_PCRE2=OFF \ - -DFUN_WITH_XML2=OFF \ - -DFUN_WITH_JSON=OFF \ - -DFUN_WITH_TCLTK=OFF \ - -DFUN_WITH_INI=OFF \ - -DFUN_LINK_STATIC=OFF \ - -DFUN_USE_MUSL=OFF \ - -DFUN_DEBUG=OFF \ && cmake --build build --target fun else echo "Build target $target not found... aborting!"; @@ -166,3 +101,4 @@ else echo " - musl"; echo " - repl"; fi + diff --git a/scripts/check_op_includes.py b/scripts/check_op_includes.py index 8cd2d3c..1a8c4da 100755 --- a/scripts/check_op_includes.py +++ b/scripts/check_op_includes.py @@ -19,7 +19,8 @@ BYTECODE = ROOT / "src" / "bytecode.h" VM_C = ROOT / "src" / "vm.c" OP_RE = re.compile(r'\bOP_([A-Z0-9_]+)\b') -INCLUDE_RE = re.compile(r'#include\s+"vm/(?:[a-z0-9_]+/)?([a-z0-9_]+)\.c"') +# Capture optional subdirectory and the basename separately +INCLUDE_RE = re.compile(r'#include\s+"vm/(?:([a-z0-9_]+)/)?([a-z0-9_]+)\.c"') def read_text(path: Path) -> str: try: @@ -42,14 +43,14 @@ def parse_opcodes_from_bytecode(text: str) -> set[str]: return ops def parse_includes_from_vm(text: str) -> set[str]: - incs = set(m.group(1) for m in INCLUDE_RE.finditer(text)) + pairs = [(m.group(1) or "", m.group(2)) for m in INCLUDE_RE.finditer(text)] # Drop support includes that are not opcode handlers - support_includes = {"thread_common"} - incs = {name for name in incs if name not in support_includes} + support_includes = {"thread_common", "stubs", "handles"} + pairs = [(d, n) for (d, n) in pairs if n not in support_includes] - # Map include base names to OP_* tokens. - overrides = { + # Base-name overrides (dir-agnostic) for a few special cases + base_overrides = { # core "nop": "NOP", "halt": "HALT", "load_const": "LOAD_CONST", "load_local": "LOAD_LOCAL", "store_local": "STORE_LOCAL", @@ -66,7 +67,7 @@ def parse_includes_from_vm(text: str) -> set[str]: "make_array": "MAKE_ARRAY", "len": "LEN", "index_get": "INDEX_GET", "index_set": "INDEX_SET", "push": "PUSH", "apop": "APOP", "set": "SET", "insert": "INSERT", "remove": "REMOVE", - "slice": "SLICE", + "slice": "SLICE", "clear": "CLEAR", "contains": "CONTAINS", "index_of": "INDEX_OF", # conversions and type/meta "to_number": "TO_NUMBER", "to_string": "TO_STRING", "cast": "CAST", "typeof": "TYPEOF", "uclamp": "UCLAMP", "sclamp": "SCLAMP", @@ -89,9 +90,50 @@ def parse_includes_from_vm(text: str) -> set[str]: "thread_spawn": "THREAD_SPAWN", "thread_join": "THREAD_JOIN", } - tokens = set() - for inc in incs: - tokens.add(overrides.get(inc, inc.upper())) + def map_token(d: str, n: str) -> str: + # Directory-specific namespaces + if d == "curl": + return f"CURL_{n.upper()}" + if d == "ini": + if n in {"load", "free", "get_string", "get_int", "get_double", "get_bool", "set", "unset", "save"}: + return f"INI_{n.upper()}" + if d == "json": + if n in {"parse", "stringify", "from_file", "to_file"}: + return f"JSON_{n.upper()}" + if d == "xml": + if n in {"parse", "root", "name", "text"}: + return f"XML_{n.upper()}" + if d == "sqlite": + if n in {"open", "close", "exec", "query"}: + return f"SQLITE_{n.upper()}" + if d == "libsql": + if n in {"open", "close", "exec", "query"}: + return f"LIBSQL_{n.upper()}" + if d == "pcsc": + if n in {"establish", "release", "list_readers", "connect", "disconnect", "transmit"}: + return f"PCSC_{n.upper()}" + if d == "pcre2": + if n in {"test", "match", "findall"}: + return f"PCRE2_{n.upper()}" + if d == "tk": + # wm_title already uses underscore + return f"TK_{n.upper()}" + if d == "notcurses": + return f"NC_{n.upper()}" + if d == "os": + # special cases in OS + if n == "list_dir": + return "OS_LIST_DIR" + if n.startswith("socket_"): + rest = n.split("socket_", 1)[1].upper() + return f"SOCK_{rest}" + if n.startswith("serial_"): + # serial_open/config/send/recv/close + return f"SERIAL_{n.split('serial_',1)[1].upper()}" + # Fallbacks: known base overrides, else uppercase name + return base_overrides.get(n, n.upper()) + + tokens = set(map(lambda p: map_token(p[0], p[1]), pairs)) return tokens def main() -> int: diff --git a/scripts/run_examples.sh b/scripts/run_examples.sh index 1690cd7..183fdba 100755 --- a/scripts/run_examples.sh +++ b/scripts/run_examples.sh @@ -11,8 +11,9 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)" EX_DIR="$ROOT/examples" -# Allow override via first arg or FUN_BIN env -BIN="${1:-${FUN_BIN:-}}" +# Optional overrides via FUN_BIN env. First positional arg may now be an examples subdirectory. +BIN="${FUN_BIN:-}" +SUBDIR="${1:-}" pick_bin() { local cands=( @@ -36,11 +37,17 @@ pick_bin() { return 1 } +# If user passed a subdir that actually looks like an executable path, treat it as BIN for backward compatibility +if [[ -n "$SUBDIR" && -x "$SUBDIR" && ! -d "$EX_DIR/$SUBDIR" ]]; then + BIN="$SUBDIR" + SUBDIR="" +fi + if [[ -z "${BIN}" ]]; then if ! BIN="$(pick_bin)"; then echo "error: fun binary not found. Try building it (e.g., via CMake) or pass it explicitly:" >&2 - echo " scripts/run_examples.sh /path/to/fun" >&2 - echo "or set FUN_BIN=/path/to/fun" >&2 + echo " FUN_BIN=/path/to/fun scripts/run_examples.sh [examples-subdir]" >&2 + echo "or: scripts/run_examples.sh /path/to/fun" >&2 exit 2 fi fi @@ -55,15 +62,26 @@ if [[ -z "${FUN_LIB_DIR:-}" ]]; then export FUN_LIB_DIR="$ROOT/lib" fi -# Ensure error bucket exists -mkdir -p "$EX_DIR/error" +# Do not auto-move failing examples; keep user's workspace unchanged + +# Determine target examples directory (optionally restricted to a subdir) +TARGET_DIR="$EX_DIR" +if [[ -n "${SUBDIR}" ]]; then + if [[ -d "$EX_DIR/$SUBDIR" ]]; then + TARGET_DIR="$EX_DIR/$SUBDIR" + else + echo "error: examples subdirectory not found: $SUBDIR" >&2 + echo " expected at: $EX_DIR/$SUBDIR" >&2 + exit 2 + fi +fi shopt -s nullglob -files=("$EX_DIR"/*.fun) +files=("$TARGET_DIR"/*.fun) shopt -u nullglob if (( ${#files[@]} == 0 )); then - echo "No .fun example files found in $EX_DIR" + echo "No .fun example files found in $TARGET_DIR" exit 0 fi @@ -80,13 +98,7 @@ for f in "${files[@]}"; do echo "=== Running: ${f#$ROOT/} ===" if ! "$BIN" "$f"; then echo "FAILED: ${f#$ROOT/}" - dest="$EX_DIR/error/$base_name" - # Move the failing example to the error folder - if mv -f "$f" "$dest"; then - echo "Moved to: examples/error/$base_name" - else - echo "warning: failed to move $base_name to examples/error/" >&2 - fi + # Intentionally do not move files on failure; leave control to the user rc=1 fi done diff --git a/spec/v0.3.md b/spec/v0.3.md new file mode 100644 index 0000000..b91d582 --- /dev/null +++ b/spec/v0.3.md @@ -0,0 +1,484 @@ +# Fun Language Specification v0.3 + +This document describes Fun (Fun Uses Nothing) as of version 0.3. It supersedes v0.2 by formalizing classes/objects, inheritance, namespaced includes, richer collections and builtins, concurrency primitives, networking and OS integration, as reflected by the examples. + +--- + +## 1) Overview and Goals + +- Readable: strict, indentation-based syntax (2 spaces), no semicolons. +- Safe: explicit types, no implicit numeric coercions, bounds-checked operations, controlled side effects. +- Hackable: pragmatic stdlib, process I/O, sockets, threads. + +What’s new in v0.3 (high level): +- Classes and objects with methods, constructors, and inheritance (`extends`). +- Dot-call sugar for method calls, and explicit `this` in method definitions. +- Namespaced `#include ... as alias` for module imports. +- Maps (dictionaries) with literals and helpers. +- Control-flow additions: `break` and `continue`. +- Threads: `thread_spawn`, `thread_join` and `sleep`. +- Expanded system and network APIs (TCP/Unix sockets, serial, env, timers). +- Bitwise helpers (`band`, `bor`, `bxor`, `bnot`, `shl`). +- Exception syntax (`try/catch/finally`) is defined; runtime throwing/handling may be partial. + +--- + +## 2) Lexical Structure + +- Case-sensitive identifiers: letters, digits, `_`; must not start with a digit. +- Comments: + - Single-line: `// comment` + - Multi-line: `/* ... */` +- Whitespace and newlines: + - Indentation is exactly 2 spaces; tabs are forbidden. + - Newline terminates statements; no semicolons. + +Reserved keywords (cannot be redefined): +- `if`, `else`, `for`, `while`, `break`, `continue` +- `fun`, `return` +- `class`, `extends` +- `global`, `private` +- `true`, `false` +- `try`, `catch`, `finally` + +Notes: +- `#include` is a directive, not an expression; `as` is part of the include alias syntax (see Modules & Includes). + +--- + +## 3) Types + +Scalar types: +- `number`: 64-bit signed integer. +- `float`: 64-bit IEEE-754 floating point. +- `string` +- `boolean`: `true` / `false` (in conditionals `0` and `1` are accepted where noted). +- `byte`: 8-bit value (see conversions and overflow rules). + +Fixed-width integers (signed/unsigned): +- `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64` + +Aggregate types: +- `array` and typed arrays: `array` +- `map` (dictionary / associative array) +- `object` (instances of `class`) + +Examples: +```fun +number n = 42 +float pi = 3.14159 +string s = 'He said: "Fun!"' +boolean ok = true +byte b = 0x41 + +array nums = [1, 2, 3] +array mixed = [1, "two", true, [3, 4]] + +m = { "a": 1, "b": 2 } // map +``` + +Dynamic typing escape hatch (discouraged; use when interacting with unknown data): +```fun +dynamic string x = 42 // allowed by spec; runtime performs dynamic checks +x = "Now I'm a string" +``` + +--- + +## 4) Variables and Scope + +- `global` variables are visible program-wide. +- `private` variables are file-local (module private). +- Rebinding a global or shadowing a name is a compile-time error. + +```fun +global string message = "Hello" +private number count = 42 +number local = 23 +``` + +--- + +## 5) Operators and Builtins + +Arithmetic: `+`, `-`, `*`, `/`, `%` + +Comparison: `==`, `!=`, `>`, `<`, `>=`, `<=` + +Boolean: `&&`, `||`, `!` + +Assignment: `=` + +Bitwise helpers (functions): +- `band(a, b)`, `bor(a, b)`, `bxor(a, b)`, `bnot(a)`, `shl(a, n)` + +```fun +print(bxor(0x80000000, 0x00000001)) // 2147483649 +print(shl(0x80, 24)) // 2147483648 +``` + +Collection helpers (selected): +- Arrays: `push(arr, v)`, `join(arr, sep)`, `map(arr, f)`, `filter(arr, pred)`, `reduce(arr, init, f)` +- Maps: `has(m, key)`, `keys(m)`, `values(m)` + +Type/convert helpers (selected): +- `typeof(x) -> string` +- `to_string(x)` and numeric casts (see examples for `cast_demo.fun` and `conversions_showcase.fun`) + +--- + +## 6) Control Flow + +If/Else: +```fun +if (x != y) + print(x) +else if (a == b || h != i) + print(a + b) +else + if (k < 1 && l > 1) + print("Buh!") +``` + +While: +```fun +number i = 0 +while i < 10 + if i % 2 == 0 + i = i + 1 + continue + if i > 5 + break + i = i + 1 +``` + +For: +- Range iteration: `for i in range(start, end)` +- Array iteration: `for x in arr` +- Map iteration: `for k in keys(m)` then `m[k]` + +```fun +for i in range(0, 5) + print(i) + +for x in [1, 2, 3] + print(x) +``` + +Loop control: +- `break` exits the innermost loop. +- `continue` skips to next iteration of the current loop. + +Try/Catch/Finally (syntax defined; runtime throwing may be incomplete): +```fun +try + // protected section + risky() +catch err + print("caught error:") + print(err) +finally + print("cleanup") +``` + +--- + +## 7) Functions + +Built-in/runtime functions (selected): +- Basic: `print(x)`, `range(a, b)`, `sleep(ms)` +- Processes: `exec(cmd) -> string`, `system(cmd) -> number` +- Async processes: `nexec(cmd) -> pid/object`, `nsystem(cmd) -> number`, `nspawn(cmd) -> pid`, `wait(pid)`, `read(pid)`, `kill(pid)` +- Threads: `thread_spawn(fn, argOrArgs) -> thread_id`, `thread_join(thread_id) -> any` + +User-defined functions: +```fun +fun add(a, b) + return a + b + +fun divide(a, b) + if b == 0 + return 0, "division by zero" + return a / b, "" +``` + +Higher-order helper (example pattern): +```fun +fun call(f, arg) + return f(arg) +``` + +Multiple return values are supported syntactically; use tuple-like unpacking via arrays as needed in user code patterns. + +--- + +## 8) Classes and Objects + +Definition: +```fun +class Name(/* optional ctor params with types */) + // field defaults + field1 = 0 + field2 = "" + + // method: first parameter must be `this` + fun method(this, arg1, arg2) + // ... + return 0 +``` + +Constructors: +- Default constructor maps the class header parameters to fields of the same names. +- Optional explicit constructor hook: define `fun _construct(this, ...params...)` to customize initialization. It is invoked automatically on instantiation. + +Fields and methods: +- Fields are created/initialized with simple assignments in the class body. +- Methods are functions declared inside the class; the first parameter must be `this`. +- Private members: any field or method whose name starts with `_` is considered private to the class. Accessing them from outside should raise an access error. + +Instantiation and method calls: +```fun +p = Point(10, -2) +print(p.x) // field access via `.` or indexing +print(p["x"]) // map-like field access is supported + +// Dot-call sugar: p.method(a, b) is equivalent to method(p, a, b) +print(p.toString()) +``` + +Method references: +```fun +move_fn = p["move"] +move_fn(p, 3, 5) // call with explicit `this` +``` + +Inheritance: +```fun +class Parent(number start) + value = 0 + fun _construct(this, s) + this.value = s + + fun describe(this) + return "Parent(value=" + to_string(this.value) + ")" + +class Child(number start) extends Parent + bonus = 5 + fun _construct(this, s) + // runs after parent fields merged + this.value = this.value + this.bonus + fun describe(this) + return "Child(value=" + to_string(this.value) + ", bonus=" + to_string(this.bonus) + ")" +``` + +`typeof` on classes and instances: +- `typeof(Point) == "Class"` +- `typeof(p) == "Point(10, -2)"` (implementation-specific descriptive form) + +--- + +## 9) Modules and Includes + +Include sources: +- System/stdlib: angle brackets search the Fun library path (e.g., `FUN_LIB_DIR`). + ```fun + #include + ``` +- Local file: quoted, relative to the current working directory or file. + ```fun + #include "./utils/file.fun" + ``` +- Absolute path is supported. + +Namespaced includes: +- Use `as` to bind a module into a namespace alias. +```fun +#include as m +#include "examples/namespaced_mod.fun" as mod + +print(m.add(2, 3)) +g = mod.Greeter("Hi") +g.say("World") +``` + +Aliased access uses `alias.symbol` or `alias.ClassName`. + +Global/private at file scope control symbol exports from a module. + +--- + +## 10) Collections + +Arrays: +- Literals with `[ ... ]`; may be heterogeneous unless `array` is declared. +- Helpers: `push`, `join`, `map`, `filter`, `reduce`, iteration via `for x in arr`. + +Maps: +- Literal: `{ key: value, ... }` +- Indexing: `m["a"]`, assignment `m["c"] = 5` +- Introspection: `has(m, key)`, `keys(m)`, `values(m)` +- Iterate keys/values using arrays returned by `keys`/`values`. + +--- + +## 11) Concurrency (Threads) + +- `thread_spawn(fn, args)` starts `fn` in a new thread. `args` may be a single value or an array for multiple arguments. +- `thread_join(id)` waits for the thread and returns its result. +- `sleep(ms)` suspends current thread. + +Example: +```fun +fun square(n) + sleep(100) + return n * n + +ids = [] +for x in [1, 2, 3] + push(ids, thread_spawn(square, x)) + +results = [] +for id in ids + push(results, thread_join(id)) + +print(results) +``` + +--- + +## 12) System, Files, Environment, and Networking + +Processes: +- Blocking: `exec(cmd) -> string` (stdout), `system(cmd) -> number` (exit code) +- Non-blocking: `nexec`, `nspawn`, `nsystem`, with `wait(pid)`, `read(pid)`, `kill(pid)` helpers + +Environment and CLI: +- `env(NAME) -> string` to read env variables (see `os_env.fun`) +- `argv() -> array` from ``; also `FUN_ARGC`/`FUN_ARGS` environment interoperability in examples + +Files: +- Basic file I/O helpers exist in the stdlib; see `file_io.fun`, `file_print_for_file_line_by_line.fun` + +Time: +- Date/time and timers (see `datetime_basic.fun`, `datetime_extended.fun`, `datetime_timer.fun`) + +Random: +- `random` helpers (see `random_demo.fun`, `random_number_example.fun`) + +Regex: +- Regex operations via stdlib (see `regex_demo.fun`, `regex_procedural.fun`) + +Networking: +- TCP client helpers: `tcp_connect(host, port) -> fd`, `sock_send(fd, data)`, `sock_recv(fd, nbytes)`, `sock_close(fd)` +- Unix domain sockets (see `unix_socket_echo.fun`) + +Serial: +- Serial port helpers (see `serial_demo.fun`) + +Progress/UI: +- Helper functions to render CLI progress (see `progress.fun`, `progress_inline.fun`) + +Note: function names may live in stdlib modules; import accordingly (system-dependent availability). + +--- + +## 13) Error Handling and Type Safety + +- No implicit type coercion between numeric types; explicit casts or constructors are required. +- Overflow/underflow on fixed-width types is an error. +- Accessing undefined variables or re-defining globals is a compile-time error. +- Shadowing internal/runtime functions is forbidden. +- Exception syntax `try/catch/finally` is standardized in v0.3; throwing and catching at runtime may be partially implemented depending on the feature (see examples like `byte_overflow_try_catch.fun` and notes within). + +--- + +## 14) Examples (from the repository) + +Hello: +```fun +print("Hello, World!") +``` + +Namespaced includes: +```fun +#include as m +print(m.add(2, 3)) +``` + +Classes: +```fun +class Counter + value = 0 + fun inc(this) + this.value = this.value + 1 + return this.value + +c = Counter() +print(c.inc()) +``` + +Inheritance: +```fun +class Parent(number start) + value = 0 + fun _construct(this, s) + this.value = s + +class Child(number start) extends Parent + bonus = 5 + fun _construct(this, s) + this.value = this.value + this.bonus +``` + +Threads: +```fun +tid = thread_spawn(add3, [10, 20, 30]) +print(thread_join(tid)) // 60 +``` + +TCP GET: +```fun +fd = tcp_connect("example.org", 80) +req = "GET / HTTP/1.0\r\nHost: example.org\r\n\r\n" +sent = sock_send(fd, req) +print(sock_recv(fd, 8192)) +sock_close(fd) +``` + +--- + +## 15) Versioning and Compatibility + +- v0.3 keeps v0.2 syntax intact and adds new features. Where runtime support is evolving (exceptions, some stdlib facets), the syntax is stable and forward-compatible. +- Examples are authoritative for idioms and available helpers; consult `examples/` and `lib/` modules. + +--- + +## 16) Appendix: Notation and Conventions + +- Use backticks for code identifiers in prose (`fun`, `class`, `extends`, etc.). +- All code blocks are in `fun` pseudolanguage. +- Indentation is always 2 spaces; tabs will cause errors. + +--- + +## Changelog (from v0.2 to v0.3) + +- Added: `class`, methods with explicit `this`, default and custom constructors via `_construct`. +- Added: inheritance with `extends`. +- Added: private members by leading underscore naming convention. +- Added: dot-call sugar `obj.method(a, b)` ≡ `method(obj, a, b)`. +- Added: namespaced includes: `#include as alias`, `#include "path" as alias`. +- Added: `map` type with literals `{ key: value }` and helpers. +- Added: loop control `break`, `continue`. +- Added: threads (`thread_spawn`, `thread_join`), sleep. +- Added: socket helpers (TCP/Unix), serial devices, CLI argv, env helpers, timers. +- Added: bitwise helper functions: `band`, `bor`, `bxor`, `bnot`, `shl`. +- Added: exception syntax `try/catch/finally` (runtime handling is evolving). + +--- + +## How to use this file + +- Save this content as `./spec/v0.3.md` in the repository. +- Keep `examples/` in sync with this spec. When adding a new feature, provide an example and update the spec accordingly. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..6aacbbd --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,5 @@ +# This file is intentionally minimal. Targets have been moved to cmake/Targets.cmake +# to keep the source tree clean. Keeping this include preserves compatibility if +# someone adds this directory as a subdirectory in the future. + +include(${CMAKE_SOURCE_DIR}/cmake/Targets.cmake) diff --git a/src/repl.c b/src/repl.c index 215a38f..81ebcef 100644 --- a/src/repl.c +++ b/src/repl.c @@ -158,16 +158,28 @@ static void rl_word_right(const char *out, size_t len, size_t *pos) { while (*pos < len && out[*pos] == ' ') (*pos)++; } -/* Expand file path for ":load " completion in-place; returns 1 if buffer changed (redraw) */ +/* Expand file path for path-taking REPL commands (e.g., :load, :run) in-place; returns 1 if buffer changed (redraw) */ static int complete_load_path(char *buf, size_t *len_io) { size_t len = *len_io; - if (len < 5) return 0; + if (len < 3) return 0; /* minimally ":x" */ const char *p = buf; while (*p == ' ') p++; if (*p != ':') return 0; p++; - if (strncmp(p, "load", 4) != 0) return 0; - p += 4; + /* Parse command token (letters only) */ + const char *cmd_start = p; + while (*p && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z'))) p++; + size_t cmd_len = (size_t)(p - cmd_start); + if (cmd_len == 0) return 0; + /* Accept both full and short aliases for :load and :run */ + int is_supported = 0; + if ((cmd_len == 4 && strncmp(cmd_start, "load", 4) == 0) || + (cmd_len == 2 && strncmp(cmd_start, "lo", 2) == 0) || + (cmd_len == 3 && strncmp(cmd_start, "run", 3) == 0) || + (cmd_len == 2 && strncmp(cmd_start, "ru", 2) == 0)) { + is_supported = 1; + } + if (!is_supported) return 0; while (*p == ' ' || *p == '\t') p++; size_t arg_off = (size_t)(p - buf); if (arg_off > len) return 0; @@ -840,40 +852,41 @@ static int buffer_looks_incomplete(const char *buf) { static void show_repl_help(void) { printf("Commands:\n"); - printf(" :help Show this help\n"); - printf(" :quit | :q | :exit Exit the REPL\n"); - printf(" :reset Reset VM state (clears globals)\n"); - printf(" :dump | :globals Dump current globals\n"); - printf(" :globals [pattern] Dump globals filtering by value substring\n"); - printf(" :vars [pattern] Alias for :globals\n"); - printf(" :clear Clear current input buffer\n"); - printf(" :print Show current buffer\n"); - printf(" :run Execute current buffer immediately\n"); - printf(" :profile Execute buffer and show timing + instruction count\n"); - printf(" :save Save current buffer to file\n"); - printf(" :load Load file into buffer (does not run)\n"); - printf(" :paste [run] Enter paste mode; end with a single '.' line (optional 'run')\n"); - 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(" :frame N Select frame N for :locals/:list/:disas (default: top)\n"); - printf(" :list [±K] Show K lines of source around current frame line (default 5)\n"); - printf(" :disas [±N] Disassemble around current frame ip (default 5)\n"); - printf(" :disasm WHAT [off [len]] [to ] Hexdump VM memory region to screen or file\n"); + printf(" :help | :h Show this help\n"); + printf(" :quit | :q | :exit Exit the REPL\n"); + printf(" :reset | :re Reset VM state (clears globals)\n"); + printf(" :dump | :du | :globals | :gl Dump current globals\n"); + printf(" :globals [pattern] Dump globals filtering by value substring\n"); + printf(" :vars | :v [pattern] Alias for :globals\n"); + printf(" :clear | :cl Clear current input buffer\n"); + printf(" :print | :pr Show current buffer\n"); + printf(" :run | :ru [file] Execute current buffer or the given file immediately\n"); + printf(" :profile | :pf Execute buffer and show timing + instruction count\n"); + printf(" :save | :sa Save current buffer to file\n"); + printf(" :load | :lo Load file into buffer (does not run)\n"); + printf(" :paste | :pa [run] Enter paste mode; end with a single '.' line (optional 'run')\n"); + printf(" :history | :hi [N] Show last N lines of history (default 50)\n"); + printf(" :time | :ti on|off|toggle Toggle/enable/disable timing\n"); + printf(" :env | :en [NAME[=VALUE]] Get or set environment variable\n"); + printf(" :backtrace | :bt | :ba Show backtrace of VM frames (most recent first)\n"); + printf(" :frame | :fr N Select frame N for :locals/:list/:disasm (default: top)\n"); + printf(" :list | :li [±K] Show K lines of source around current frame line (default 5)\n"); + printf(" :disasm | :di [±N] Disassemble around current frame ip (default 5)\n"); + printf(" :mdump | :md WHAT [offset [len]] [raw] [to ] Dump VM memory region\n"); printf(" WHAT = code | stack | globals | consts\n"); - printf(" :stack [N] Show top N (default all) stack values\n"); - printf(" :top Show the top of the VM stack\n"); - printf(" :locals [FRAME] Show locals of frame (default: selected frame)\n"); - printf(" :printv WHAT Print value: local[i] | stack[i] | global[i]\n"); - printf(" :break [file:]line Set a breakpoint (default file = current frame file)\n"); - printf(" :info breaks List breakpoints\n"); - printf(" :delete ID Delete breakpoint by ID\n"); - printf(" :clear breaks Remove all breakpoints\n"); - printf(" :cont Continue execution (exit REPL if in debug stop)\n"); - printf(" :step Step one instruction\n"); - printf(" :next Step over (current frame)\n"); - printf(" :finish Run until the current frame returns\n"); + printf(" 'raw' writes binary bytes instead of a formatted hexdump\n"); + printf(" :stack | :st [N] Show top N (default all) stack values\n"); + printf(" :top | :to Show the top of the VM stack\n"); + printf(" :locals | :lc [FRAME] Show locals of frame (default: selected frame)\n"); + printf(" :printv | :pv WHAT Print value: local[i] | stack[i] | global[i]\n"); + printf(" :break | :br [file:]line Set a breakpoint (default file = current frame file)\n"); + printf(" :info | :in breaks List breakpoints\n"); + printf(" :delete | :de ID Delete breakpoint by ID\n"); + printf(" :clear breaks | :cb Remove all breakpoints\n"); + printf(" :cont | :co Continue execution (exit REPL if in debug stop)\n"); + printf(" :step | :sp Step one instruction\n"); + printf(" :next | :ne Step over (current frame)\n"); + printf(" :finish | :fi Run until the current frame returns\n"); } static char *read_entire_file(const char *path, size_t *out_len) { @@ -900,6 +913,15 @@ static int write_entire_file(const char *path, const char *data, size_t len) { return n == len; } +/* ---------- REPL command matching helper ---------- */ +static int cmd_is_one_of(const char *cmd, const char *const names[]) { + if (!cmd || !*cmd) return 0; + for (int i = 0; names[i] != NULL; ++i) { + if (strcmp(cmd, names[i]) == 0) return 1; + } + return 0; +} + /* ---------- Hexdump helper ---------- */ static void hexdump_to(FILE *out, const unsigned char *data, size_t len, size_t base_off) { if (!out || !data || len == 0) return; @@ -1071,19 +1093,19 @@ int fun_run_repl(VM *vm) { char arg[2048] = {0}; sscanf(line, ":%63s %2047[^\n]", cmd, arg); - if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "q") == 0 || strcmp(cmd, "exit") == 0) { + if (cmd_is_one_of(cmd, (const char*[]){"quit","q","qu","exit", NULL})) { break; - } else if (strcmp(cmd, "help") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"help","h", NULL})) { show_repl_help(); continue; - } else if (strcmp(cmd, "reset") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"reset","re", NULL})) { vm_reset(vm); printf("VM state reset.\n"); continue; - } else if (strcmp(cmd, "dump") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"dump","du", NULL})) { vm_dump_globals(vm); continue; - } else if (strcmp(cmd, "globals") == 0 || strcmp(cmd, "vars") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"globals","vars","gl","v","va", NULL})) { const char *pattern = lstrip(arg); int filtered = (pattern && *pattern); printf("=== globals%s%s ===\n", filtered ? " matching '" : "", filtered ? pattern : ""); @@ -1098,11 +1120,11 @@ int fun_run_repl(VM *vm) { } printf("===============\n"); continue; - } else if (strcmp(cmd, "clear") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"clear","cl", NULL})) { buflen = 0; printf("(buffer cleared)\n"); continue; - } else if (strcmp(cmd, "print") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"print","pr", NULL})) { if (buflen == 0) printf("(buffer empty)\n"); else { if (buflen >= bufcap) { @@ -1114,28 +1136,46 @@ int fun_run_repl(VM *vm) { if (buflen > 0 && buffer[buflen-1] != '\n') printf("\n"); } continue; - } else if (strcmp(cmd, "run") == 0 || strcmp(cmd, "profile") == 0) { - if (buflen == 0) { - printf("(buffer empty)\n"); - continue; + } else if (cmd_is_one_of(cmd, (const char*[]){"run","ru","profile","pf", NULL})) { + int is_profile = cmd_is_one_of(cmd, (const char*[]){"profile","pf", NULL}); + // 'arg' is a fixed-size local array, so its address is always non-null. + // Only check whether it contains a non-empty string. + int from_file = (arg[0] != '\0'); + + const char *src = NULL; + char *filebuf = NULL; + if (from_file) { + size_t flen = 0; + filebuf = read_entire_file(arg, &flen); + if (!filebuf) { + printf("Failed to load '%s'\n", arg); + continue; + } + src = filebuf; + } else { + if (buflen == 0) { + printf("(buffer empty)\n"); + continue; + } + if (buflen + 1 > bufcap) { + buffer = (char*)realloc(buffer, buflen + 1); + bufcap = buflen + 1; + } + buffer[buflen] = '\0'; + src = buffer; } - if (buflen + 1 > bufcap) { - buffer = (char*)realloc(buffer, buflen + 1); - bufcap = buflen + 1; - } - buffer[buflen] = '\0'; clock_t t_parse0 = 0, t_parse1 = 0, t_run0 = 0, t_run1 = 0; - if (strcmp(cmd, "profile") == 0) t_parse0 = clock(); - Bytecode *bc = parse_string_to_bytecode(buffer); - if (strcmp(cmd, "profile") == 0) t_parse1 = clock(); + if (is_profile) t_parse0 = clock(); + Bytecode *bc = parse_string_to_bytecode(src); + if (is_profile) t_parse1 = clock(); if (bc) { - if (repl_timing || strcmp(cmd, "profile") == 0) t_run0 = clock(); + if (repl_timing || is_profile) t_run0 = clock(); vm_run(vm, bc); - if (repl_timing || strcmp(cmd, "profile") == 0) t_run1 = clock(); + if (repl_timing || is_profile) t_run1 = clock(); - if (strcmp(cmd, "profile") == 0) { + if (is_profile) { double ms_parse = (double)(t_parse1 - t_parse0) * 1000.0 / (double)CLOCKS_PER_SEC; double ms_run = (double)(t_run1 - t_run0) * 1000.0 / (double)CLOCKS_PER_SEC; printf("[profile] parse: %.2f ms, run: %.2f ms, total: %.2f ms, instr: %lld\n", @@ -1148,14 +1188,14 @@ int fun_run_repl(VM *vm) { vm_print_output(vm); vm_clear_output(vm); bytecode_free(bc); - append_history(hist, buffer); + if (!from_file) append_history(hist, buffer); } else { int line_no = 0, col_no = 0; char emsg[256]; if (parser_last_error(emsg, sizeof(emsg), &line_no, &col_no)) { printf("Parse error at %d:%d: %s\n", line_no, col_no, emsg); int cur_line = 1; - const char *p = buffer; + const char *p = src ? src : buffer; while (*p && cur_line < line_no) { if (*p == '\n') cur_line++; p++; @@ -1167,7 +1207,7 @@ int fun_run_repl(VM *vm) { for (int i = 1; i < col_no; ++i) putchar(' '); printf("^\n"); #ifdef FUN_DEBUG - if (hist) { + if (hist && !from_file) { fprintf(hist, "// ERROR %d:%d: %s\n", line_no, col_no, emsg); fflush(hist); } @@ -1175,16 +1215,20 @@ int fun_run_repl(VM *vm) { } else { printf("Parse error.\n"); #ifdef FUN_DEBUG - if (hist) { + if (hist && !from_file) { fprintf(hist, "// ERROR: parse error\n"); fflush(hist); } #endif } } - buflen = 0; + if (from_file) { + free(filebuf); + } else { + buflen = 0; /* keep behavior: clear buffer only when running current buffer */ + } continue; - } else if (strcmp(cmd, "save") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"save","sa", NULL})) { if (arg[0] == '\0') { printf("Usage: :save \n"); continue; } if (buflen == 0) { printf("(buffer empty)\n"); continue; } if (!write_entire_file(arg, buffer, buflen)) { @@ -1193,7 +1237,7 @@ int fun_run_repl(VM *vm) { printf("Saved %zu bytes to '%s'\n", buflen, arg); } continue; - } else if (strcmp(cmd, "load") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"load","lo", NULL})) { if (arg[0] == '\0') { printf("Usage: :load \n"); continue; } size_t flen = 0; char *filebuf = read_entire_file(arg, &flen); @@ -1212,7 +1256,7 @@ int fun_run_repl(VM *vm) { free(filebuf); printf("Loaded %zu bytes into buffer. Use :run or submit an empty line to execute.\n", buflen); continue; - } else if (strcmp(cmd, "paste") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"paste","pa", NULL})) { int run_after = 0; const char *opt = lstrip(arg); if (opt && (strcmp(opt, "run") == 0 || strcmp(opt, "exec") == 0)) run_after = 1; @@ -1275,13 +1319,13 @@ int fun_run_repl(VM *vm) { printf("(pasted %zu bytes into buffer)\n", buflen); } continue; - } else if (strcmp(cmd, "history") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"history","hi", NULL})) { int n = 50; if (arg[0] != '\0') n = atoi(arg); if (n <= 0) n = 50; print_last_n_lines(hist_path, n); continue; - } else if (strcmp(cmd, "time") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"time","ti", NULL})) { if (strcmp(lstrip(arg), "on") == 0) repl_timing = 1; else if (strcmp(lstrip(arg), "off") == 0) repl_timing = 0; else if (strcmp(lstrip(arg), "toggle") == 0) repl_timing = !repl_timing; @@ -1291,7 +1335,7 @@ int fun_run_repl(VM *vm) { } printf("Timing %s\n", repl_timing ? "enabled" : "disabled"); continue; - } else if (strcmp(cmd, "env") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"env","en", NULL})) { const char *spec = lstrip(arg); if (!spec || *spec == '\0') { env_show_usage(); @@ -1310,7 +1354,7 @@ int fun_run_repl(VM *vm) { env_set(name, val); } continue; - } else if (strcmp(cmd, "backtrace") == 0 || strcmp(cmd, "bt") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"backtrace","bt","ba", NULL})) { if (vm->fp < 0) { printf("(no frames)\n"); continue; } printf("Backtrace (most recent call first):\n"); for (int i = vm->fp; i >= 0; --i) { @@ -1321,7 +1365,7 @@ int fun_run_repl(VM *vm) { printf(" #%d %s at %s ip=%d line=%d\n", i, fname, sfile, ip, vm->current_line); } continue; - } else if (strcmp(cmd, "stack") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"stack","st", NULL})) { int n = -1; const char *p = lstrip(arg); if (p && *p) n = atoi(p); @@ -1336,7 +1380,7 @@ int fun_run_repl(VM *vm) { free(sv); } continue; - } else if (strcmp(cmd, "locals") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"locals","lc", NULL})) { int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp; const char *p = lstrip(arg); if (p && *p) { @@ -1358,7 +1402,7 @@ int fun_run_repl(VM *vm) { } if (!any) printf(" (no non-nil locals)\n"); continue; - } else if (strcmp(cmd, "frame") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"frame","fr", NULL})) { const char *p = lstrip(arg); if (!p || !*p) { printf("Usage: :frame N\n"); continue; } int v = atoi(p); @@ -1369,7 +1413,7 @@ int fun_run_repl(VM *vm) { const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : ""; printf("Selected frame #%d: %s (%s)\n", selected_frame, fname, sfile); continue; - } else if (strcmp(cmd, "list") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"list","li", NULL})) { int k = 5; const char *p = lstrip(arg); if (p && *p) k = atoi(p); @@ -1408,7 +1452,7 @@ int fun_run_repl(VM *vm) { } free(src); continue; - } else if (strcmp(cmd, "disas") == 0 || strcmp(cmd, "disassemble") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"disasm","disassemble","di", NULL})) { int n = 5; const char *p = lstrip(arg); if (p && *p) n = atoi(p); @@ -1439,26 +1483,29 @@ int fun_run_repl(VM *vm) { printf("%c %6d: %-14s %d\n", (i == curip ? '>' : ' '), i, opname, ins.operand); } continue; - } else if (strcmp(cmd, "disasm") == 0) { - /* Syntax: :disasm WHAT [off [len]] [to ] - WHAT: code | stack | globals | consts */ + } else if (cmd_is_one_of(cmd, (const char*[]){"mdump","md", NULL})) { + /* Syntax: :mdump WHAT [offset [len]] [raw] [to ] + WHAT: code | stack | globals | consts + 'raw' writes binary bytes instead of a formatted hexdump */ const char *p = lstrip(arg); if (!p || !*p) { - printf("Usage: :disasm WHAT [off [len]] [to ]\n"); + printf("Usage: :mdump WHAT [offset [len]] [raw] [to ]\n"); printf(" WHAT = code | stack | globals | consts\n"); + printf(" 'raw' writes binary bytes instead of a formatted hexdump\n"); continue; } char what[32]; int consumed = 0; if (sscanf(p, "%31s %n", what, &consumed) != 1) { - printf("Usage: :disasm WHAT [off [len]] [to ]\n"); + printf("Usage: :mdump WHAT [offset [len]] [raw] [to ]\n"); continue; } p += consumed; size_t off = 0; size_t len = (size_t)-1; /* default later to clamp */ + int want_raw = 0; /* output raw bytes instead of hexdump */ /* parse optional off */ while (*p == ' ' || *p == '\t') p++; @@ -1481,6 +1528,13 @@ int fun_run_repl(VM *vm) { } } + /* optional: 'raw' keyword */ + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "raw", 3) == 0 && (p[3] == '\0' || isspace((unsigned char)p[3]))) { + want_raw = 1; + p += 3; + } + /* optional: to */ while (*p == ' ' || *p == '\t') p++; int to_file = 0; @@ -1546,17 +1600,29 @@ int fun_run_repl(VM *vm) { } if (!to_file) { - printf("Hexdump %s: total=%zu, offset=%zu, len=%zu\n", what, total, off, len); - hexdump_to(stdout, base + off, len, off); + if (want_raw) { + /* Write raw bytes directly to stdout with no header */ + fwrite(base + off, 1, len, stdout); + fflush(stdout); + } else { + printf("Hexdump %s: total=%zu, offset=%zu, len=%zu\n", what, total, off, len); + hexdump_to(stdout, base + off, len, off); + } } else { FILE *fout = fopen(path, "wb"); if (!fout) { printf("Failed to open '%s' for writing\n", path); continue; } - hexdump_to(fout, base + off, len, off); - fclose(fout); - printf("Wrote hexdump (%zu bytes from %s) to %s\n", len, what, path); + if (want_raw) { + fwrite(base + off, 1, len, fout); + fclose(fout); + printf("Wrote raw bytes (%zu from %s) to %s\n", len, what, path); + } else { + hexdump_to(fout, base + off, len, off); + fclose(fout); + printf("Wrote hexdump (%zu bytes from %s) to %s\n", len, what, path); + } } continue; - } else if (strcmp(cmd, "printv") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"printv","pv", NULL})) { const char *spec = lstrip(arg); if (!spec || !*spec) { printf("Usage: :printv local[i] | stack[i] | global[i]\n"); continue; } int idx = -1; @@ -1581,13 +1647,13 @@ int fun_run_repl(VM *vm) { printf("Usage: :printv local[i] | stack[i] | global[i]\n"); } continue; - } else if (strcmp(cmd, "top") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"top","to", NULL})) { if (vm->sp < 0) { printf("(stack empty)\n"); continue; } char *sv = value_to_string_alloc(&vm->stack[vm->sp]); printf("%s\n", sv ? sv : "nil"); free(sv); continue; - } else if (strcmp(cmd, "break") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"break","br", NULL})) { const char *p = lstrip(arg); if (!p || !*p) { printf("Usage: :break [file:]line\n"); continue; } const char *colon = strchr(p, ':'); @@ -1613,7 +1679,7 @@ int fun_run_repl(VM *vm) { if (id >= 0) printf("Breakpoint %d set at %s:%d\n", id, filebuf, line); else printf("Failed to set breakpoint\n"); continue; - } else if (strcmp(cmd, "info") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"info","in", NULL})) { const char *what = lstrip(arg); if (what && strcmp(what, "breaks") == 0) { vm_debug_list_breakpoints(vm); @@ -1621,11 +1687,15 @@ int fun_run_repl(VM *vm) { printf("Usage: :info breaks\n"); } continue; - } else if (strcmp(cmd, "delete") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"delete","de", NULL})) { int id = atoi(lstrip(arg)); if (vm_debug_delete_breakpoint(vm, id)) printf("Deleted breakpoint %d\n", id); else printf("No such breakpoint %d\n", id); continue; + } else if (cmd_is_one_of(cmd, (const char*[]){"cb", NULL})) { + vm_debug_clear_breakpoints(vm); + printf("Cleared all breakpoints\n"); + continue; } else if (strcmp(cmd, "clear") == 0) { const char *what = lstrip(arg); if (what && strcmp(what, "breaks") == 0) { @@ -1635,22 +1705,22 @@ int fun_run_repl(VM *vm) { printf("Usage: :clear breaks\n"); } continue; - } else if (strcmp(cmd, "cont") == 0 || strcmp(cmd, "continue") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"cont","continue","co", NULL})) { vm_debug_request_continue(vm); printf("Continuing...\n"); if (vm->on_error_repl) return 0; /* exit REPL to continue execution */ continue; - } else if (strcmp(cmd, "step") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"step","sp", NULL})) { vm_debug_request_step(vm); printf("Stepping one instruction...\n"); if (vm->on_error_repl) return 0; continue; - } else if (strcmp(cmd, "next") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"next","ne", NULL})) { vm_debug_request_next(vm); printf("Stepping over...\n"); if (vm->on_error_repl) return 0; continue; - } else if (strcmp(cmd, "finish") == 0) { + } else if (cmd_is_one_of(cmd, (const char*[]){"finish","fi", NULL})) { vm_debug_request_finish(vm); printf("Running until current frame returns...\n"); if (vm->on_error_repl) return 0; diff --git a/src/vm.c b/src/vm.c index 61e347f..01864f8 100644 --- a/src/vm.c +++ b/src/vm.c @@ -23,6 +23,7 @@ #include #include #include +#include #ifdef __unix__ #include @@ -62,7 +63,8 @@ /* Central INI handle registry and helpers */ #include "vm/ini/handles.c" #endif -/* Note: INI opcode handlers are included below; changes in vm/ini/*.c require vm.c to rebuild. */ +/* Note: INI opcode handlers are included below; changes in vm/ini/ .c files + * require vm.c to rebuild. */ #include "external/json.c" #include "external/libsql.c" #include "external/pcsc.c"