diff --git a/xo-interpreter/.gitrepo b/xo-interpreter/.gitrepo new file mode 100644 index 00000000..894c9bca --- /dev/null +++ b/xo-interpreter/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme +; +[subrepo] + remote = git@github.com:Rconybea/xo-interpreter.git + branch = main + commit = 047658a5b367705afe69422a3409c7c068e2a5fb + parent = b82663b75d776208cc0921c697475c202fe043ab + method = merge + cmdver = 0.4.9 diff --git a/xo-interpreter/CMakeLists.txt b/xo-interpreter/CMakeLists.txt new file mode 100644 index 00000000..a14ee394 --- /dev/null +++ b/xo-interpreter/CMakeLists.txt @@ -0,0 +1,41 @@ +# xo-interpreter/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project (xo_interpreter VERSION 0.1) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# c++ settings + +set(PROJECT_CXX_FLAGS "") +#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") # gcc-only! +add_definitions(${PROJECT_CXX_FLAGS}) + +# ---------------------------------------------------------------- + +# note on ordering: must read .cmake defn of lib before configuring any examples +add_subdirectory(src/interpreter) +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- + +add_subdirectory(example) +add_subdirectory(utest) + +# ---------------------------------------------------------------- + +#if (XO_ENABLE_EXAMPLES) +# install(TARGETS xo_interpreter_ex1 DESTINATION bin/xo/example) +#endif() + +# ---------------------------------------------------------------- +# docs targets depend on all other library/utest targets +# +#add_subdirectory(docs) + +# end CMakeLists.txt diff --git a/xo-interpreter/README.md b/xo-interpreter/README.md new file mode 100644 index 00000000..133805c0 --- /dev/null +++ b/xo-interpreter/README.md @@ -0,0 +1 @@ +# xo-interpreter diff --git a/xo-interpreter/cmake/xo-bootstrap-macros.cmake b/xo-interpreter/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..592272c0 --- /dev/null +++ b/xo-interpreter/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,41 @@ +# ---------------------------------------------------------------- +# for example: +# $ PREFIX=/usr/local # for example +# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build +# +# will get +# CMAKE_MODULE_PATH +# from xo-cmake-config --cmake-module-path +# +# and expect .cmake macros in +# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake +# ---------------------------------------------------------------- + +find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED) + +if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND") + message(FATAL "could not find xo-cmake-config executable") +endif() + +message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}") + +if (XO_SUBMODULE_BUILD) + if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix)) + # local version of xo-cmake macros + set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/xo-cmake/cmake") + message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") + endif() +else() + if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix)) + # default to typical install location for xo-project-macros + execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH) + message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") + endif() +endif() + +# needs to have been installed somewhere on CMAKE_MODULE_PATH, +# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX) +# +include(xo_macros/xo_cxx) + +xo_cxx_bootstrap_message() diff --git a/xo-interpreter/cmake/xo_interpreterConfig.cmake.in b/xo-interpreter/cmake/xo_interpreterConfig.cmake.in new file mode 100644 index 00000000..a1dba569 --- /dev/null +++ b/xo-interpreter/cmake/xo_interpreterConfig.cmake.in @@ -0,0 +1,8 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(xo_alloc) +#find_dependency(xo_flatstring) +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/xo-interpreter/docs/CMakeLists.txt b/xo-interpreter/docs/CMakeLists.txt new file mode 100644 index 00000000..b9df2dd6 --- /dev/null +++ b/xo-interpreter/docs/CMakeLists.txt @@ -0,0 +1,9 @@ +# xo-alloc/docs/CMakeLists.txt + +xo_doxygen_collect_deps() +xo_docdir_doxygen_config() +xo_docdir_sphinx_config( + index.rst install.rst) + +# see xo-reader/doc or xo-unit/doc for working examples +# example.rst install.rst implementation.rst diff --git a/xo-interpreter/docs/README b/xo-interpreter/docs/README new file mode 100644 index 00000000..6aff5d41 --- /dev/null +++ b/xo-interpreter/docs/README @@ -0,0 +1,41 @@ +standalone build + + +-----------------------------------------------+ + | cmake | + | CMakeLists.txt | + | $PREFIX/share/cmake/xo_macros/xo_cxx.cmake | + +-----------------------------------------------+ + | + | +----------------------+ + +------------------------------------------------->| .build/docs/Doxyfile | + | +----------------------+ + | ^ + | (cmake) | + | /------------/ + | | + | +---------------------------------------+ +-----------------+ + +---->| doxygen |--------->| .build/docs/dox | + | | $PREFIX/share/xo-macros/Doxyfile.in | (doxygen)| +- html/ | + | +---------------------------------------+ | +- xml/ | + | +-----------------+ + | | + | |(sphinx) + | | + | v + | +---------------------------------------+ +--------------------+ + \---->| sphinx |------->| .build/docs/sphinx | + | +- conf.py | | +- html/ | + | +- _static/ | +--------------------+ + | +- *.rst | + +---------------------------------------+ + +umbrella build relies on top-level cmake macros + +files + + README this file + CMakeLists.txt build entry point + conf.py sphinx config + _static static files for sphinx + + index.rst toplevel sphinx document; entry point diff --git a/xo-interpreter/docs/_static/README b/xo-interpreter/docs/_static/README new file mode 100644 index 00000000..8230095c --- /dev/null +++ b/xo-interpreter/docs/_static/README @@ -0,0 +1 @@ +add any static {.html, .js, ..} files for sphinx to pickup here \ No newline at end of file diff --git a/xo-interpreter/docs/_static/img/favicon.ico b/xo-interpreter/docs/_static/img/favicon.ico new file mode 100644 index 00000000..4163dd69 Binary files /dev/null and b/xo-interpreter/docs/_static/img/favicon.ico differ diff --git a/xo-interpreter/docs/conf.py b/xo-interpreter/docs/conf.py new file mode 100644 index 00000000..e5855955 --- /dev/null +++ b/xo-interpreter/docs/conf.py @@ -0,0 +1,39 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'xo interpreter documentation' +copyright = '2025, Roland Conybeare' +author = 'Roland Conybeare' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +#extensions = [] +extensions = [ "breathe", + "sphinx.ext.mathjax", # inline math + "sphinx.ext.autodoc", # generate info from docstrings + "sphinxcontrib.ditaa", # diagrams-through-ascii-art + "sphinxcontrib.plantuml" # text -> uml diagrams + ] + +# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in +# match project name in Doxyfile.in +breathe_default_project = "xodoxxml" + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +pygments_style = 'sphinx' + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +#html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +html_favicon = '_static/img/favicon.ico' diff --git a/xo-interpreter/docs/index.rst b/xo-interpreter/docs/index.rst new file mode 100644 index 00000000..e5b5a1c2 --- /dev/null +++ b/xo-interpreter/docs/index.rst @@ -0,0 +1,12 @@ +.. xo-interpreter documentation master file. + +xo-interpreter documentation +============================ + +xo-interpreter provides an interpreter for the Schematika language + +.. toctree:: + :maxdepth: 2 + :caption: xo-interpreter contents + + install diff --git a/xo-interpreter/docs/install.rst b/xo-interpreter/docs/install.rst new file mode 100644 index 00000000..827bb1b0 --- /dev/null +++ b/xo-interpreter/docs/install.rst @@ -0,0 +1,202 @@ +.. _install: + +.. toctree: + :maxdepth: 2 + +Source +====== + +Source code lives on github `here`_ + +.. _here: https://github.com/rconybea/xo-interpreter + +To clone from git: + +.. code-block:: bash + + git clone https://github.com/rconybea/xo-interpreter + +Tested with gcc 14.2 + +Install +======= + +One-step Install +---------------- + +Install xo-interpreter along with the rest of *XO* from `xo-umbrella2 source`_: +see install instructions for xo-umbrella2. + +.. _xo-umbrella2 source: https://github.com/rconybea/xo-umbrella2 + +Essential Xo Dependencies +------------------------- + +``xo-interpreter`` uses several supporting libraries from elsewhere in the *XO* project: + +- `xo-reader source`_ (Schematika expression parser) +- `xo-expression source`_ (Schematika AST representation) +- `xo-tokenizer source`_ (Schematika lexer) +- `xo-object source`_ (gc-eligible runtime polymorphism) +- `xo-randomgen source`_ (fast pseudo-random number generators) +- `xo-alloc source`_ (arena allocators, garbage collector) +- `xo-unit source`_ (dimension checking library) +- `xo-ratio source`_ (exact ratio library) +- `xo-flatstring source`_ (no-allocation string library) +- `xo-callback source`_ (callback library) +- `xo-reflectutil source`_ (reflection utils for participating libs) +- `xo-reflect source`_ (reflection library) +- `xo-refcnt source`_ (reference-counting library) +- `xo-subsys source`_ (utility library) +- `xo-indentlog source`_ (structured logging, pretty-printing) +- `xo-cmake source`_ (shared cmake macros) + +.. _xo-reader source: https://github.com/rconybea/xo-reader +.. _xo-expression source: https://github.com/rconybea/xo-expression +.. _xo-tokenizer source: https://github.com/rconybea/xo-tokenizer +.. _xo-object source: https://github.com/rconybea/xo-object +.. _xo-randomgen source: https://github.com/rconybea/xo-randomgen +.. _xo-alloc source: https://github.com/rconybea/xo-alloc +.. _xo-unit source: https://github.com/rconybea/xo-unit +.. _xo-ratio source: https://github.com/rconybea/xo-ratio +.. _xo-flatstring source: https://github.comr/rconybea/xo-flatstring +.. _xo-callback source: https://github.com/rconybea/xo-callback +.. _xo-reflect source: https://github.com/rconybea/xo-reflect +.. _xo-refcnt source: https://github.com/rconybea/refcnt +.. _xo-subsys source: https://github.com/rconybea/subsys +.. _xo-indentlog source: https://github.com/rconybea/indentlog +.. _xo-cmake source: https://github.com/rconybea/xo-cmake + +Building from source +-------------------- + +Instructions for building xo-interpreter from source, along with only its essential dependencies. + +Install scripts for XO libraries depend on helper scripts installed from `xo-cmake`. + +Preamble: + +.. code-block:: bash + + mkdir -p ~/proj/xo + cd ~/proj/xo + + git clone https://github.com/rconybea/xo-cmake + + PREFIX=$HOME/local # or desired installation path + + # will want PREFIX/bin in PATH to use xo-cmake helpers + PATH=$PREFIX/bin:$PATH + +Isntall `xo-cmake`: + +.. code-block:: bash + + cmake -B xo-cmake/.build -S xo-cmake + cmake --install xo-cmake/.build + +Now that we have xo-build in PATH, can build+install XO components in topological order: + +.. code-block:: bash + + xo-build --clone --configure --build --install xo-indentlog + xo-build --clone --configure --build --install xo-subsys + xo-build --clone --configure --build --install xo-refcnt + xo-build --clone --configure --build --install xo-reflect + xo-build --clone --configure --build --install xo-reflectutil + xo-build --clone --configure --build --install xo-callback + xo-build --clone --configure --build --install xo-flatstring + xo-build --clone --configure --build --install xo-ratio + xo-build --clone --configure --build --install xo-unit + xo-build --clone --configure --build --install xo-alloc + xo-build --clone --configure --build --install xo-randomgen + xo-build --clone --configure --build --install xo-object + xo-build --clone --configure --build --install xo-tokenizer + xo-build --clone --configure --build --install xo-expression + xo-build --clone --configure --build --install xo-reader + +Directories under ``PREFIX`` will then contain something like: + +.. code-block:: + + PREFIX + += bin + | +- xo-build + │ +- xo-cmake-config + │ \- xo-cmake-lcov-harness + +─ include + | \- xo + │ +- alloc/ + | +- callback/ + | +- cxxutil/ + | +- expression/ + | | +- typeinf/ + | | .. + | +- flatstring/ + | +- indentlog/ + | | +- machdep/ + | | +- print/ + | | +- timeutil/ + | | .. + | +- object/ + | +- randomgen/ + | +- ratio/ + | +- reader/ + | +- refcnt/ + | +- reflect/ + | | +- atomic/ + | | +- function/ + | | +- pointer/ + | | +- struct/ + | | +- vector/ + | | .. + | +- reflectutil/ + | +- subsys/ + | +- tokenizer/ + | \- unit/ + +- lib + | +- cmake + | | +- callback/ + | | +- indentlog/ + | | +- randomgen/ + | | +- refcnt/ + | | +- reflect/ + | | +- subsys/ + | | +- xo_alloc/ + | | +- xo_expression/ + | | +- xo_flatstring/ + | | +- xo_object/ + | | +- xo_ratio/ + | | +- xo_reader/ + | | +- xo_reflectutil/ + | | +- xo_tokenizer/ + | | \- xo_unit/ + | +- librefcnt.so + | .. + \- share + +- cmake + | \- xo-macros + | +- code-coverage.cmake + | +- xo_cxx.cmake + | \- xo-project-macros.cmake + +- etc + | \- xo + | \- subsystem_list + +- xo-macros + +- Doxyfile.in + +- gen-ccov.in + \- xo-bootstrap-macros.cmake + +CMake Support +------------- + +To use built-in cmake support, when using ``xo-interpreter`` from another project: + +Make sure ``PREFIX/lib/cmake`` is searched by cmake (for example include it in ``CMAKE_PREFIX_PATH``) + +Add to your ``CMakeLists.txt``: + +.. code-block:: cmake + + FindPackage(xo_interpreter CONFIG REQUIRED) + target_link_libraries(mytarget PUBLIC xo_interpreter) diff --git a/xo-interpreter/example/CMakeLists.txt b/xo-interpreter/example/CMakeLists.txt new file mode 100644 index 00000000..ce7aaf20 --- /dev/null +++ b/xo-interpreter/example/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(replxx) diff --git a/xo-interpreter/example/replxx/CMakeLists.txt b/xo-interpreter/example/replxx/CMakeLists.txt new file mode 100644 index 00000000..96f11ae4 --- /dev/null +++ b/xo-interpreter/example/replxx/CMakeLists.txt @@ -0,0 +1,17 @@ +# xo-interpreter/example/replxx/CMakeLists.txt + +set(SELF_EXE xo_interpreter_replxx) +set(SELF_SRCS replxx.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_dependency(${SELF_EXE} xo_interpreter) + # TODO: consider promoting to regular app + xo_dependency(${SELF_EXE} xo_reader) + xo_external_target_dependency(${SELF_EXE} replxx replxx::replxx) + + find_package(Threads REQUIRED) + target_link_libraries(${SELF_EXE} PUBLIC Threads::Threads) +endif() + +# end CMakeLists.txt diff --git a/xo-interpreter/example/replxx/replxx.cpp b/xo-interpreter/example/replxx/replxx.cpp new file mode 100644 index 00000000..ef677594 --- /dev/null +++ b/xo-interpreter/example/replxx/replxx.cpp @@ -0,0 +1,19 @@ +/** @file replxx.cpp **/ + +#include "xo/interpreter/Schematika.hpp" + +int +main(int argc, char ** argv) +{ + using xo::log_level; + using xo::scm::Schematika; + + Schematika::Config cfg; + cfg.debug_flag = true; + cfg.vsm_log_level_ = log_level::verbose; + Schematika scm = Schematika::make(cfg); + + scm.interactive_repl(); +} + +/* end replxx.cpp */ diff --git a/xo-interpreter/include/xo/interpreter/BuiltinPrimitives.hpp b/xo-interpreter/include/xo/interpreter/BuiltinPrimitives.hpp new file mode 100644 index 00000000..39995b23 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/BuiltinPrimitives.hpp @@ -0,0 +1,36 @@ +/** @file BuiltinPrimitives.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#include "xo/object/ObjectConverter.hpp" +#include "xo/allocutil/IAlloc.hpp" +#include "Primitive.hpp" +#include "GlobalEnv.hpp" + +namespace xo { + namespace scm { + struct BuiltinPrimitives { + public: + using ObjectConverter = xo::obj::ObjectConverter; + + template + static void install_pm(gc::IAlloc * mm, rp pm_expr, gp env) { + gp rhs + = xo::obj::make_primitive(mm, pm_expr->name(), pm_expr->value()); + + /* store in env using this variable-expr */ + rp lhs + = Variable::make(pm_expr->name(), pm_expr->value_td()); + + gp * addr = env->establish_var(lhs.borrow()); + + *addr = rhs; + } + + static void install(gc::IAlloc * mm, gp env); + }; + } +} + +/* end BuiltinPrimitives.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/Env.hpp b/xo-interpreter/include/xo/interpreter/Env.hpp new file mode 100644 index 00000000..bce51319 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/Env.hpp @@ -0,0 +1,47 @@ +/** @file Env.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "xo/alloc/Object.hpp" +#include "xo/refcnt/Refcounted.hpp" + +namespace xo { + namespace scm { + class Variable; // see xo::scm::Variable in xo/expression/Variable.hpp + + /** @class Env + * @brief runtime environment, holding variable bindings for schematika interpreter + * + * Garbage-collected + * + * TODO: rename xo-expression xo::scm::Environment -> xo::scm::SymbolTable + **/ + class Env : public Object { + public: + /** true iff @p vname is present in Symtab for innermost environment **/ + virtual bool local_contains_var(const std::string & vname) const = 0; + + /** Fetch storage location for innermost binding of variable with name @p vname. + * nullptr if not found + **/ + virtual gp * lookup_slot(const std::string & vname) = 0; + + /** require storage for variable @p v. + * will also establish binding path. + * + * Intended for introducing a new variable, + * replacing any previous variable with the same name. + * + * Beware of invalidating type correctness + * + * @return slot address for runtime value of @p v + **/ + virtual gp * establish_var(bp v) = 0; + + //gp lookup_symbol(const std::string & name) const; + }; + } /*namespace scm*/ +} /*namespace xo*/ diff --git a/xo-interpreter/include/xo/interpreter/ExpressionBoxed.hpp b/xo-interpreter/include/xo/interpreter/ExpressionBoxed.hpp new file mode 100644 index 00000000..5668a08a --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/ExpressionBoxed.hpp @@ -0,0 +1,48 @@ +/** @file ExpressionBoxed.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "xo/alloc/Object.hpp" +#include "xo/expression/Expression.hpp" + +namespace xo { + namespace scm { + /** @class ExpressionBoxed + * @brief xo::scm::Expression, adapted to xo::Object interface + **/ + class ExpressionBoxed : public Object { + public: + explicit ExpressionBoxed(bp c); + + /** create boxed version of @p c, using allocator @p mm **/ + static gp make(gc::IAlloc * mm, + bp c); + + /** runtime downcast **/ + static gp from(gp x) { + return gp::from(x); + } + + const rp & contents() const { return contents_; } + + // inherited from Object + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc * mm) const final override; + virtual std::size_t _forward_children(gc::IAlloc * /*gc*/) final override; + + private: + /** reference-counted Expression pointer + * + * NOTE correctness requires finalization support in xo::gc::GC + **/ + rp contents_; + }; + } /*namespace scm*/ +} /*namespace xo*/ + +/* end ExpressionBoxed.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/GlobalEnv.hpp b/xo-interpreter/include/xo/interpreter/GlobalEnv.hpp new file mode 100644 index 00000000..50e765ba --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/GlobalEnv.hpp @@ -0,0 +1,73 @@ +/** @file GlobalEnv.hpp **/ + +#pragma once + +#include "Env.hpp" +#include "xo/allocutil/IAlloc.hpp" +#include "xo/expression/GlobalSymtab.hpp" + +namespace xo { + namespace scm { + /** @class GlobalEnv + * @brief Top-level global environment + **/ + class GlobalEnv : public Env { + public: + using map_type = std::map>; + + public: + /** Create top-level global environment, allocating via @p mm. + * Expect one of these per interpreter session. + **/ + static gp make_empty(gc::IAlloc * mm, + const rp & symtab); + +#ifdef NOT_USING + gc::IAlloc * get_mm() const { return mm_; } +#endif + + const rp & symtab() const { return symtab_; } + + // inherited from Env.. + virtual bool local_contains_var(const std::string & vname) const final override; + virtual gp * lookup_slot(const std::string & vname) final override; + virtual gp * establish_var(bp var) final override; + + // inherited from Object.. + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc * mm) const final override; + virtual std::size_t _forward_children(gc::IAlloc * mm) final override; + + private: + GlobalEnv(const GlobalEnv & x); + GlobalEnv(gc::IAlloc * mm, const rp & symtab); + + private: + /** memory manager to use **/ + gc::IAlloc * mm_ = nullptr; + + /** global symbol table. + * variables known to @c symtab_ are represented by + * corresponding values in @p slot_map_ + **/ + rp symtab_; + + /** environment contents. + * expression @c symtab_->lookup_binding(vname) + * has associated value @c slot_map_.at(vname) + * + * TODO: replace with something subject to GC ? + * every member of @ref slot_map_ will have to be a + * GC root + * + * TODO: probably want to hash here instead. + * May also want lhs names to be separately hashed symbols + **/ + up slot_map_; + }; + } /*namespace scm*/ +} /*namespace xo*/ + +/* end GlobalEnv.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/LocalEnv.hpp b/xo-interpreter/include/xo/interpreter/LocalEnv.hpp new file mode 100644 index 00000000..9745a854 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/LocalEnv.hpp @@ -0,0 +1,95 @@ +/** @file LocalEnv.hpp **/ + +#include "Env.hpp" +#include "CVector.hpp" +#include "xo/allocutil/IAlloc.hpp" +#include "xo/expression/LocalSymtab.hpp" +#include +#include + +namespace xo { + namespace scm { + /** @class LocalEnv + * @brief Represent a single runtime stack frame for a Schematika function + * + * LocalEnv intended to be used for interpreted functions. + * + * Compiled functions will still likely have stack frames, but need not use the + * @ref LocalEnv class + * + * memory layout: + * ^ + * +-----------------------+ | + * | vtable | | + * +-----------------------+ | + * | .parent +------/ + * +------------+----------+ + * | .slot_v_ | .n_ | + * | +----------+ + * | | .v_ +------\ + * +------------+----------+ <--/ + * | .v_[0] +---------> Object(1) + * +-----------------------+ + * . .. . + * +-----------------------+ + * | .v_[.n_-1] +---------> Object(n) + * +-----------------------+ + **/ + class LocalEnv : public Env { + public: + using TaggedPtr = xo::reflect::TaggedPtr; + + public: + LocalEnv(gc::IAlloc * mm, gp p, const rp & s, std::size_t n); + + /** create frame using allocator @p mm, + * with parent @p p and exactly @p n_slot object pointers. + * variable types are taken from symbol table @p s. + **/ + static gp make(gc::IAlloc * mm, + gp p, + const rp & s, + std::size_t n_slot); + + /** reflect LocalEnv object representation **/ + static void reflect_self(); + + gp parent() const { return parent_; } + std::size_t size() const { return slot_v_.size(); } + + gp operator[](std::size_t i) const { return slot_v_[i]; } + gp & operator[](std::size_t i) { return slot_v_[i]; } + + // inherited from Env.. + + virtual bool local_contains_var(const std::string & vname) const final override; + + virtual gp * lookup_slot(const std::string & vname) final override; + + /** LocalEnv policy is that variable can be established once only. + * For example function arguments must all have distinct names. + **/ + virtual gp * establish_var(bp v) final override; + + // inherited from Object.. + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc * mm) const final override; + virtual std::size_t _forward_children(gc::IAlloc * /*gc*/) final override; + + private: + /** parent stack frame **/ + gp parent_; + /** origin symbol table. records variable names and bindings. + * for a binding path p with leaf slot index j = p.j_slot_: + * @c slot_v_[j] holds value associated with variable @c symtab_->argv_[j] + **/ + rp symtab_; + /** environment contents **/ + obj::CVector> slot_v_; + }; + } /*namespace scm*/ +} /*namespace xo*/ + +/* end LocalEnv.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/Schematika.hpp b/xo-interpreter/include/xo/interpreter/Schematika.hpp new file mode 100644 index 00000000..0c02d974 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/Schematika.hpp @@ -0,0 +1,70 @@ +/** @file Schematika.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "xo/alloc/GC.hpp" + +namespace xo { + namespace scm { + /** @class Schematika + * @brief schematika interpreter state + **/ + class Schematika { + public: + class Impl; + + struct Config { + /** true to enable welcome message **/ + bool welcome_flag_ = true; + /** number of command history items to preserve **/ + std::size_t history_size = 100; + /** on startup: load command history from this file; + persist last @ref history_size commands to the same file + **/ + std::string history_file = "scm_history.txt"; + /** when true enable console logging for repl internals **/ + bool debug_flag = false; + + /** garbage collector configuration **/ + gc::Config gc_config_; + + /** control schematika vsm logging **/ + log_level vsm_log_level_; + }; + + using IAlloc = xo::gc::IAlloc; + + public: + ~Schematika(); + + /** create instance with configuration @p cfg **/ + static Schematika make(const Config & cfg); + + /** interactive read-eval-print loop. + * Uses replxx to read from stdin. + * If stdin is interactive, accepts line editing commands: + * - ctrl-a goto beginning of line + * - ctrl-e goto end of line + * - ctrl-k delete to end of line + * - meta- backwards delete word + * - meta-p| retrieve previous command from history + * - meta-n| retrieve next command from history + * - / page through history faster + * - ctrl-s forward history search + * - ctrl-r backward history search + **/ + void interactive_repl(); + + private: + explicit Schematika(const Config & cfg); + + private: + up p_impl_; + }; + } +} + +/* end Schematika.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/SchematikaError.hpp b/xo-interpreter/include/xo/interpreter/SchematikaError.hpp new file mode 100644 index 00000000..6baabad3 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/SchematikaError.hpp @@ -0,0 +1,28 @@ +/** @file SchematikaError.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include + +namespace xo { + namespace scm { + class SchematikaError { + public: + SchematikaError() = default; + explicit SchematikaError(std::string x) : what_{std::move(x)} {} + + const std::string & what() const { return what_; } + + bool is_error() const { return !what_.empty(); } + bool is_not_an_error() const { return what_.empty(); } + + private: + std::string what_; + }; + } +} + +/* end SchematikaError.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/VirtualSchematikaMachine.hpp b/xo-interpreter/include/xo/interpreter/VirtualSchematikaMachine.hpp new file mode 100644 index 00000000..a8a182d9 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/VirtualSchematikaMachine.hpp @@ -0,0 +1,187 @@ +/** @file VirtualSchematikaMachine.hpp **/ + +#pragma once + +#include "VsmInstr.hpp" +#include "VsmStackFrame.hpp" +#include "SchematikaError.hpp" +#include "GlobalEnv.hpp" +#include "xo/expression/Expression.hpp" +#include "xo/object/ObjectConverter.hpp" +#include "xo/alloc/Object.hpp" + +namespace xo { + namespace scm { + /** @brief state that may be shared across VirtualSchematikaMachine instances **/ + struct VirtualSchematikaMachineFlyweight { + explicit VirtualSchematikaMachineFlyweight(gc::IAlloc * mm, + gp env, + log_level log_level); + + /** memory allocator for interpreter operation. **/ + gc::IAlloc * object_mm_ = nullptr; + /** global environment **/ + gp toplevel_env_; + /** convert TaggedPtr->Object **/ + xo::obj::ObjectConverter object_converter_; + /** control logging level. higher values -> more logging **/ + log_level log_level_; + }; + + /** @class VirtualSchematikaMachine + * @brief Virtual machine implementing a Schematika interpreter + * + **/ + class VirtualSchematikaMachine { + public: + using IAlloc = xo::gc::IAlloc; + + public: + VirtualSchematikaMachine(IAlloc * mm, gp toplevel_env, log_level log_level); + ~VirtualSchematikaMachine(); + + gp toplevel_env() const { return flyweight_.toplevel_env_; } + + /** evaluate expression @p expr. + * borrows calling thread until completion + * return [value, error]. error ignored unless value is nullptr. + * conversely when value is nullptr, error gives details of original + * error. + * + * Evaluate schematika expression @p expr in environment @p env + **/ + std::pair, SchematikaError> eval(bp expr, gp env); + + /** evaluate expression @p expr in toplevel environment **/ + std::pair, SchematikaError> toplevel_eval(bp expr); + + private: + /** Not moveable or copyable. + * One constraint is member variables added to flyweight_.object_mm_ + * as GC roots, with no provision for unwinding later. + **/ + VirtualSchematikaMachine(const VirtualSchematikaMachine &) = delete; + VirtualSchematikaMachine(VirtualSchematikaMachine &&) = delete; + + /** borrow calling thread to run schematika machine + * indefinitely, or until null continuation + **/ + void run(); + + /** execute vsm instruction in program counter. + * Note: may possibly be able to replace with just opcode + * + * Registers: + * - expr_ input, caller saves + * - env_ input, caller saves + * - cont_ input, caller saves + * - stack_ input, caller saves + * - value_ output + * - error_ output + * + **/ + void execute_one(); + + /* design note: + * - eval_xxx_op() methods are primary VSM transitions, + * in the sense that they begin a sequence of transitions to interpret a + * particular kind of expression + * - do_xxx_op() methods represent secondary VSM transitions, + * that continue an instruction sequence that was initiated by a preceding + * eval_xxx_op() method + */ + + /** interpret literal constant expression **/ + void eval_constant_op(); + + /** interpreter literal primitive expression + * (these appear implicitly as result of builtin operators like {+, ==, ..}) + **/ + void eval_primitive_op(); + + /** execute define expression (finished in do_complete_assign_op()) **/ + void eval_define_op(); + /** execute assign expression (finishes in do_complete_assign_op()) **/ + void eval_assign_op(); + /** continue after establishing value fo rhs of define exprsssion **/ + void do_complete_assign_op(); + + /** interpret variable expression **/ + void eval_variable_op(); + + /** interpret if-expression **/ + void eval_ifexpr_op(); + /** continue after establish value of test expression **/ + void do_complete_ifexpr_op(); + + /** interpret sequence **/ + void eval_sequence_op(); + /** continue after establishing value for a sequence element **/ + void do_complete_sequence_op(); + + /** interpret apply-expression (i.e. function call) **/ + void eval_apply_op(); + /** continue assembling args for a function call; + * transition to (interpretation of) called function once all arguments + * are evaluated. + **/ + void do_complete_evalargs_op(); + + /** execute function application, given actuals in top stack frame **/ + void apply_op(); + + /** goto error state with message @p err **/ + void report_error(const std::string & err); + + /** implementation class; contains instruction implementations **/ + friend struct VsmOps; + + private: + /** program counter. + * (Perhaps replace with VsmInstr::Opcode ?) + **/ + const VsmInstr * pc_ = nullptr; + + /** register to hold Schematika expression to drive @ref execute_one. + * + * caller saves! + **/ + rp expr_; + /** holds bindings for all schematika variables, to drive @ref execute_one. + * execute_one will not save this + * + * caller saves! + **/ + gp env_; + + /** vsm stack. callee saves! + **/ + gp stack_; + + /** non-error result value from eval() / apply() + * + * output register: caller must save + **/ + gp value_; + + /** error result value from eval() / apply() + * + * output regisetr: caller must save + **/ + SchematikaError error_; + + /** continuation + * (Perhaps replace with VsmInstr::Opcode ?) + * + * input register: callee saves! + **/ + const VsmInstr * cont_ = nullptr; + + /** possibly-shared data **/ + VirtualSchematikaMachineFlyweight flyweight_; + }; + + } /*namespace scm*/ +} /*namespace xo*/ + +/* end VirtualSchematikaMachine.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/VsmInstr.hpp b/xo-interpreter/include/xo/interpreter/VsmInstr.hpp new file mode 100644 index 00000000..a1539bd0 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/VsmInstr.hpp @@ -0,0 +1,80 @@ +/** @file VsmInstr.hpp **/ + +#pragma once + +#include + +namespace xo { + namespace scm { + class VirtualSchematikaMachine; // see VirtualSchematikaMachine.hpp + + /** @class VmInstr + * @brief Represent a particular vritual schematika machine instruction + * + * A vsm instruction acts on a virtual schematika machine instance. + **/ + class VsmInstr + { + public: + enum class Opcode { + /** Halt virtual schematika machine **/ + halt, + + /** Evaluate a schematika expression. + * See VirtualSchematikaMachine::eval() + **/ + eval, + + /** assign to variable + continue + * + * stack: frame with: + * [0] lhs : variable to assign + **/ + complete_assign, + + /** execute ifexpr branch, given + * result of test expression has been established + **/ + complete_ifexpr, + + /** execute remainder of expression sequence + **/ + complete_sequence, + + /** execute remainder of argument sequence evaluation; + * subsidiary to marshalling a function call + **/ + complete_evalargs, + + /** Call a function. Arguments have been evaluated + * and are in top stack frame, in order, + * starting with target function itself + **/ + apply, + + /** choose branch of if-expression + continue + * + * stack: frame with + * [0] ifexpr : original if-expression + **/ + N_Opcode + }; + + //using ActionFn = void (*)(VirtualSchematikaMachine * vm); + + public: + VsmInstr(Opcode opcode, std::string_view name); + + Opcode opcode() const { return opcode_; } + + private: + /** unique opcode for this instruction **/ + Opcode opcode_; + /** **/ + std::string_view name_; + //ActionFn action_; + }; + } +} + +/* end VsmInstr.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/VsmStackFrame.hpp b/xo-interpreter/include/xo/interpreter/VsmStackFrame.hpp new file mode 100644 index 00000000..0fc0dee7 --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/VsmStackFrame.hpp @@ -0,0 +1,83 @@ +/** @file VsmStackFrame.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "VsmInstr.hpp" +#include "xo/object/CVector.hpp" +#include "xo/alloc/Object.hpp" + +namespace xo { + namespace scm { + /** @class VsmStackFrame + * @brief Virtual Schematika Machine stack frame + * + * Intending to use the "cheney on the MTA" strategy, + * i.e. allocate frames using GC's bump allocator. + * + * Parallels LocalEnv, but VSM implementation isn't reflected + **/ + class VsmStackFrame : public Object { + public: + VsmStackFrame(gc::IAlloc * mm, gp p, std::size_t n, const VsmInstr * cont); + + /** create frame using allocator @p mm, + * with parent @p p and exactly @p n_slot object pointers. + **/ + static gp make(gc::IAlloc * mm, + gp p, + std::size_t n_slot, + const VsmInstr * cont); + + /** create new stack frame using allocator @p mm, + * with parent frame @p p; new frame contains values @p s0 + **/ + static gp push1(gc::IAlloc * mm, + gp p, + gp s0, + const VsmInstr * cont); + + /** create new stack frame using allocator @p mm, + * with parent frame @p p; new frame contains values @p s0, @p s1 + **/ + static gp push2(gc::IAlloc * mm, + gp p, + gp s0, + gp s1, + const VsmInstr * cont); + + + /** reflect VsmStackFrame object representation **/ + static void reflect_self(); + + gp parent() const { return parent_; } + std::size_t size() const { return slot_v_.size(); } + const obj::CVector> & argv() const { return slot_v_; } + const VsmInstr * continuation() const { return cont_; } + + gp operator[](std::size_t i) const { return slot_v_[i]; } + gp & operator[](std::size_t i) { return slot_v_[i]; } + + // inherited from Object.. + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc *) const final override; + virtual std::size_t _forward_children(gc::IAlloc *) final override; + + private: + /** parent stack frame **/ + gp parent_; + + /** stored state **/ + obj::CVector> slot_v_; + + /** proceed to this continuation when popping this frame **/ + const VsmInstr * cont_ = nullptr; + }; + } /*namespace scm*/ +} /*namespace xo*/ + +/* end VsmStackFrame.hpp */ diff --git a/xo-interpreter/include/xo/interpreter/init_interpreter.hpp b/xo-interpreter/include/xo/interpreter/init_interpreter.hpp new file mode 100644 index 00000000..91e4828c --- /dev/null +++ b/xo-interpreter/include/xo/interpreter/init_interpreter.hpp @@ -0,0 +1,21 @@ +/** @file init_interpreter.hpp + * + * author: Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "xo/subsys/Subsystem.hpp" + +namespace xo { + /* tag to represent the interpreter/ subsystem in ordered initialization */ + enum S_interpreter_tag {}; + + template<> + struct InitSubsys { + static void init(); + static InitEvidence require(); + }; +} + +/* end init_interpreter.hpp */ diff --git a/xo-interpreter/src/interpreter/BuiltinPrimitives.cpp b/xo-interpreter/src/interpreter/BuiltinPrimitives.cpp new file mode 100644 index 00000000..bead7c87 --- /dev/null +++ b/xo-interpreter/src/interpreter/BuiltinPrimitives.cpp @@ -0,0 +1,94 @@ +/** @file BuiltinPrimitives.cpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#include "BuiltinPrimitives.hpp" +#include "Integer.hpp" +#include "Primitive.hpp" +#include "xo/expression/PrimitiveExpr.hpp" +#include "xo/object/ObjectConversion.hpp" +#include "xo/reflect/Reflect.hpp" +#include + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + using xo::reflect::TypeDescr; + + namespace scm { + int64_t + add64(int64_t x, int64_t y) + { + return x + y; + } + + void + BuiltinPrimitives::install(gc::IAlloc * mm, gp env) + { + scope log(XO_DEBUG(true)); + + // add(x,y) + { + gp rhs = xo::obj::make_primitive(mm, "add", add64); + + TypeDescr td = Reflect::require_function(); + + rp lhs = Variable::make("add", td); + gp * addr = env->establish_var(lhs.borrow()); + + *addr = rhs; + } + + // i64 comparisons + + // @cmp_eq2_i64 + install_pm(mm, PrimitiveExpr_cmp_i64::make_cmp_eq2_i64(), env); + + // @cmp_ne2_i64 + install_pm(mm, PrimitiveExpr_cmp_i64::make_cmp_ne2_i64(), env); + + // @cmp_lt2_i64 + install_pm(mm, PrimitiveExpr_cmp_i64::make_cmp_lt2_i64(), env); + + // @cmp_le2_i64 + install_pm(mm, PrimitiveExpr_cmp_i64::make_cmp_le2_i64(), env); + + // @cmp_gt2_i64 + install_pm(mm, PrimitiveExpr_cmp_i64::make_cmp_gt2_i64(), env); + + // @cmp_ge2_i64 + install_pm(mm, PrimitiveExpr_cmp_i64::make_cmp_ge2_i64(), env); + + // i64 arithmetic + + // @add2_i64 + install_pm(mm, PrimitiveExpr_i64::make_add2_i64(), env); + + // @sub2_i64 + install_pm(mm, PrimitiveExpr_i64::make_sub2_i64(), env); + + // @mul2_i64 + install_pm(mm, PrimitiveExpr_i64::make_mul2_i64(), env); + + // @div2_i64 + install_pm(mm, PrimitiveExpr_i64::make_div2_i64(), env); + + // ---------------------------------------------------------------- + + // @add2_f64 + install_pm(mm, PrimitiveExpr_f64::make_add2_f64(), env); + + // @sub2_f64 + install_pm(mm, PrimitiveExpr_f64::make_sub2_f64(), env); + + // @mul2_f64 + install_pm(mm, PrimitiveExpr_f64::make_mul2_f64(), env); + + // @div2_f64 + install_pm(mm, PrimitiveExpr_f64::make_div2_f64(), env); + } + } +} + +/* end BuiltinPrimitives.cpp */ diff --git a/xo-interpreter/src/interpreter/CMakeLists.txt b/xo-interpreter/src/interpreter/CMakeLists.txt new file mode 100644 index 00000000..912d4a93 --- /dev/null +++ b/xo-interpreter/src/interpreter/CMakeLists.txt @@ -0,0 +1,24 @@ +# interpreter/CMakeLists.txt + +set(SELF_LIB xo_interpreter) +set(SELF_SRCS + init_interpreter.cpp + Schematika.cpp + BuiltinPrimitives.cpp + LocalEnv.cpp + GlobalEnv.cpp + VirtualSchematikaMachine.cpp + VsmInstr.cpp + VsmStackFrame.cpp + ExpressionBoxed.cpp +) + +find_package(Threads REQUIRED) + +xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) +xo_dependency(${SELF_LIB} xo_object) +xo_dependency(${SELF_LIB} xo_expression) +xo_dependency(${SELF_LIB} xo_reader) +xo_external_target_dependency(${SELF_LIB} replxx replxx::replxx) +target_link_libraries(${SELF_LIB} PUBLIC Threads::Threads) +xo_headeronly_dependency(${SELF_LIB} subsys) diff --git a/xo-interpreter/src/interpreter/ExpressionBoxed.cpp b/xo-interpreter/src/interpreter/ExpressionBoxed.cpp new file mode 100644 index 00000000..2d72ef33 --- /dev/null +++ b/xo-interpreter/src/interpreter/ExpressionBoxed.cpp @@ -0,0 +1,59 @@ +/** @file ExpressionBoxed.cpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#include "ExpressionBoxed.hpp" +#include "xo/reflect/Reflect.hpp" + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + + namespace scm { + ExpressionBoxed::ExpressionBoxed(bp c) : contents_{c.promote()} + {} + + gp + ExpressionBoxed::make(gc::IAlloc * mm, + bp c) + { + return new (MMPtr(mm)) ExpressionBoxed(c); + } + + + TaggedPtr + ExpressionBoxed::self_tp() const + { + return Reflect::make_tp(const_cast(this)); + } + + void + ExpressionBoxed::display(std::ostream & os) const + { + os << contents_; + } + + std::size_t + ExpressionBoxed::_shallow_size() const + { + return sizeof(ExpressionBoxed); + } + + Object * + ExpressionBoxed::_shallow_copy(gc::IAlloc * mm) const + { + Cpof cpof(mm, this); + + return new (cpof) ExpressionBoxed(*this); + } + + std::size_t + ExpressionBoxed::_forward_children(gc::IAlloc *) + { + return _shallow_size(); + } + } /*namespace scm*/ +} /*namespace xo*/ + +/* end ExpressionBoxed.cpp */ diff --git a/xo-interpreter/src/interpreter/GlobalEnv.cpp b/xo-interpreter/src/interpreter/GlobalEnv.cpp new file mode 100644 index 00000000..736af083 --- /dev/null +++ b/xo-interpreter/src/interpreter/GlobalEnv.cpp @@ -0,0 +1,127 @@ +/** @file GlobalEnv.cpp **/ + +#include "GlobalEnv.hpp" +#include "xo/reflect/Reflect.hpp" + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + + namespace scm { + gp + GlobalEnv::make_empty(gc::IAlloc * mm, const rp & symtab) + { + /* by design: GlobalEnv and GlobalEnv.slot_map_ are heap-allocated */ + + return new GlobalEnv(mm, symtab); + } + + GlobalEnv::GlobalEnv(const GlobalEnv & x) + : mm_{x.mm_}, + symtab_{x.symtab_}, + slot_map_{std::make_unique(*x.slot_map_)} + { + } + + GlobalEnv::GlobalEnv(gc::IAlloc * mm, + const rp & symtab) : mm_{mm}, + symtab_{symtab}, + slot_map_{std::make_unique()} + {} + + bool + GlobalEnv::local_contains_var(const std::string & vname) const + { + return symtab_->lookup_local(vname).get(); + } + + gp * + GlobalEnv::lookup_slot(const std::string & vname) + { + scope log(XO_DEBUG(true), xtag("name", vname)); + + assert(slot_map_.get()); + + auto ix = slot_map_->find(vname); + + if (ix == slot_map_->end()) { + return nullptr; + } else { + log && log("binding found", xtag("vname", vname)); + return &(ix->second); + } + } + + gp * + GlobalEnv::establish_var(bp var) + { + scope log(XO_DEBUG(true), xtag("name", var->name()), xtag("type", var->valuetype())); + + // Warning: altering declared type for an already-existing variable + // invalidates any type checking that relied on that variable. + // + // Ignoring this problem for now. + // + // Actual solution might look like: + // - keep track of which functions/defs depend on each global variable. + // - invalidate any jit / types for such variables. + // - maybe use seqno's to track + // - re-check / re-complie + // - need to admit invalid states. + // suppose have mutually recursive functions f(), g() + // want ability to modify type signatures separately + // + // Alternatives: + // - forbid changing type of an already-established variable + // Actually: can't even change values if we intend supporting dependent types + // - quietly number variables so new definitions shadow old ones but don't + // affect previously-encountered expressions + + this->symtab_->require_global(var->name(), var); + + gp &slot = (*this->slot_map_)[var->name()]; + + /* discard any pre-existing value, we're redefining a variable */ + slot = gp(); + + return &slot; + } + + TaggedPtr + GlobalEnv::self_tp() const + { + return Reflect::make_tp(const_cast(this)); + } + + void + GlobalEnv::display(std::ostream & os) const + { + os << "size()) << ">"; + } + + std::size_t + GlobalEnv::_shallow_size() const + { + return sizeof(GlobalEnv); + } + + Object * + GlobalEnv::_shallow_copy(gc::IAlloc * mm) const + { + Cpof cpof(mm, this); + + return new (cpof) GlobalEnv(*this); + } + + std::size_t + GlobalEnv::_forward_children(gc::IAlloc * gc) + { + for (auto & ix : *slot_map_) { + Object::_forward_inplace(ix.second, gc); + } + return _shallow_size(); + } + } /*namespace scm*/ +} /*namespace xo*/ + +/* end GlobalEnv.cpp */ diff --git a/xo-interpreter/src/interpreter/LocalEnv.cpp b/xo-interpreter/src/interpreter/LocalEnv.cpp new file mode 100644 index 00000000..8085f126 --- /dev/null +++ b/xo-interpreter/src/interpreter/LocalEnv.cpp @@ -0,0 +1,185 @@ +/** @file LocalEnv.cpp **/ + +#include "LocalEnv.hpp" +#include "xo/reflect/Reflect.hpp" +#include "xo/reflect/StructReflector.hpp" +#include + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::StructReflector; + using xo::reflect::TypeDescrW; + using xo::reflect::TaggedPtr; + using xo::reflect::TypeDescrExtra; + using xo::reflect::EstablishTypeDescr; + using xo::reflect::StlVectorTdx; + using xo::print::quot; + + namespace scm { + namespace { + std::size_t + slot_array_size(std::size_t n) { + return n * sizeof(gp); + } + } + + gp + LocalEnv::make(gc::IAlloc * mm, + gp p, + const rp & s, + std::size_t n) + { + if (s) { + assert(static_cast(n) == s->n_arg()); + } + + return new (MMPtr(mm)) LocalEnv(mm, p, s, n); + } + + LocalEnv::LocalEnv(gc::IAlloc * mm, + gp p, + const rp & s, + std::size_t n) : parent_{p}, + symtab_{s}, + slot_v_{mm, n} + {} + + bool + LocalEnv::local_contains_var(const std::string & vname) const + { + assert(symtab_.get()); + + return symtab_->lookup_local(vname); + } + + gp * + LocalEnv::lookup_slot(const std::string & vname) + { + binding_path b = symtab_->lookup_local_binding(vname); + + if (b.i_link_ == 0) { + assert((b.j_slot_ >= 0) && (static_cast(b.j_slot_) < slot_v_.size())); + + return &(slot_v_[b.j_slot_]); + } + + if (parent_.get()) { + return parent_->lookup_slot(vname); + } + + return nullptr; + } + + gp * + LocalEnv::establish_var(bp v) + { + assert(v); + + throw std::runtime_error(tostr("LocalEnv::establish_var:" + " inserting new variables not supported for LocalEnv", + xtag("v.name", v->name()))); + } + + TaggedPtr + LocalEnv::self_tp() const + { + return Reflect::make_tp(const_cast(this)); + } + + void + LocalEnv::display(std::ostream & os) const + { + os << ""; + } + + std::size_t + LocalEnv::_shallow_size() const + { + std::size_t retval = sizeof(LocalEnv); + + retval += gc::IAlloc::with_padding(slot_array_size(slot_v_.size())); + + return retval; + } + + Object * + LocalEnv::_shallow_copy(gc::IAlloc * mm) const + { + Cpof cpof(mm, this); + + size_t z = this->size(); + + LocalEnv * copy = new (cpof) LocalEnv(cpof.mm_, parent_, symtab_, z); + + void * v_dest = copy->slot_v_.v_; + + if (slot_v_.v_) { + ::memcpy(v_dest, slot_v_.v_, slot_array_size(z)); + } + +#ifdef OBSOLETE + for (size_t i = 0, n = n_slot_; i < n; ++i) { + copy->v_[i] = v_[i]; + } +#endif + + return copy; + } + + std::size_t + LocalEnv::_forward_children(gc::IAlloc * gc) + { + static_assert(decltype(symtab_)::is_gc_ptr == false); + + Object::_forward_inplace(parent_, gc); + // Object::_forward_inplace(symtab_); // not a gp yet + for (std::size_t i = 0, n = slot_v_.size(); i < n; ++i) { + Object::_forward_inplace((*this)[i], gc); + } + + return _shallow_size(); + } + + void + LocalEnv::reflect_self() + { + StructReflector sr; + + if (sr.is_incomplete()) { + /* reflect CVector> + * + * note: placement here works b/c CVector not used anywhere else + */ + using VectorType = obj::CVector>; + + /* custom reflection for array of Object pointers. + * Can use StlVectorTdx here, treating CVector as a vector + * via .size() and .operator[] members + */ + std::unique_ptr tdx1 + = std::make_unique>(); + TypeDescrW td1 + = EstablishTypeDescr::establish(); + td1->assign_tdextra(Reflect::get_final_invoker(), + std::move(tdx1)); + + REFLECT_MEMBER(sr, parent); + REFLECT_MEMBER(sr, slot_v); + } + } + } /*namespace scm*/ +} /*namespace xo*/ + +/* end LocalEnv.cpp */ diff --git a/xo-interpreter/src/interpreter/Schematika.cpp b/xo-interpreter/src/interpreter/Schematika.cpp new file mode 100644 index 00000000..039b2e67 --- /dev/null +++ b/xo-interpreter/src/interpreter/Schematika.cpp @@ -0,0 +1,284 @@ +/** @file Schematika.cpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#include "Schematika.hpp" +#include "VirtualSchematikaMachine.hpp" +#include "BuiltinPrimitives.hpp" +#include "GlobalEnv.hpp" +#include "xo/reader/reader.hpp" +#include +#include +#include // for STDIN_FILENO on OSX + +namespace xo { + using xo::gc::IAlloc; + using xo::gc::GC; + using xo::print::ppconfig; + using xo::print::ppstate_standalone; + using replxx::Replxx; + using namespace std; + + namespace scm { + + class Schematika::Impl { + public: + /** note: choosing to have Schemtika::Impl + * rather than VirtualSchematikaMachine to own allocator + * to preserve option to share it + **/ + Impl(const Config & config, up mm, gp toplevel_env); + ~Impl(); + + /** create instance + allocator **/ + static up make(const Config & cfg); + + /** borrow calling thread to run interactive read-eval-print loop; + * input from stdin, output to stdout. + **/ + void interactive_repl(); + + void welcome(std::ostream & os); + + /** get one line of input. prompt if @p interactive, + * with prompt depending on @p parser_stack_size. + * Use @p rx to perform line editing (when @p interactive). + * Store completed line in @p input. + **/ + bool replxx_getline(bool interactive, + std::size_t parser_stack_size, + replxx::Replxx & rx, + std::string & input); + + private: + /** configuration **/ + Config config_; + /** ownership for memory allocator / garbage collector; + * @ref vsm_ holds naked pointer, so this could in principle be nullptr + * in case want to maintain allocator ownership from outside. + * + * note: must appear before @ref vsm_, so latter gets destroyed first + **/ + up mm_; + /** schematika interpreter **/ + VirtualSchematikaMachine vsm_; + }; + + Schematika::Impl::Impl(const Config & config, up mm, gp toplevel_env) : + config_{config}, + mm_{std::move(mm)}, + vsm_{mm_.get(), toplevel_env, config.vsm_log_level_} + { + + } + + Schematika::Impl::~Impl() = default; + + up + Schematika::Impl::make(const Config & cfg) + { + up mm = GC::make(cfg.gc_config_); + rp symtab = GlobalSymtab::make_empty(); + gp env = GlobalEnv::make_empty(mm.get(), symtab); + + /* also see VirtualSchematikaMachineFlyweight::Impl::Impl, + * for BuiltinPrimitives::install_interpreter_conversions() + */ + BuiltinPrimitives::install(mm.get(), env); + + return std::make_unique(cfg, std::move(mm), env); + } + + void + Schematika::Impl::welcome(std::ostream & os) + { + using namespace std; + + os << "read-eval-print loop for schematika expressions" << endl; + os << " ctrl-a/ctrl-e beginning/end of line" << endl; + os << " ctrl-u delete entire line" << endl; + os << " ctrl-k delete to end of line" << endl; + os << " meta- backward delete word" << endl; + os << " |meta-p previous command from history" << endl; + os << " |meta-n next command from history" << endl; + os << " / page through history faster" << endl; + os << " ctrl-s/ctrl-r forward/backward history search" << endl; + os << endl; + } + + // similar helper in exprreplxx.cpp + // + bool + Schematika::Impl::replxx_getline(bool interactive, + std::size_t parser_stack_size, + replxx::Replxx & rx, + std::string & input) + { + using namespace std; + + char const * prompt = ""; + + if (interactive) { + if (parser_stack_size <= 1) + prompt = "> "; + else + prompt = ". "; + } + + /* input_cstr: next line of input from replxx library */ + const char * input_cstr = rx.input(prompt); + + bool retval = (input_cstr != nullptr); + + if (retval) { + /* got new input */ + input = input_cstr; + } + + rx.history_add(input); + + input.push_back('\n'); + + return retval; + } + + void + Schematika::Impl::interactive_repl() + { + scope log(XO_DEBUG(true)); + + using span_type = xo::scm::span; + + bool interactive = isatty(STDIN_FILENO); + + Replxx rx; + rx.set_max_history_size(config_.history_size); + rx.history_load(config_.history_file); + // rx.bind_key_internal(Replxx::KEY::control('p'), "history_previous"); + // rx.bind_key_internal(Replxx::KEY::control('n'), "history_next"); + + reader rdr(vsm_.toplevel_env()->symtab(), config_.debug_flag); + rdr.begin_interactive_session(); + + string input_str; + + bool eof = false; + + span_type input; + std::size_t parser_stack_size = 0; + + if (config_.welcome_flag_) + welcome(cerr); + + while (replxx_getline(interactive, parser_stack_size, rx, input_str)) { + input = span_type::from_string(input_str); + + while (!input.empty()) { + /** + * Three cases here: + * 1. available input is invalid (does not conform to schematika syntax). + * 1a. expr=nullptr + * 1b. consumed reads all available input + * 1c. psz=0 + * 1d. error.is_error(); details including exact location where parsing failed. + * 1e. parser reset to top level. + * 2. available input represents prefix of a possibly-valid expression + * 2a. expr=nullptr; + * 2b. consumed reads all available input + * 2c. psz reflects nesting level after reading available input. + * 2d. error.is_not_an_error() + * 3. available input completes at least one expression + * 3a. expr contains first completed top-level expression + * 3b. consumed reports portion of input up to end of expr + * 3c. psz=0 + * 3d. error.is_not_an_error() + * + * expr :: rp if non-null: the next expression from input + * consumed :: span extent of input read up to next Expression + * psz :: size_t parser stack size + * error :: reader_error error details on parsing failure + **/ + auto [expr, consumed, psz, error] = rdr.read_expr(input, eof); + + if (expr) { + /** configuration for pretty-printing **/ + ppconfig ppc; + ppstate_standalone pps(&cout, 0, &ppc); + + //pps.prettyn(expr); + + // TODO: + auto [ value, scm_error ] = this->vsm_.toplevel_eval(expr); + + if (scm_error.is_error()) { + /* print error */ + + cout << "scm error: " << scm_error.what() << endl; + cout << "top-level expression: " << expr << endl; + } else { + /* print value */ + + cout << "scm result:" << endl; + cout << value << endl; + //pps.pretty(value); + } + + } else if (error.is_error()) { + cout << "parsing error (detected in " << error.src_function() << "): " << endl << endl; + error.report(cout); + + /* discard stashed remainder of input line + * (for nicely-formatted errors) + */ + rdr.reset_to_idle_toplevel(); + break; + } + + input = input.after_prefix(consumed); + parser_stack_size = psz; + } + + /* here: input.empty() or error encountered */ + + cerr << endl; + } + + auto [expr, _1, _2, error] = rdr.read_expr(input, true /*eof*/); + + if (expr) { + ppconfig ppc; + ppstate_standalone pps(&cout, 0, &ppc); + + pps.prettyn>(rp(expr)); + } else if (error.is_error()) { + cout << "parsing error (detected in " << error.src_function() << "): " << endl; + error.report(cout); + } + + rx.history_save("repl_history.txt"); + } + + // ----- Schematika ----- + + Schematika::Schematika(const Config & cfg) : p_impl_{Impl::make(cfg)} + {} + + Schematika::~Schematika() + {} + + Schematika + Schematika::make(const Config & cfg) + { + return Schematika(cfg); + } + + void + Schematika::interactive_repl() + { + p_impl_->interactive_repl(); + } + } +} + +/* end Schematika.cpp */ diff --git a/xo-interpreter/src/interpreter/VirtualSchematikaMachine.cpp b/xo-interpreter/src/interpreter/VirtualSchematikaMachine.cpp new file mode 100644 index 00000000..25f620b0 --- /dev/null +++ b/xo-interpreter/src/interpreter/VirtualSchematikaMachine.cpp @@ -0,0 +1,865 @@ +/** @file VirtualSchematikaMachine.cpp **/ + +#include "VirtualSchematikaMachine.hpp" +#include "VsmInstr.hpp" +#include "BuiltinPrimitives.hpp" +#include "ExpressionBoxed.hpp" +#include "xo/expression/Constant.hpp" +#include "xo/expression/PrimitiveExprInterface.hpp" +#include "xo/expression/DefineExpr.hpp" +#include "xo/expression/AssignExpr.hpp" +#include "xo/expression/Variable.hpp" +#include "xo/expression/IfExpr.hpp" +#include "xo/expression/Sequence.hpp" +#include "xo/expression/Apply.hpp" +#include "xo/object/Procedure.hpp" +#include "xo/object/Primitive.hpp" +#include "xo/object/Integer.hpp" +#include "xo/object/Boolean.hpp" +#include "xo/alloc/GC.hpp" + +/** continue after completing a VSM instruction; + * achieve by jumping to continuation. + **/ +#define VSM_CONTINUE() this->pc_ = this->cont_; return; + +/** report error and terminate VSM execution + **/ +#define VSM_ERROR(msg) report_error(msg); return; + + + +namespace xo { + using xo::gc::GC; + using xo::obj::Procedure; + using xo::obj::Integer; + using xo::obj::Boolean; + + namespace scm { + struct VsmOps { + /** halt virtual scheme machine. + * This will cause innermost run() to return to its caller + **/ + static VsmInstr halt_op; + + /** evaluate an expression. + * - opcode is Opcode::eval + * - expression in register @ref expr_ + **/ + static VsmInstr eval_op; + + /** assign variable after evaluating rhs of a define-expression or assign-expression + * - opcode is Opcode::complete_assign + * - top stack frame contains {lhs, cont} + **/ + static VsmInstr complete_assign_op; + + /** choose branch of if-expr after evaluating test condition. + * - opcode is Opcode::complete_ifexpr + * - top stack frame contains {ifexpr, cont} + **/ + static VsmInstr complete_ifexpr_op; + + /** proceed to next element of sequence-expr. + * - opcode is Opcode::complete_sequence + * - top stack frame contains {seq, next, cont} + */ + static VsmInstr complete_sequence_op; + + /** proceed to next argument in apply-expr + * - opcode is Opcode::eval_collect_args + * - top stack frame contains {apply, targetarg, cont} + */ + static VsmInstr complete_evalargs_op; + + /** call a procedure, where evaluated arguments (including target function) + * are in top stack frame. + * - opcode is Opcode::apply + * - top stack frame contains evaluated arguments. + **/ + static VsmInstr apply_op; + }; + + VsmInstr + VsmOps::halt_op{VsmInstr::Opcode::halt, "halt"}; + + VsmInstr + VsmOps::eval_op{VsmInstr::Opcode::eval, "eval"}; + + VsmInstr + VsmOps::complete_assign_op{VsmInstr::Opcode::complete_assign, "complete-assign"}; + + VsmInstr + VsmOps::complete_ifexpr_op{VsmInstr::Opcode::complete_ifexpr, "complete-ifexpr"}; + + VsmInstr + VsmOps::complete_sequence_op{VsmInstr::Opcode::complete_sequence, "complete-sequence"}; + + VsmInstr + VsmOps::complete_evalargs_op{VsmInstr::Opcode::complete_evalargs, "complete-evalargs"}; + + VsmInstr + VsmOps::apply_op{VsmInstr::Opcode::apply, "apply"}; + + // ----- VirtualSchematikaMachineFlyweight ----- + + VirtualSchematikaMachineFlyweight::VirtualSchematikaMachineFlyweight(gc::IAlloc * mm, + gp env, + log_level ll) : + object_mm_{mm}, + toplevel_env_{env}, + log_level_{ll} + { + } + + // ----- VirtualSchematikaMachine ----- + + VirtualSchematikaMachine::VirtualSchematikaMachine(gc::IAlloc * mm, + gp env, + log_level ll) : flyweight_{mm, env, ll} + { + scope log(XO_DEBUG(true), xtag("env", env), xtag("symtab", env->symtab())); + + this->env_ = env; + + // gc roots + gc::GC * gc = GC::from(mm); + + if (gc) { + assert((gc->gc_in_progress() == false) && "cannot add roots while GC running"); + + gc->add_gc_root_dwim(&env_); + gc->add_gc_root_dwim(&value_); + } else { + // Want to support VSM with arena-allocator-only; + // if only for unit testing. + } + + // TODO: install builtin primitives here + } + + VirtualSchematikaMachine::~VirtualSchematikaMachine() + { + gc::GC * gc = GC::from(flyweight_.object_mm_); + + if (gc) { + assert((gc->gc_in_progress() == false) && "cannot remove roots while GC running"); + + gc->remove_gc_root_dwim(&env_); + gc->remove_gc_root_dwim(&value_); + } else { + // nothing to do in arena-only mode + } + } + + std::pair, + SchematikaError> + VirtualSchematikaMachine::toplevel_eval(bp expr) + { + return this->eval(expr, this->env_); + } + + std::pair, + SchematikaError> + VirtualSchematikaMachine::eval(bp expr, gp env) + { + scope log(XO_DEBUG(true), xtag("env", env), xtag("symtab", env->symtab())); + + this->pc_ = &VsmOps::eval_op; + this->expr_ = expr.promote(); + this->env_ = env; + this->stack_ = nullptr; + this->value_ = nullptr; + this->error_ = SchematikaError(); + this->cont_ = &VsmOps::halt_op; + + this->run(); + + return std::make_pair(this->value_, this->error_); + } + + void + VirtualSchematikaMachine::run() + { + while(pc_) + this->execute_one(); + } + + void + VirtualSchematikaMachine::execute_one() + { + scope log(XO_DEBUG(true)); + log && log(xtag("pc", pc_), xtag("cont", cont_)); + log && log(xtag("stack", stack_)); + + using Opcode = VsmInstr::Opcode; + + switch (pc_->opcode()) { + + case Opcode::halt: + { + this->pc_ = nullptr; + this->cont_ = nullptr; + break; + } + + case Opcode::eval: + { + log && log("Opcode::eval"); + + /* generally speaking: opcode will be 1:1 with extypes */ + + switch (expr_->extype()) { + case exprtype::constant: + log && log("eval -> constant"); + this->eval_constant_op(); + break; + + case exprtype::primitive: + log && log("eval -> primitive"); + this->eval_primitive_op(); + break; + + case exprtype::define: + log && log("eval -> define"); + this->eval_define_op(); + break; + + case exprtype::assign: + log && log("eval -> assign"); + this->eval_assign_op(); + break; + + case exprtype::variable: + log && log("eval -> variable"); + this->eval_variable_op(); + break; + + case exprtype::ifexpr: + log && log("eval -> ifexpr"); + this->eval_ifexpr_op(); + break; + + case exprtype::sequence: + log && log("eval -> sequence"); + this->eval_sequence_op(); + break; + + case exprtype::apply: + log && log("eval -> apply"); + this->eval_apply_op(); + break; + + case exprtype::invalid: + + case exprtype::lambda: + case exprtype::convert: + case exprtype::n_expr: + this->pc_ = nullptr; + this->value_ = nullptr; + this->error_ = SchematikaError(tostr("execute_vsm: not implemented", + xtag("extype", expr_->extype()))); + this->cont_ = nullptr; + break; + } + } + break; + + case Opcode::complete_assign: + this->do_complete_assign_op(); + break; + + case Opcode::complete_ifexpr: + this->do_complete_ifexpr_op(); + break; + + case Opcode::complete_sequence: + this->do_complete_sequence_op(); + break; + + case Opcode::complete_evalargs: + this->do_complete_evalargs_op(); + break; + + case Opcode::apply: + this->apply_op(); + break; + + case Opcode::N_Opcode: + assert(false); + break; + } + } + + void + VirtualSchematikaMachine::report_error(const std::string & err) + { + /* error short-circuits vsm operation */ + + this->pc_ = nullptr; + this->value_ = nullptr; + this->error_ = SchematikaError(err); + this->cont_ = nullptr; + } + + void + VirtualSchematikaMachine::eval_constant_op() + { + using xo::scm::ConstantInterface; + + scope log(XO_DEBUG(true)); + + bp expr = ConstantInterface::from(expr_); + + assert(expr); + + this->value_ = flyweight_.object_converter_.tp_to_object(flyweight_.object_mm_, + expr->value_tp(), + false); + if (this->value_.ptr()) { + log && log("got object: ", xtag("value", value_)); + + VSM_CONTINUE(); + } else { + /* see ObjectConverter::ctor to add more builtin types + * + * generally conversion for a type Foo will appear in Foo.hpp + * see + * xo/object/Boolean.hpp + * xo/object/Integer.hpp + * xo/object/Float.hpp + * xo/object/String.hpp + */ + + VSM_ERROR(tostr("constant_op: unable to convert native value to object", + xtag("id", expr->value_tp().td()->id()), + xtag("short_name", expr->value_tp().td()->short_name()))); + } + } + + void + VirtualSchematikaMachine::eval_primitive_op() + { + using xo::obj::Primitive; + using xo::reflect::TaggedPtr; + + scope log(XO_DEBUG(true)); + + bp expr = PrimitiveExprInterface::from(expr_); + + const gp * slot = env_->lookup_slot(expr->name()); + + if (slot) { + this->value_ = *slot; + this->pc_ = cont_; + } else { + std::string err = tostr("no binding for primitive", xtag("name", expr->name())); + + this->value_ = nullptr; + this->error_ = SchematikaError(err); + + /* note: poor man's exception */ + this->pc_ = nullptr; + this->cont_ = nullptr; + } + } + + void + VirtualSchematikaMachine::eval_define_op() + { + using xo::scm::DefineExpr; + + scope log(XO_DEBUG(true)); + + auto mm = flyweight_.object_mm_; + + bp expr = DefineExpr::from(expr_); + + assert(expr); + assert(env_.get()); + + // note: expecting nested define to have expanded iteself into + // applying a lambda + + // note: establish lhs_var first, to allow for recursion, for example: + // def fact(n: i64) { if (n == 0) then 1; else n * fact(n-1); } + + /** remembers promised variable type **/ + this->env_->establish_var(expr->lhs_variable()); + + /** must promote rp -> gp **/ + gp lhs_0 = ExpressionBoxed::make(mm, expr->lhs_variable()); + + this->pc_ = &VsmOps::eval_op; + this->expr_ = expr->rhs(); + + /* when control arrives at .cont_, will have: + * .value_ -> result of evaluating expr->rhs() + */ + this->stack_ = VsmStackFrame::push1(mm, this->stack_, lhs_0, cont_); + + /* .stack_: + * frame + * [0] = lhs_0 (boxed lhs Variable) + * .. + */ + + this->cont_ = &VsmOps::complete_assign_op; + } + + void + VirtualSchematikaMachine::eval_assign_op() + { + using xo::scm::AssignExpr; + + scope log(XO_DEBUG(true)); + + auto mm = flyweight_.object_mm_; + + bp assign = AssignExpr::from(expr_); + + assert(assign.get()); + assert(env_.get()); + + assert(assign->lhs().get()); + assert(assign->rhs().get()); + + /* verify slot exists, before we evaluate rhs */ + gp * slot = env_->lookup_slot(assign->lhs()->name()); + + if (slot) { + /** must promote rp -> gp **/ + gp lhs = ExpressionBoxed::make(mm, assign->lhs()); + + this->pc_ = &VsmOps::eval_op; + this->expr_ = assign->rhs(); + + /* when control arrives at .cont_, will have: + * .value_ -> result of evaluating assign->rhs() + */ + this->stack_ = VsmStackFrame::push1(mm, this->stack_, lhs, cont_); + + /* .stack_: + * frame + * [0] = lhs (boxed lhs Variable) + * .. + */ + + this->cont_ = &VsmOps::complete_assign_op; + } else { + std::string err = tostr("no binding for lhs of assignment", xtag("name", assign->lhs()->name())); + + this->value_ = nullptr; + this->error_ = SchematikaError(err); + + /* note: poor man's exception */ + this->pc_ = nullptr; + this->cont_ = nullptr; + } + } + + void + VirtualSchematikaMachine::do_complete_assign_op() + { + scope log(XO_DEBUG(true)); + + /* + * - value: contains result of evaluating rhs of define + * - stack: top frame has 1 slot, holds variable to receive assignment + */ + assert(value_.get()); + assert(stack_.get()); + assert(env_.get()); + + gp sp0 = this->stack_; + + bp var = Variable::from(ExpressionBoxed::from((*sp0)[0])->contents()); + + assert(var.get()); + + gp * slot = this->env_->establish_var(var); + assert(slot); + + *slot = this->value_; + + //this->value_ = this->value_; // preserve value from rhs of defexpr + + this->stack_ = sp0->parent(); + this->pc_ = this->cont_ = sp0->continuation(); + } + + void + VirtualSchematikaMachine::eval_variable_op() + { + using xo::scm::Variable; + + scope log(XO_DEBUG(true)); + + bp var = Variable::from(expr_); + + assert(var.get()); + assert(env_.get()); + + const gp * slot = env_->lookup_slot(var->name()); + + if (slot) { + this->value_ = *slot; + this->pc_ = cont_; + } else { + /* Unknown variable error will often be recognized in expression parser, + * in such cases this path won't be used. + * + * In interactive environment will need some kind of support for modifying + * code (e.g. replacing top-level functions/variables), and in particular, + * replacements may have different type signature. + * It's possible that allowing for such replacements winds up giving up + * typesafety guarantees. In that case this path may get activated after + * all. + */ + + std::string err = tostr("no binding for variable", xtag("name", var->name())); + + this->value_ = nullptr; + this->error_ = SchematikaError(err); + + /* note: poor man's exception */ + this->pc_ = nullptr; + this->cont_ = nullptr; + } + } + + void + VirtualSchematikaMachine::eval_ifexpr_op() + { + using xo::scm::IfExpr; + + scope log(XO_DEBUG(true)); + + gc::IAlloc * mm = flyweight_.object_mm_; + + /** must promote bp -> gp **/ + gp ifexpr_boxed = ExpressionBoxed::make(mm, expr_); + bp ifexpr = IfExpr::from(expr_); + + assert(ifexpr.get()); + assert(env_.get()); + + this->pc_ = &VsmOps::eval_op; + this->expr_ = ifexpr->test(); + + + /* when control arrives at .cont_ will have: + * .value_ -> result of evaluating ifexpr->test() + */ + this->stack_ = VsmStackFrame::push1(mm, this->stack_, ifexpr_boxed, cont_); + + /* .stack_: + * frame + * [0] = ifexpr (boxed expression) + */ + + this->cont_ = &VsmOps::complete_ifexpr_op; + } + + void + VirtualSchematikaMachine::do_complete_ifexpr_op() + { + using xo::scm::IfExpr; + + scope log(XO_DEBUG(true)); + + /* + * - value: contains result of evaluating test condition of if-expr + * - stack: top frame has 1 slot, holds (boxed) if-expr itself + */ + assert(value_.get()); + assert(stack_.get()); + assert(env_.get()); + + gp test_value = gp::from(value_); + + if (test_value.get()) { + gp sp0 = this->stack_; + + bp ifexpr = IfExpr::from(ExpressionBoxed::from((*sp0)[0])->contents()); + + assert(ifexpr.get()); + + this->pc_ = &VsmOps::eval_op; + + if (test_value->value()) { + this->expr_ = ifexpr->when_true(); + } else { + if (ifexpr->when_false()) { + this->expr_ = ifexpr->when_false(); + } else { + /* 1-sided if-expr; evaluate to false */ + this->expr_ = Constant::make(false); + } + } + + this->stack_ = sp0->parent(); + this->cont_ = sp0->continuation(); + } else { + std::string err = tostr("expect boolean value for result of if-expr test", xtag("value", test_value)); + + this->value_ = nullptr; + this->error_ = SchematikaError(err); + + /* note: poor man's exception */ + this->pc_ = nullptr; + this->cont_ = nullptr; + } + } + + void + VirtualSchematikaMachine::eval_sequence_op() + { + using xo::scm::Sequence; + + scope log(XO_DEBUG(true)); + + gc::IAlloc * mm = flyweight_.object_mm_; + + /** must promote bp -> gp **/ + gp seq_boxed = ExpressionBoxed::make(mm, expr_); + bp seq = Sequence::from(expr_); + + assert(seq.get()); + assert(env_.get()); + + this->pc_ = &VsmOps::eval_op; + + if (seq->size() == 0) { + /* for 0-size sequence, invent an expression */ + this->expr_ = Constant::make(false); + } else { + this->expr_ = (*seq)[0]; + } + + if (seq->size() > 1) { + /* remainder */ + + gp next = Integer::make(mm, 1); + + /* when control arrives at .cont_ will have: + * .value_ -> result of evaluating last expr in seq + */ + this->stack_ = VsmStackFrame::push2(mm, stack_, seq_boxed, next, cont_); + + /* .stack_: + * frame + * [0] = seq (boxed sequence) + * [1] = next (index of next seq member to evaluate) + * .. + */ + + this->cont_ = &VsmOps::complete_sequence_op; + } else { + /* sequence completes when expr_ evaluated + * -> proceed with o.g. cont_ + */ + } + } + + void + VirtualSchematikaMachine::do_complete_sequence_op() + { + using xo::scm::Sequence; + + scope log(XO_DEBUG(true)); + + /* - stack: top frame has 2 slots: + * [0] : seq (boxed Sequence) + * [1] : next (index of next seq element to eval + */ + + assert(value_.get()); + assert(stack_.get()); + + gp sp0 = this->stack_; + + assert(sp0->size() == 2); + + bp seq = Sequence::from(ExpressionBoxed::from((*sp0)[0])->contents()); + gp next_obj = Integer::from((*sp0)[1]); + size_t i_next = next_obj->value(); + + assert(i_next < seq->size()); + + this->pc_ = &VsmOps::eval_op; + this->expr_ = (*seq)[i_next]; + + if (i_next + 1 == seq->size()) { + /* last member of sequence -> tail call optimization */ + this->stack_ = sp0->parent(); + this->cont_ = sp0->continuation(); + } else { + /* we can modify next_obj in place, + * since it's unique to frame sp0 + */ + next_obj->assign_value(i_next + 1); + this->cont_ = &VsmOps::complete_sequence_op; + } + } + + void + VirtualSchematikaMachine::eval_apply_op() + { + /* strategy: + * 1. calling sequence will involve two stack frames. + * 1a. the outer frame will hold 'final evaluated arguments' + * to the called function. When control transfers to that + * function, this frame will be at the top of stack_ + * 1b. innert frame will be used by eval_apply_op() and + * helper do_eval_collect_args() to evaluate function + * arguments, and populate the outer frame. + */ + + using xo::scm::Apply; + + scope log(XO_DEBUG(true)); + + gc::IAlloc * mm = flyweight_.object_mm_; + + /** must promote bp -> gp **/ + gp apply_boxed = ExpressionBoxed::make(mm, expr_); + bp apply = Apply::from(expr_); + + assert(apply.get()); + + size_t n = apply->n_arg() + 1; + + /* reminder: argument 0 refers to the function being called */ + gp targetarg = Integer::make(mm, 0); + + /* outer frame */ + gp argstack = VsmStackFrame::make(mm, stack_, n, cont_); + + /* scratch frame during call sequence. + * probably collect->cont_ will not be used? + */ + gp collect = VsmStackFrame::push2(mm, + argstack, + apply_boxed, + targetarg, + &VsmOps::complete_evalargs_op); + + this->pc_ = &VsmOps::eval_op; + this->expr_ = apply->fn(); + this->stack_ = collect; + this->cont_ = &VsmOps::complete_evalargs_op; + } + + void + VirtualSchematikaMachine::do_complete_evalargs_op() + { + using xo::scm::Apply; + + scope log(XO_DEBUG(true)); + + /* - stack: top frame has 2 slots + * [0] : apply (boxed Apply) + * [1] : targetarg index of next evaluated argument to deliver. + * (to corresponding slot in 2nd frame) + * - 2nd frame has n slots, where n = #of arguments at this site + * [0] : actual #0 + * .. + * [targetarg-1] : actual #{targetarg-1} + */ + + assert(value_.get()); + assert(stack_.get()); + + gp sp0 = this->stack_; + + assert(sp0.get()); + assert(sp0->size() == 2); + + bp apply = Apply::from(ExpressionBoxed::from((*sp0)[0])->contents()); + assert(apply.get()); + gp targetarg_obj = Integer::from((*stack_)[1]); + size_t targetarg = targetarg_obj->value(); + + /* note: apply->n_arg() doesn't count function itself */ + assert(targetarg < apply->n_arg() + 1); + + gp argstack = sp0->parent(); + + assert(argstack.get()); + + /* storing actual parameter in its correct position in call stackframe */ + (*argstack)[targetarg] = value_; + + ++targetarg; + + if (targetarg < apply->n_arg() + 1) { + /* + * arguments 0 .. #targetarg-1 already present in argstack + * arguments #targetarg .. #n still need to be evaluated + */ + + /* ok to update in place, since Integer in sp0 is unique */ + targetarg_obj->assign_value(targetarg); + + rp targetexpr = apply->lookup_arg(targetarg - 1); + + this->pc_ = &VsmOps::eval_op; + this->expr_ = targetexpr; + assert(this->stack_.get() == sp0.get()); + this->cont_ = &VsmOps::complete_evalargs_op; + } else { + /* all args evaluated: proceed to invoke evaluated function */ + + this->pc_ = &VsmOps::apply_op; + this->expr_ = nullptr; + this->stack_ = argstack; + /* unnecessary - will actually be set by apply_op() */ + this->cont_ = argstack->continuation(); + } + } + + void + VirtualSchematikaMachine::apply_op() + { + scope log(XO_DEBUG(true)); + + auto mm = flyweight_.object_mm_; + + // NOTE: Closures will have special handling. + // Could alternatively forward the whole problem + // (along with VSM state) to procedure implementation + + /* stack: top frame has n slots for procedure with n canonical args */ + + gp sp0 = stack_; + + assert(sp0->size() > 0); + + gp fn = Procedure::from((*sp0)[0]); + + if (fn->n_args() + 1 != sp0->size()) { + throw std::runtime_error(tostr("VirtualSchematikaMachine::apply_op:" + " argument mismatch in apply" + ": k arguments supplied where n expected", + xtag("k", sp0->size() - 1), + xtag("n", fn->n_args()))); + } + + /* todo: + * check function signature? + * should have been guaranteed by expression parser, + * but complications in interactive session when variables redefined. + */ + + gp retval = fn->apply_nocheck(mm, sp0->argv()); + + this->pc_ = this->cont_; + this->stack_ = sp0->parent(); + this->value_ = retval; + } + + } /*namespace scm*/ +} /*namespace xo*/ + +/* end VirtualSchematikaMachine.cpp */ diff --git a/xo-interpreter/src/interpreter/VsmInstr.cpp b/xo-interpreter/src/interpreter/VsmInstr.cpp new file mode 100644 index 00000000..3ca138d9 --- /dev/null +++ b/xo-interpreter/src/interpreter/VsmInstr.cpp @@ -0,0 +1,16 @@ +/** @file VsmInstr.cpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#include "VsmInstr.hpp" + +namespace xo { + namespace scm { + VsmInstr::VsmInstr(Opcode opcode, + std::string_view name) : opcode_{opcode}, name_{name} + {} + } +} + +/* end VsmInstr.cpp */ diff --git a/xo-interpreter/src/interpreter/VsmStackFrame.cpp b/xo-interpreter/src/interpreter/VsmStackFrame.cpp new file mode 100644 index 00000000..9bca575e --- /dev/null +++ b/xo-interpreter/src/interpreter/VsmStackFrame.cpp @@ -0,0 +1,177 @@ +/** @file VsmStackFrame.cpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#include "VsmStackFrame.hpp" +#include "xo/reflect/Reflect.hpp" +#include "xo/reflect/StructReflector.hpp" + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::StructReflector; + using xo::reflect::TypeDescrW; + using xo::reflect::TaggedPtr; + using xo::reflect::TypeDescrExtra; + using xo::reflect::EstablishTypeDescr; + using xo::reflect::StlVectorTdx; + + namespace scm { + namespace { + // TOOD: move into CVector + + std::size_t + slot_array_size(std::size_t n) { + return n * sizeof(gp); + } + } + + VsmStackFrame::VsmStackFrame(gc::IAlloc * mm, + gp p, + std::size_t n, + const VsmInstr * cont) : parent_{p}, + slot_v_{mm, n}, + cont_{cont} + {} + + gp + VsmStackFrame::make(gc::IAlloc * mm, + gp p, + std::size_t n, + const VsmInstr * cont) + { + gp retval = new (MMPtr(mm)) VsmStackFrame(mm, p, n, cont); + + for (std::size_t i = 0; i < n; ++i) + (*retval)[i] = nullptr; + + return retval; + } + + gp + VsmStackFrame::push1(gc::IAlloc * mm, + gp p, + gp s0, + const VsmInstr * cont) + { + gp retval = new (MMPtr(mm)) VsmStackFrame(mm, p, 1, cont); + + (*retval)[0] = s0; + + return retval; + } + + gp + VsmStackFrame::push2(gc::IAlloc * mm, + gp p, + gp s0, + gp s1, + const VsmInstr * cont) + { + gp retval = new (MMPtr(mm)) VsmStackFrame(mm, p, 2, cont); + + (*retval)[0] = s0; + (*retval)[1] = s1; + + return retval; + } + + TaggedPtr + VsmStackFrame::self_tp() const + { + return Reflect::make_tp(const_cast(this)); + } + + void + VsmStackFrame::display(std::ostream & os) const + { + os << ""; + } + + std::size_t + VsmStackFrame::_shallow_size() const + { + std::size_t retval = sizeof(VsmStackFrame); + + retval += gc::IAlloc::with_padding(slot_array_size(slot_v_.size())); + + return retval; + } + + Object * + VsmStackFrame::_shallow_copy(gc::IAlloc * mm) const + { + Cpof cpof(mm, this); + + size_t n = this->size(); + + VsmStackFrame * copy = new (cpof) VsmStackFrame(cpof.mm_, parent_, n, cont_); + + void * v_dest = copy->slot_v_.v_; + + if (slot_v_.v_) { + ::memcpy(v_dest, slot_v_.v_, slot_array_size(n)); + } + +#ifdef OBSOLETE + for (size_t i = 0, n = n_slot_; i < n; ++i) { + copy->v_[i] = v_[i]; + } +#endif + return copy; + } + + std::size_t + VsmStackFrame::_forward_children(gc::IAlloc * gc) + { + Object::_forward_inplace(parent_, gc); + + for (std::size_t i = 0, n = slot_v_.size(); i < n; ++i) { + Object::_forward_inplace((*this)[i], gc); + } + + return _shallow_size(); + } + + void + VsmStackFrame::reflect_self() + { + StructReflector sr; + + if (sr.is_incomplete() ) { + /* reflect CVector>. + * duplicates similar code in LocalEnv::reflect_self() + */ + using VectorType = obj::CVector>; + + /* custom reflection for array of Object pointers. + * Can use StlVectorTdx here, treating CVector as a vector + * via .size() and .operator[] members + */ + std::unique_ptr tdx1 + = std::make_unique>(); + TypeDescrW td1 + = EstablishTypeDescr::establish(); + td1->assign_tdextra(Reflect::get_final_invoker(), + std::move(tdx1)); + + REFLECT_MEMBER(sr, parent); + REFLECT_MEMBER(sr, slot_v); + } + } + } /*namespace scm*/ +} /*namespace xo*/ + +/* end VsmStackFrame.cpp */ diff --git a/xo-interpreter/src/interpreter/init_interpreter.cpp b/xo-interpreter/src/interpreter/init_interpreter.cpp new file mode 100644 index 00000000..eaa0bffe --- /dev/null +++ b/xo-interpreter/src/interpreter/init_interpreter.cpp @@ -0,0 +1,27 @@ +/** @file init_interpreter.cpp + * + * author: Roland Conybeare, Nov 2025 + */ + +#include "init_interpreter.hpp" +#include "LocalEnv.hpp" +#include "xo/subsys/Subsystem.hpp" + +namespace xo { + using xo::scm::LocalEnv; + + void + InitSubsys::init() + { + LocalEnv::reflect_self(); + } + + InitEvidence + InitSubsys::require() + { + return Subsystem::provide("interpreter", &init); + } + +} /*namespace xo*/ + +/* end init_interpreter.cpp */ diff --git a/xo-interpreter/utest/CMakeLists.txt b/xo-interpreter/utest/CMakeLists.txt new file mode 100644 index 00000000..01cef051 --- /dev/null +++ b/xo-interpreter/utest/CMakeLists.txt @@ -0,0 +1,12 @@ +# build unittest interpreter/utest + +set(UTEST_EXE utest.interpreter) +set(UTEST_SRCS + interpreter_utest_main.cpp + LocalEnv.test.cpp +) + +xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) +xo_self_dependency(${UTEST_EXE} xo_interpreter) +xo_dependency(${UTEST_EXE} xo_object) +xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) diff --git a/xo-interpreter/utest/LocalEnv.test.cpp b/xo-interpreter/utest/LocalEnv.test.cpp new file mode 100644 index 00000000..084d5a4d --- /dev/null +++ b/xo-interpreter/utest/LocalEnv.test.cpp @@ -0,0 +1,134 @@ +/** @file LocalEnv.test.cpp **/ + +#include "xo/interpreter/init_interpreter.hpp" +#include "xo/interpreter/LocalEnv.hpp" +#include "xo/object/Integer.hpp" +#include "xo/alloc/GC.hpp" +#include +#include +#include + +namespace xo { + using xo::scm::LocalEnv; + using xo::obj::Integer; + using xo::gc::GC; + using xo::gc::ArenaAlloc; + using xo::gc::generation; + using xo::gc::generation_result; + using xo::reflect::TaggedPtr; + + namespace ut { + static InitEvidence s_init = (InitSubsys::require()); + + namespace { + struct Testcase_LocalEnv { + Testcase_LocalEnv(const std::vector & contents) : contents_{contents} {} + + /* build xo::obj::Integer for each contents_[i], store in F[i] for new LocalEnv F */ + std::vector contents_; + }; + + std::vector + s_testcase_v = { + Testcase_LocalEnv({}), + Testcase_LocalEnv({}), + Testcase_LocalEnv({111}), + Testcase_LocalEnv({111, 222}), + }; + } + + TEST_CASE("LocalEnv", "[LocalEnv][interpreter]") + { + Subsystem::initialize_all(); + + constexpr bool c_debug_flag = false; + + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + scope log(XO_DEBUG(c_debug_flag), xtag("test", "LocalEnv2"), xtag("i_tc", i_tc)); + + const Testcase_LocalEnv & tc = s_testcase_v[i_tc]; + + up alloc = ArenaAlloc::make("utest", 16384, c_debug_flag); + REQUIRE(alloc.get()); + Object::mm = alloc.get(); + + std::size_t n = tc.contents_.size(); + gp frame = LocalEnv::make(alloc.get(), nullptr /*parent*/, nullptr /*symtab*/, n); + + TaggedPtr tp = frame->self_tp(); + + REQUIRE(tp.is_struct()); + } + } + + TEST_CASE("LocalEnv2", "[LocalEnv][gc][interpreter]") + { + Subsystem::initialize_all(); + + constexpr bool c_debug_flag = false; + + try { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + scope log(XO_DEBUG(c_debug_flag), xtag("test", "LocalEnv2"), xtag("i_tc", i_tc)); + + const Testcase_LocalEnv & tc = s_testcase_v[i_tc]; + + up gc = GC::make( + {.initial_nursery_z_ = 16384, + .initial_tenured_z_ = 32768, + .incr_gc_threshold_ = 4096, + .full_gc_threshold_ = 4096, + .object_stats_flag_ = true, + .debug_flag_ = c_debug_flag, + }); + + REQUIRE(gc.get()); + + /* use gc for all Object allocs */ + GC * mm = gc.get(); + Object::mm = mm; + + std::size_t n = tc.contents_.size(); + + gp x = Integer::make(gc.get(), 42); + gc->add_gc_root(reinterpret_cast(&x)); + REQUIRE(gc->tospace_generation_of(x.ptr()) == generation_result::nursery); + + gp frame = LocalEnv::make(gc.get(), nullptr /*parent*/, nullptr /*symtab*/, n); + LocalEnv ** frame_pp = frame.ptr_address(); + gc->add_gc_root(reinterpret_cast(frame_pp)); + + /* verifying allocated in N1 */ + REQUIRE(gc->tospace_generation_of(frame.ptr()) == generation_result::nursery); + + for (std::size_t i = 0; i < n; ++i) + (*frame)[i] = Integer::make(mm, tc.contents_.at(i)); + + std::size_t expected_alloc_z = frame->_shallow_size(); + REQUIRE(expected_alloc_z >= sizeof(LocalEnv) + n * sizeof(gp)); + + gc->request_gc(generation::nursery); // <<<<<<<<< GC here <<<<<<<<< + + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + + /* verify Integer x preserved across gc */ + REQUIRE(gc->tospace_generation_of(x.ptr()) == generation_result::nursery); + + /* verify LocalEnv preserved across gc */ + REQUIRE(gc->tospace_generation_of(frame.ptr()) == generation_result::nursery); + REQUIRE(frame->size() == n); + for (std::size_t i = 0; i < n; ++i) { + //REQUIRE(Integer::from(frame->lookup(i)).ptr()); + //REQUIRE(Integer::from(frame->lookup(i))->value() == tc.contents_.at(i)); + } + } + } catch (std::exception & ex) { + std::cerr << "exception: " << ex.what() << std::endl; + REQUIRE(false); + } + } + } +} + +/* end LocalEnv.test.cpp */ diff --git a/xo-interpreter/utest/interpreter_utest_main.cpp b/xo-interpreter/utest/interpreter_utest_main.cpp new file mode 100644 index 00000000..e385f9b4 --- /dev/null +++ b/xo-interpreter/utest/interpreter_utest_main.cpp @@ -0,0 +1,6 @@ +/** @file interpreter_utest_main.cpp **/ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end interpreter_utest_main.cpp */