1
0
Fork 0
forked from fun/fun

Compare commits

...

14 commits

85 changed files with 1663 additions and 800 deletions

2
.gitignore vendored
View file

@ -4,7 +4,7 @@ CMakeCache.txt
!.gitignore
.venv
build*
cmake*
cmake_install.cmake
database.sqlite
demo_*
dist/

View file

@ -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 $<TARGET_FILE:fun>
DEPENDS fun
USES_TERMINAL
COMMENT "Run Fun REPL"
)
COMMAND $<TARGET_FILE:fun>
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}" $<TARGET_FILE:fun> ${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=$<IF:$<BOOL:${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=$<IF:$<BOOL:${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

14
cmake/Dependencies.cmake Normal file
View file

@ -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()

View file

@ -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()

View file

@ -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 "--------------------------------")

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -0,0 +1,2 @@
# REPL toggle (affects sources of the fun executable)
option(FUN_WITH_REPL "Enable interactive REPL in the fun CLI" OFF)

View file

@ -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()

View file

@ -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()

View file

@ -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()

35
cmake/Options.cmake Normal file
View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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)

130
cmake/Targets.cmake Normal file
View file

@ -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)

51
examples/base64_demo.fun Executable file
View file

@ -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 <you@hanez.org>
* 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 <encoding/base64.fun>
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 ===
*/

314
examples/broken/ripemd160.fun Executable file
View file

@ -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 <strings.fun>
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)

View file

@ -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"

View file

@ -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

View file

@ -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:

70
make
View file

@ -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

View file

@ -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:

View file

@ -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

484
spec/v0.3.md Normal file
View file

@ -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.
Whats 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<T>`
- `map<K, V>` (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<number> nums = [1, 2, 3]
array mixed = [1, "two", true, [3, 4]]
m = { "a": 1, "b": 2 } // map<string, number>
```
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 <utils/math.fun>
```
- 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 <utils/math.fun> 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<T>` 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<string>` from `<cli.fun>`; 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 <utils/math.fun> 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 <path> 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.

5
src/CMakeLists.txt Normal file
View file

@ -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)

View file

@ -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 <file> Save current buffer to file\n");
printf(" :load <file> 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 <file>] 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 <file> Save current buffer to file\n");
printf(" :load | :lo <file> 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 <file>] 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 <file>\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 <file>\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 : "<unknown>";
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 <file>]
WHAT: code | stack | globals | consts */
} else if (cmd_is_one_of(cmd, (const char*[]){"mdump","md", NULL})) {
/* Syntax: :mdump WHAT [offset [len]] [raw] [to <file>]
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 <file>]\n");
printf("Usage: :mdump WHAT [offset [len]] [raw] [to <file>]\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 <file>]\n");
printf("Usage: :mdump WHAT [offset [len]] [raw] [to <file>]\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 <file> */
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;

View file

@ -23,6 +23,7 @@
#include <string.h>
#include <stdarg.h>
#include <time.h>
#include <math.h>
#ifdef __unix__
#include <sys/wait.h>
@ -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"