diff --git a/xo-expression/.gitignore b/xo-expression/.gitignore new file mode 100644 index 00000000..f3b23fc3 --- /dev/null +++ b/xo-expression/.gitignore @@ -0,0 +1,8 @@ +# emacs projectile config +.projectile +# clangd working space (see emacs+lsp) +.cache +# typical cmake build directory (source-tree-nephew) +.build* +# symlink to builddir/compile_commands.json; should be set manually in dev sandbox +compile_commands.json diff --git a/xo-expression/.gitrepo b/xo-expression/.gitrepo new file mode 100644 index 00000000..94f3cd20 --- /dev/null +++ b/xo-expression/.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-expression.git + branch = main + commit = fbc5b6192ee63c91e24601e20f693fac0a8fa039 + parent = 2b0859f3390bc47da9a4348a69f0070fa4269b4d + method = merge + cmdver = 0.4.9 diff --git a/xo-expression/CMakeLists.txt b/xo-expression/CMakeLists.txt new file mode 100644 index 00000000..012f1e65 --- /dev/null +++ b/xo-expression/CMakeLists.txt @@ -0,0 +1,42 @@ +# expression/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo_expression 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}) + +# ---------------------------------------------------------------- + +# must complete definition of expression lib before configuring examples +add_subdirectory(src/expression) + +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- + +add_subdirectory(example) +add_subdirectory(utest) + +# ---------------------------------------------------------------- + +if (XO_ENABLE_EXAMPLES) + install(TARGETS xo_expression_ex1 DESTINATION bin/xo/example) +endif() + +# ---------------------------------------------------------------- +# docs targets depend on all the other library/utest targets +# +#add_subdirectory(docs) + +# end CMakeLists.txt diff --git a/xo-expression/LESSONS b/xo-expression/LESSONS new file mode 100644 index 00000000..c719f02e --- /dev/null +++ b/xo-expression/LESSONS @@ -0,0 +1,9 @@ +* want reflection to work without requiring std::type_info, + so that in xo-expression we can create function types that + C++ compiler has never encountered. + +** This means we need the ability to construct the canonical name + for a type without c++ compiler's assistance. + +** need lookup using function ingredients (return types + arg types + noexcept) + to intern lambda types diff --git a/xo-expression/LICENSE b/xo-expression/LICENSE new file mode 100644 index 00000000..cae3cb5d --- /dev/null +++ b/xo-expression/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2024 Roland Conybeare , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of +external contributions to this project including patches, pull requests, etc. diff --git a/xo-expression/README.md b/xo-expression/README.md new file mode 100644 index 00000000..151c0493 --- /dev/null +++ b/xo-expression/README.md @@ -0,0 +1,63 @@ +# xo-expression library + +A library for representing abstract syntax trees for EGAD (a small expression-based language). +See also +- [github/Rconybea/xo-jit](https://github.com/Rconybea/xo-jit) + EGAD code generation via LLVM +- [github/Rconybea/xo-pyexpression](https://github.com/Rconybea/xo-pyexpression) + build EGAD expressions from a python session +- [github/Rconybea/xo-pyjit](https://github.com/Rconybea/xo-pyjit) + compile + run EGAD expressions from a python session + +## Getting Started + +### build + install `xo-cmake` dependency + +- [github/Rconybea/xo-cmake](https://github.com/Rconybea/xo-cmake) + +Installs a few cmake ingredients, along with a build assistant `xo-build` for XO projects such as this one. + +### build + install other XO dependencies +``` +$ xo-build --clone --configure --build --install xo-indentlog +$ xo-build --clone --configure --build --install xo-refnct +$ xo-build --clone --configure --build --install xo-subsys +$ xo-build --clone --configure --build --install xo-reflect +``` + +Note: can use `xo-build -n` to dry-run here + +### copy `xo-expression` repository locally +``` +$ xo-build --clone xo-expression +``` + +or eqivalently +``` +$ git clone git@github.com:Rconybea/xo-expression.git +``` + +### build + install xo-expression +``` +$ xo-build --configure --build --install xo-expression +``` + +or equivalently: +``` +$ PREFIX=/usr/local # or wherever you prefer +$ cmake -DCMAKE_INSTALL_PREFIX=${PREFIX} -S xo-expression -B xo-jit/.build +$ cmake --build xo-expression/.build +$ cmake --install xo-expression/.build +``` + +### build for unit test coverage +``` +$ cmake -DCMAKE_BUILD_TYPE=coverage -DCMAKE_INSTALL_PREFIX=$PREFIX xo-expression/.build-ccov +$ cmake --build xo-expression/.build-ccov +``` + +### LSP support +``` +$ cd xo-expression +$ ln -s build/compile_commands.json # lsp will look for compile_commands.json in the root of the source tree +``` diff --git a/xo-expression/TODO b/xo-expression/TODO new file mode 100644 index 00000000..c2a91ce8 --- /dev/null +++ b/xo-expression/TODO @@ -0,0 +1,3 @@ +- In xo-reflect, TypeDescr objects are immortal. + In original context rationale was recording information for already-established C++ types. + Maybe want to revisit that choice when associating TypeDescr objects with expressions? diff --git a/xo-expression/cmake/xo-bootstrap-macros.cmake b/xo-expression/cmake/xo-bootstrap-macros.cmake new file mode 100755 index 00000000..aba31169 --- /dev/null +++ b/xo-expression/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------- +# 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 (NOT XO_SUBMODULE_BUILD) + 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-expression/cmake/xo_expressionConfig.cmake.in b/xo-expression/cmake/xo_expressionConfig.cmake.in new file mode 100644 index 00000000..3c6d3b16 --- /dev/null +++ b/xo-expression/cmake/xo_expressionConfig.cmake.in @@ -0,0 +1,8 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(reflect) +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-expression/doc/notes.rst b/xo-expression/doc/notes.rst new file mode 100644 index 00000000..c27ffcbb --- /dev/null +++ b/xo-expression/doc/notes.rst @@ -0,0 +1,16 @@ +Random notes. + +Type inference. + +Expressions represent parsed abstract syntax trees. +We use the same data structure to represent syntax trees both before and after +type inference. + +We record each Expression's type using a type_ref instance. +A type_ref does two things: +1. gives the expression's type a unique name +2. records expression's concrete type once type infrerence has completed. + +Type inference resolves each Expression's type to a concrete TypeDescr. +An Expression tree can't be compiled unless all reachable Expression nodes +are resolved to concrete types, i.e. type inference has completed successfully. diff --git a/xo-expression/example/CMakeLists.txt b/xo-expression/example/CMakeLists.txt new file mode 100644 index 00000000..4151ec21 --- /dev/null +++ b/xo-expression/example/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(ex1) diff --git a/xo-expression/example/ex1/CMakeLists.txt b/xo-expression/example/ex1/CMakeLists.txt new file mode 100644 index 00000000..d7e8b3b6 --- /dev/null +++ b/xo-expression/example/ex1/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-expression/example/ex1/CMakeLists.txt + +set(SELF_EXE xo_expression_ex1) +set(SELF_SRCS ex1.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_dependency(${SELF_EXE} xo_expression) + xo_dependency(${SELF_EXE} refcnt) +endif() + +# end CMakeLists.txt diff --git a/xo-expression/example/ex1/ex1.cpp b/xo-expression/example/ex1/ex1.cpp new file mode 100644 index 00000000..a8e6e72e --- /dev/null +++ b/xo-expression/example/ex1/ex1.cpp @@ -0,0 +1,47 @@ +/** @file ex1.cpp **/ + +#include "xo/expression/Constant.hpp" +#include "xo/expression/PrimitiveExpr.hpp" +#include "xo/expression/llvmintrinsic.hpp" +#include +#include +#include + +// address of &sqrt ambiguous on osx/clang +// (perhaps it's a template..?) +// +double xo_sqrt(double x) { + return sqrt(x); +} + +int +main() { + using xo::scm::make_constant; + using xo::scm::make_primitive; + using xo::scm::llvmintrinsic; + using std::cout; + using std::endl; + + { + auto expr = make_constant(7); + } + + { + auto expr = make_primitive("sqrt", + &xo_sqrt, + false /*!explicit_symbol_def*/, + llvmintrinsic::fp_sqrt); + + auto expr_td = expr->value_td(); + + cout << "expr_td: " << expr_td->short_name() << endl; + cout << "expr_td->is_function(): " << expr_td->is_function() << endl; + cout << "expr_td->fn_retval(): " << expr_td->fn_retval()->short_name() << endl; + cout << "expr_td->n_fn_arg(): " << expr_td->n_fn_arg() << endl; + for (uint32_t i = 0; i < expr_td->n_fn_arg(); ++i) + cout << "expr_td->fn_arg(" << i << "): " << expr_td->fn_arg(i)->short_name() << endl; + cout << "expr_td->fn_is_noexcept(): " << expr_td->fn_is_noexcept() << endl; + } +} + +/** end ex1.cpp **/ diff --git a/xo-expression/include/xo/expression/Apply.hpp b/xo-expression/include/xo/expression/Apply.hpp new file mode 100644 index 00000000..7dd66a7e --- /dev/null +++ b/xo-expression/include/xo/expression/Apply.hpp @@ -0,0 +1,220 @@ +/** @file Apply.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" + +//#include + +namespace xo { + namespace scm { + + /** @class Apply + * @brief syntax for a function call. + * + * In general we don't know function to be invoked + * until runtime, depending on the nature of Expression. + **/ + class Apply : public Expression { + public: + using TypeDescr = xo::reflect::TypeDescr; + + public: + /** create new apply-expression instance + **/ + static rp make(const rp & fn, + const std::vector> & argv); + + /** create apply-expression to compare two 64-bit integers **/ + static rp make_cmp_eq_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression to compare two 64-bit integers **/ + static rp make_cmp_ne_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression for less-than comparison of two 64-bit integers **/ + static rp make_cmp_lt_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression for less-than-or-equal comparison of two 64-bit integers **/ + static rp make_cmp_le_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression for greater-than comparison of two 64-bit integers **/ + static rp make_cmp_gt_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression for greater-than-or-equal comparison of two 64-bit integers **/ + static rp make_cmp_ge_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression to add two 64-bit integers **/ + static rp make_add2_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression to subtract two 64-bit integers **/ + static rp make_sub2_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression to multiply two 64-bit integers **/ + static rp make_mul2_i64(const rp & lhs, + const rp & rhs); + /** create apply-expression to divide two 64-bit integers **/ + static rp make_div2_i64(const rp & lhs, + const rp & rhs); + + /** create apply-expression to add two 64-bit floating-point numbers **/ + static rp make_add2_f64(const rp & lhs, + const rp & rhs); + /** create apply-expression to subtract two 64-bit floating-point numbers **/ + static rp make_sub2_f64(const rp & lhs, + const rp & rhs); + /** create apply-expression to multiply two 64-bit floating-point numbers **/ + static rp make_mul2_f64(const rp & lhs, + const rp & rhs); + /** create apply-expression to divide two 64-bit floating-point numbers **/ + static rp make_div2_f64(const rp & lhs, + const rp & rhs); + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + const rp & fn() const { return fn_; } + const std::vector> & argv() const { return argv_; } + + std::size_t n_arg() const { return argv_.size(); } + const rp & lookup_arg(size_t i) const { return argv_.at(i); } + + virtual std::set get_free_variables() const override { + std::set retval = fn_->get_free_variables(); + + for (const auto & arg : argv_) { + std::set arg_free_set + = arg->get_free_variables(); + + for (const auto & name : arg_free_set) + retval.insert(name); + } + + return retval; + } + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += fn_->visit_preorder(visitor_fn); + + for (const auto & arg : argv_) + n += arg->visit_preorder(visitor_fn); + + return n; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += fn_->visit_layer(visitor_fn); + + for (const auto & arg : argv_) + n += arg->visit_layer(visitor_fn); + + return n; + } + + virtual rp xform_layer(TransformFn xform_fn) override { + this->fn_ = fn_->xform_layer(xform_fn); + + for (auto & arg : argv_) + arg = arg->xform_layer(xform_fn); + + return xform_fn(this); + } + + virtual void attach_envs(bp p) override; + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override; + + protected: + Apply(TypeDescr apply_valuetype, + const rp & fn, + const std::vector> & argv) + : Expression(exprtype::apply, apply_valuetype), + fn_{fn}, argv_(argv) + {} + + protected: + /** function to invoke **/ + rp fn_; + /** argument expressions, in l-to-r order **/ + std::vector> argv_; + }; /*Apply*/ + +#ifdef NOT_USING + namespace detail { + /** Use: + ** std::vector> + **/ + + template + struct apply_push_args; + + template <> + struct apply_push_args<> { + static void push_all(std::vector> * /*p_argv*/) {} + }; + + template + struct apply_push_args { + static void push_all(std::vector> * p_argv, + const ref::rp & x, Rest... rest) + { + p_argv->push_back(x); + apply_push_args::push_all(p_argv, rest...); + }; + }; + } +#endif + + /* reminder: initializer-lists are compile-time only */ + inline rp + make_apply(const rp & fn, + const std::initializer_list> args) { + std::vector> argv(args); + return Apply::make(fn, argv); + } /*make_apply*/ + + /** @class ApplyAccess + * @brief Apply with writeable members + * + * Convenient when scaffolding a parser, + * e.g. see xo-parser + **/ + class ApplyAccess : public Apply { + public: + static rp make_empty(); + + /** assign function being called to @p fn **/ + void assign_fn(const rp& fn); + /** assign expression for argument i, counting from 1. + * can use @p i = 0 as alternative to @ ref assign_fn + **/ + void assign_arg(size_t i, const rp& arg); + + // inherited from GeneralizedExpression.. + // void assign_valuetype(TypeDescr apply_valuetype); + + private: + ApplyAccess(TypeDescr apply_valuetype, + const rp& fn, + const std::vector>& argv) + : Apply(apply_valuetype, fn, argv) {} + }; + + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end Apply.hpp **/ diff --git a/xo-expression/include/xo/expression/AssignExpr.hpp b/xo-expression/include/xo/expression/AssignExpr.hpp new file mode 100644 index 00000000..2fe947ba --- /dev/null +++ b/xo-expression/include/xo/expression/AssignExpr.hpp @@ -0,0 +1,62 @@ +/* file AssignExpr.hpp + * + * author: Roland Conybeare, Aug 2024 + */ + +#pragma once + +#include "Expression.hpp" +#include "Variable.hpp" + +namespace xo { + namespace scm { + /** @class AssignExpr + * @brief Provide expression for assigning to a variable + * + * def pi = 3.14159265; + * def foo = 0.0; + * foo := pi / 2; + **/ + class AssignExpr : public Expression { + public: + static rp make(const rp & lhs, + const rp & rhs); + + static bp from(bp x) { + return bp::from(x); + } + + const rp & lhs() const { return lhs_; } + const rp & rhs() const { return rhs_; } + + std::set calc_free_variables() const; + + // ----- inherited from Expression ----- + + virtual std::set get_free_variables() const override; + virtual std::size_t visit_preorder(VisitFn visitor_fn) override; + virtual std::size_t visit_layer(VisitFn visitor_fn) override; + virtual rp xform_layer(TransformFn xform_fn) override; + virtual void attach_envs(bp p) override; + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override; + + private: + AssignExpr(const rp & lhs, + const rp & rhs); + + private: + /** assign to this variable. **/ + rp lhs_; + /** assign value of this expression to variable @p lhs **/ + rp rhs_; + + /** free variables for this assignment **/ + std::set free_var_set_; + }; + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end AssignExpr.hpp */ diff --git a/xo-expression/include/xo/expression/Constant.hpp b/xo-expression/include/xo/expression/Constant.hpp new file mode 100644 index 00000000..e5c9b0a9 --- /dev/null +++ b/xo-expression/include/xo/expression/Constant.hpp @@ -0,0 +1,110 @@ +/** @file Constant.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "ConstantInterface.hpp" +#include "pretty_expression.hpp" +#include + +namespace xo { + namespace scm { + /** @class Constant + * @brief syntax for a literal constant. + * + * Require: + * 1. T must be a POD type (plain old data) + * We need this to be true so that we can generate + * code for constructing a T instance by memcopying + * @ref value_ + * + * @tp T type of captured literal. + **/ + template + class Constant : public ConstantInterface { + public: + using Reflect = xo::reflect::Reflect; + using TaggedPtr = xo::reflect::TaggedPtr; + using TypeDescr = xo::reflect::TypeDescr; + + public: + /** create constant expression representing literal value x **/ + static rp make(const T & x) { + TypeDescr x_valuetype = Reflect::require(); + + return new Constant(x_valuetype, x); + } + + const T & value() const { return value_; } + + // ----- ConstantInterface ----- + + virtual TypeDescr value_td() const override { return value_td_; } + virtual TaggedPtr value_tp() const override { + /* note: idk why, but need to spell this out in two steps with gcc 13.2 */ + const void * erased_cptr = &value_; + void * erased_ptr = const_cast(erased_cptr); + + return TaggedPtr(value_td_, erased_ptr); + } + + // ----- Expression ----- + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + visitor_fn(this); + return 1; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + visitor_fn(this); + return 1; + } + + virtual rp xform_layer(TransformFn xform_fn) override { + return xform_fn(this); + } + + virtual void display(std::ostream & os) const override { + os << "short_name()); + else + os << xtag("type", "nullptr"); + os << xtag("value", value_); + os << ">"; + } + + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override { + return ppii.pps()->pretty_struct(ppii, "Constant", + rtag("type", print::quot(this->valuetype()->short_name())), + refrtag("value", value_)); + } + + private: + explicit Constant(TypeDescr x_type, const T & x) + : ConstantInterface(exprtype::constant, x_type), + value_td_{Reflect::require()}, + value_(x) + { + //static_assert(std::is_standard_layout_v && std::is_trivial_v); + } + + private: + /** type description for T **/ + TypeDescr value_td_; + /** value of this constant **/ + T value_; + }; /*Constant*/ + + template + rp>> + make_constant(const T & x) { + return Constant::make(x); + } + + } /*namespace scm*/ +} /*namespace xo*/ + +/** end Constant.hpp **/ diff --git a/xo-expression/include/xo/expression/ConstantInterface.hpp b/xo-expression/include/xo/expression/ConstantInterface.hpp new file mode 100644 index 00000000..57db19b8 --- /dev/null +++ b/xo-expression/include/xo/expression/ConstantInterface.hpp @@ -0,0 +1,50 @@ +/** @file ConstantInterface.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" +#include "xo/reflect/Reflect.hpp" +#include "xo/reflect/TypeDescr.hpp" +#include + +namespace xo { + namespace scm { + /** @class ConstantInterface + * @brief syntax for a literal constant. + **/ + class ConstantInterface : public Expression { + public: + using TaggedPtr = xo::reflect::TaggedPtr; + using TypeDescr = xo::reflect::TypeDescr; + + public: + /** @p extype sets expression-type; could be constant|primitive **/ + ConstantInterface(exprtype extype, TypeDescr valuetype) : Expression{extype, valuetype} {} + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + /** type description for representation of literal value **/ + virtual TypeDescr value_td() const = 0; + /** reflection-tagged pointer to literal value of this constant **/ + virtual TaggedPtr value_tp() const = 0; + + // ----- Expression ----- + + virtual std::set get_free_variables() const override { + return std::set(); + } + + virtual void attach_envs(bp /*p*/) override {} + + }; /*ConstantInterface*/ + + } /*namespace scm*/ +} /*namespace xo*/ + +/** end ConstantInterface.hpp **/ diff --git a/xo-expression/include/xo/expression/ConvertExpr.hpp b/xo-expression/include/xo/expression/ConvertExpr.hpp new file mode 100644 index 00000000..26f5c337 --- /dev/null +++ b/xo-expression/include/xo/expression/ConvertExpr.hpp @@ -0,0 +1,110 @@ +/* file ConvertExpr.hpp + * + * author: Roland Conybeare, Aug 2024 + */ + +#pragma once + +#include "Expression.hpp" + +namespace xo { + namespace scm { + /** @class Convertexpr + * @brief Convenience for automatic type conversion + * + * This is equivalent to calling a built-in primitive + * that performs the conversion. + * + * We rely on this for convenience, for example to parse + * code like + * + * def foo : i16 = 0 + **/ + class ConvertExpr : public Expression { + public: + static rp make(TypeDescr dest_type, + rp arg); + + static bp from(bp x) { + return bp::from(x); + } + + const rp & arg() const { return arg_; } + + // ----- Expression ----- + + virtual std::set get_free_variables() const override; + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += arg_->visit_preorder(visitor_fn); + + return n; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += this->arg_->visit_layer(visitor_fn); + + return n; + } + + virtual rp xform_layer(TransformFn xform_fn) override { + this->arg_ = this->arg_->xform_layer(xform_fn); + + return xform_fn(this); + } + + virtual void attach_envs(bp p) override { + arg_->attach_envs(p); + } + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override; + + protected: + ConvertExpr(TypeDescr dest_type, + rp arg) + : Expression(exprtype::convert, dest_type), + arg_{std::move(arg)} + {} + + protected: + /** source expression. Convert + * @c arg_->valuetype() to @c dest_type_ + **/ + rp arg_; + }; + + /** @class ConvertExprAccess + * @brief ConvertExpr with writeable members. + * + * Convenient when scaffolding a parser, + * e.g. see xo-parser + **/ + class ConvertExprAccess : public ConvertExpr { + public: + static rp make(TypeDescr dest_type, + rp arg); + static rp make_empty(); + + void assign_arg(rp arg) { this->arg_ = std::move(arg); } + + private: + ConvertExprAccess(TypeDescr dest_type, + rp arg) + : ConvertExpr(dest_type, + std::move(arg)) + {} + }; + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end ConvertExpr.hpp */ diff --git a/xo-expression/include/xo/expression/DefineExpr.hpp b/xo-expression/include/xo/expression/DefineExpr.hpp new file mode 100644 index 00000000..085c18ce --- /dev/null +++ b/xo-expression/include/xo/expression/DefineExpr.hpp @@ -0,0 +1,131 @@ +/* file DefineExpr.hpp + * + * author: Roland Conybeare, Jul 2024 + */ + +#pragma once + +#include "Expression.hpp" + +namespace xo { + namespace scm { + /** @class DefineExpr + * @brief Provide definition for a constant, variable or function + * + * At toplevel, introduces a new global variable. + * In a nested context, + * + * def foo = rhsexpr + * body... + * + * is equivalent to + * + * (lambda (foo) body...)(rhsexpr) + * + * Promise: + * - memory location of @ref lhs_var_ is determined when parent DefineExpr + * constructed, and is stable across calls to @ref DefineExpr::assign_lhs_name + **/ + class DefineExpr : public Expression { + public: + static rp make(std::string name, + rp value); + + + static bp from(bp x) { + return bp::from(x); + } + + const std::string & lhs_name() const; + const rp & rhs() const { return rhs_; } + + const rp& lhs_variable() const { return lhs_var_; } + + std::set calc_free_variables() const; + + // ----- Expression ----- + + virtual std::set get_free_variables() const override { + return this->free_var_set_; + } + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += rhs_->visit_preorder(visitor_fn); + + return n; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += this->rhs_->visit_layer(visitor_fn); + + return n; + } + + virtual rp xform_layer(TransformFn xform_fn) override { + this->rhs_ = this->rhs_->xform_layer(xform_fn); + + return xform_fn(this); + } + + virtual void attach_envs(bp p) override { + rhs_->attach_envs(p); + } + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override; + + protected: + /** + * + **/ + DefineExpr(TypeDescr rhs_valuetype, + std::string lhs_name, + rp rhs); + + protected: + /** symbol name for this definition **/ + rp lhs_var_; + /** right-hand side of definition **/ + rp rhs_; + + /** free variables for this definition **/ + std::set free_var_set_; + }; + + /** @class DefineExprAccess + * @brief DefineExpr with writeable members. + * + * Convenient when scaffolding a parser, + * e.g. see xo-parser + **/ + class DefineExprAccess : public DefineExpr { + public: + static rp make(std::string lhs_name, + rp rhs); + static rp make_empty(); + + void assign_lhs_name(const std::string & x); + void assign_rhs(const rp & x); + + private: + DefineExprAccess(TypeDescr rhs_valuetype, + std::string lhs_name, + rp rhs) + : DefineExpr(rhs_valuetype, + std::move(lhs_name), + std::move(rhs)) + {} + }; + + } /*namespace scm*/ +} /*namespace xo*/ + +/* end DefineExpr.hpp */ diff --git a/xo-expression/include/xo/expression/Expression.hpp b/xo-expression/include/xo/expression/Expression.hpp new file mode 100644 index 00000000..15fad60c --- /dev/null +++ b/xo-expression/include/xo/expression/Expression.hpp @@ -0,0 +1,90 @@ +/** @file Expression.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "GeneralizedExpression.hpp" +#include +#include + +namespace xo { + namespace scm { + class Variable; /* see Variable.hpp */ + class Lambda; /* see Lamnbda.hpp */ + class SymbolTable; /* see SymbolTable.hpp */ + + /** @class Expression + * @brief abstract syntax tree for an EGAD program + * + * (Expression Graph with Automagic Derivation) + * + * Things you can do with an Expression: + * - evaluate it using an interpreter + * - execute it on a VM + * - compile using LLVM + * see xo-jit/ + * + * Expressions are immutable. This means they can resused + * across jit interactions + * + * Every expression evaluates to a value with a particular type + **/ + class Expression : public GeneralizedExpression { + public: + using VisitFn = std::function + )>; + using TransformFn = std::function + (bp)>; + using TypeDescr = xo::reflect::TypeDescr; + + public: + explicit Expression(exprtype extype, TypeDescr valuetype) + : GeneralizedExpression(extype, valuetype) {} + + /** find free named variables in this expression. + * comprises the set of names that don't match formal parameters in + * enclosing lambdas. + **/ + virtual std::set get_free_variables() const = 0; + + /** visit each Expression node in this AST, + * and invoke @p fn for each. + * Returns the number of nodes visited. + * Preorder: call @p fn for a node before visiting children + **/ + virtual std::size_t visit_preorder(VisitFn visitor_fn) = 0; + + /** visit each Expression node in this AST, + * including immediately-nested Lambda nodes; + * but do not recurse into the params/body of such nested Lambdas. + * Returns the number of nodes visited + **/ + virtual std::size_t visit_layer(VisitFn visitor_fn) = 0; + + /** traverse ast @ref visit_preorder but do not visit Lambdas **/ + virtual rp xform_layer(TransformFn visitor_fn) = 0; + + /** attach an environment to each lambda expression X in this subtree, + * that will: + * - resolve names matching X's arguments (formal parameters) to + * from @p X.argv + * - resolve free variables from @p parent + **/ + virtual void attach_envs(bp parent) = 0; + + /** append to *p_set the set of free variables in this expression. + * returns the number of free variables introduced + * + * @param env stack of lexcically-enclosing lamnbda expressions, + * in nesting order, i.e. outermost first, innertmost last + **/ + //virtual std::int32_t find_free_vars(std::vector> env) = 0; + + }; /*Expression*/ + + } /*namespace scm*/ +} /*namespace xo*/ + +/** end Expression.hpp **/ diff --git a/xo-expression/include/xo/expression/FunctionExprInterface.hpp b/xo-expression/include/xo/expression/FunctionExprInterface.hpp new file mode 100644 index 00000000..340d3f11 --- /dev/null +++ b/xo-expression/include/xo/expression/FunctionExprInterface.hpp @@ -0,0 +1,33 @@ +/** @file ProcedureExprInterface.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" +//#include + +namespace xo { + namespace scm { + class ProcedureExprInterface : public Expression { + public: + ProcedureExprInterface(exprtype extype, TypeDescr fn_type) + : Expression(extype, fn_type) {} + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + virtual const std::string & name() const = 0; + virtual int n_arg() const = 0; // { return this->value_td()->n_fn_arg(); } + virtual TypeDescr fn_retval() const = 0; // { return this->value_td()->fn_retval(); } + virtual TypeDescr fn_arg(uint32_t i) const = 0; // { return this->value_td()->fn_arg(i); } + + private: + }; /*FunctionInterface*/ + } /*namespace scm*/ +} /*namespace xo*/ + +/** end ProcedureExprInterface.hpp **/ diff --git a/xo-expression/include/xo/expression/GeneralizedExpression.hpp b/xo-expression/include/xo/expression/GeneralizedExpression.hpp new file mode 100644 index 00000000..4096aaca --- /dev/null +++ b/xo-expression/include/xo/expression/GeneralizedExpression.hpp @@ -0,0 +1,72 @@ +/** @file GeneralizedExpression.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "xo/refcnt/Refcounted.hpp" +#include "xo/expression/typeinf/type_ref.hpp" +//#include "xo/reflect/TypeDescr.hpp" +#include "exprtype.hpp" + +namespace xo { + namespace scm { + /** @class GeneralizedExpression + * @brief abstract syntax tree (non-executable) for schematica + * + * 'Generalized' because it includes both kernel and macro expressions. + * Every macro expression automatically translates to an equivalent kernel expression. + * Kernel expressions are directly executable. + **/ + class GeneralizedExpression : public ref::Refcount { + public: + using type_ref = xo::scm::type_ref; + using prefix_type = xo::scm::prefix_type; + using TypeDescr = xo::reflect::TypeDescr; + using ppstate = xo::print::ppstate; + using ppindentinfo = xo::print::ppindentinfo; + + public: + /** if @p valuetype is null, generate unique type variable + * using prefix derived from @p extype. + **/ + GeneralizedExpression(exprtype extype, TypeDescr valuetype); + /** if @p valuetype is null, generate unique type variable + * name, beginning with @p prefix + **/ + GeneralizedExpression(exprtype extype, prefix_type prefix, TypeDescr valuetype); + + exprtype extype() const { return extype_; } + const type_ref & valuetype_ref() const { return valuetype_ref_; } + TypeDescr valuetype() const { return valuetype_ref_.td(); } + + /** write human-readable representation to stream @p os **/ + virtual void display(std::ostream & os) const = 0; + /** human-readable string representation **/ + virtual std::string display_string() const; + /** pretty printing support. See [xo-indentlog/xo/indentlog/pretty.hpp] **/ + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const = 0; + + /** useful when scaffolding expressions in a parser **/ + void assign_valuetype(TypeDescr x) { valuetype_ref_.resolve_to(x); } + + private: + /** expression type (constant | apply | ..) for this expression **/ + exprtype extype_ = exprtype::invalid; + /** type information (when available) for values produced by this + * expression. + **/ + type_ref valuetype_ref_; + }; + + inline std::ostream & + operator<<(std::ostream & os, const GeneralizedExpression & x) { + x.display(os); + return os; + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end GeneralizedExpression.hpp */ diff --git a/xo-expression/include/xo/expression/GlobalSymtab.hpp b/xo-expression/include/xo/expression/GlobalSymtab.hpp new file mode 100644 index 00000000..1669df3b --- /dev/null +++ b/xo-expression/include/xo/expression/GlobalSymtab.hpp @@ -0,0 +1,66 @@ +/* file GlobalSymtab.hpp + * + * author: Roland Conybeare, Jun 2024 + */ + +#pragma once + +#include "SymbolTable.hpp" +#include +#include + +namespace xo { + namespace scm { + class GlobalSymtab : public SymbolTable { + public: + /** create instance. Probably only need one of these **/ + static rp make_empty() { return new GlobalSymtab(); } + + bp require_global(const std::string & vname, + bp expr); + + // ----- Environment ----- + + virtual bool is_global_env() const override { return true; } + + virtual binding_path lookup_binding(const std::string & /*vname*/) const override { + /* i_link: -1 for global environment + * j_slot: not used + */ + return { -1, 0 }; + } + + virtual bp lookup_var(const std::string & vname) const override { + return this->lookup_local(vname); + } + + virtual bp lookup_local(const std::string & vname) const override { + auto ix = global_map_.find(vname); + + if (ix == global_map_.end()) { + /* not found */ + return bp(); + } + + return ix->second; + } + + virtual void upsert_local(bp target) override; + + virtual void print(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const xo::print::ppindentinfo & ppii) const override; + + private: + GlobalSymtab(); + + private: + /* for assignable globals, need to allocate memory + * addresses for these. + */ + std::map> global_map_; + }; + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end GlobalSymtab.hpp */ diff --git a/xo-expression/include/xo/expression/IfExpr.hpp b/xo-expression/include/xo/expression/IfExpr.hpp new file mode 100644 index 00000000..6ce220fc --- /dev/null +++ b/xo-expression/include/xo/expression/IfExpr.hpp @@ -0,0 +1,174 @@ +/** @file IfExpr.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" +//#include +#include +//#include + +namespace xo { + namespace scm { + /** @class IfExpr + * @brief abstract syntax tree for a function definition + * + **/ + class IfExpr : public Expression { + public: + using TypeDescr = xo::reflect::TypeDescr; + + public: + /** create expression for conditional execution of + * @p when_true or @p when_false, depending on result + * of evaluating expression @p test + **/ + static rp make(const rp & test, + const rp & when_true, + const rp & when_false); + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + const rp & test() const { return test_; } + const rp & when_true() const { return when_true_; } + const rp & when_false() const { return when_false_; } + + // ----- Expression ----- + + virtual std::set get_free_variables() const override { + std::set retval = test_->get_free_variables(); + + std::set free_vars; + free_vars = when_true_->get_free_variables(); + for (const auto & s : free_vars) + retval.insert(s); + + free_vars = when_false_->get_free_variables(); + for (const auto & s : free_vars) + retval.insert(s); + + return retval; + } + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += this->test_->visit_preorder(visitor_fn); + n += this->when_true_->visit_preorder(visitor_fn); + n += this->when_false_->visit_preorder(visitor_fn); + + return n; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + n += this->test_->visit_layer(visitor_fn); + n += this->when_true_->visit_layer(visitor_fn); + n += this->when_false_->visit_layer(visitor_fn); + + return n; + } + + virtual rp xform_layer(TransformFn xform_fn) override { + this->test_ = this->test_->xform_layer(xform_fn); + this->when_true_ = this->when_true_->xform_layer(xform_fn); + this->when_false_= this->when_false_->xform_layer(xform_fn); + + return xform_fn(this); + } + + virtual void attach_envs(bp p) override { + test_->attach_envs(p); + when_true_->attach_envs(p); + when_false_->attach_envs(p); + } + +#ifdef NOT_USING + virtual std::int32_t find_free_vars(std::set> * p_set) override { + return (test_->find_free_vars(p_set) + + when_true_->find_free_vars(p_set) + + when_false_->find_free_vars(p_set)); + } +#endif + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppi) const override; + + protected: + /** + * @p ifexpr_type type for value produced by if-expression. + * same as both when_true->valuetype() and + * when_false->valuetype(). + * @p test test-expression; always execute + * @p when_true then-branch; executes only when test succeeds + * @p when_false else-branch; executes only when test fails + **/ + IfExpr(TypeDescr ifexpr_type, + rp test, + rp when_true, + rp when_false) + : Expression(exprtype::ifexpr, ifexpr_type), + test_{std::move(test)}, + when_true_{std::move(when_true)}, + when_false_{std::move(when_false)} {} + + static TypeDescr check_consistent_valuetype(const rp & when_true, + const rp & when_false); + + /** determine if-expr valuetype **/ + void establish_valuetype(); + + protected: + /** if: + * (if x y z) + * + * executes x; if true execute y; otherwise execute z + **/ + rp test_; + rp when_true_; + rp when_false_; + }; /*IfExpr*/ + + inline rp + make_ifexpr(const rp & test, + const rp & when_true, + const rp & when_false) + { + return IfExpr::make(test, when_true, when_false); + } + + class IfExprAccess : public IfExpr { + public: + static rp make(rp test, + rp when_true, + rp when_false); + static rp make_empty(); + + void assign_test(rp x) { test_ = std::move(x); } + void assign_when_true(rp x); + void assign_when_false(rp x); + + private: + IfExprAccess(TypeDescr ifexpr_type, + rp test, + rp when_true, + rp when_false) + : IfExpr(ifexpr_type, + std::move(test), + std::move(when_true), + std::move(when_false)) {} + }; + } /*namespace scm*/ +} /*namespace xo*/ + +/** end IfExpr.hpp **/ diff --git a/xo-expression/include/xo/expression/Lambda.hpp b/xo-expression/include/xo/expression/Lambda.hpp new file mode 100644 index 00000000..e5b3aeb1 --- /dev/null +++ b/xo-expression/include/xo/expression/Lambda.hpp @@ -0,0 +1,242 @@ +/** @file Lambda.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" +#include "ProcedureExprInterface.hpp" +#include "Variable.hpp" +#include "LocalSymtab.hpp" +#include +#include +#include + +namespace xo { + namespace scm { + /** @class Lambda + * @brief abstract syntax tree for a function definition + * + **/ + class Lambda : public ProcedureExprInterface { + public: + /** + * @p name. Name for this lambda -- must be unique + * @p lambda_type. Function signature + * @p local_env. Environment with formals as content + * @p body. Expression for lambda function body + **/ + static rp make(const std::string & name, + TypeDescr lambda_type, + const rp & local_env, + const rp & body); + + /** + * @p name Name for this lambda -- must be unique + * @p argv Formal parameters, in left-to-right order + * @p body Expression for body of this function + * @p parent_env Environment for enclosing lexical scope + **/ + static rp make(const std::string & name, + const std::vector> & argv, + const rp & body, + const rp & parent_env); + + /** + * @p name Name for this lambda -- must be unique + * @p env Environment with {name,type} for each formal parameter + * @p body Expression for body of function + **/ + static rp make_from_env(const std::string & name, + const rp & env, + TypeDescr explicit_return_td, + const rp & body); + + /** create type description for lambda with arguments @p argv + * and return type @p return_td + **/ + static TypeDescr assemble_lambda_td(const std::vector> & argv, + TypeDescr return_td); + + /** create type description for lambda with arguments @p argv + * and body expression @p body. + * @p explicit_return_td will be used if non-null. + * otherwise use @p body valuetype + **/ + static TypeDescr assemble_lambda_td(const std::vector> & argv, + TypeDescr explicit_return_td, + const rp & body); + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + const std::string & type_str() const { return type_str_; } + const std::vector> & argv() const { return local_env_->argv(); } + const rp & body() const { return body_; } + + const std::string& i_argname(int i_arg) const { return local_env_->lookup_arg(i_arg)->name(); } + bool needs_closure_flag() const { return !free_var_set_.empty(); } + bool is_captured(const std::string& var) const { return (captured_var_set_.find(var) != captured_var_set_.end()); } + + // ----- FunctionInterface ----- + + virtual const std::string & name() const override { return name_; } + /** return number of arguments expected by this function **/ + virtual int n_arg() const override { return local_env_->n_arg(); } + virtual TypeDescr fn_retval() const override { return body_->valuetype(); } + virtual TypeDescr fn_arg(uint32_t i) const override { return local_env_->fn_arg(i); } + + // ----- Expression ----- + + virtual std::set get_free_variables() const override { + return this->free_var_set_; + } + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + for (const auto & arg : local_env_->argv()) + n += arg->visit_preorder(visitor_fn); + + n += body_->visit_preorder(visitor_fn); + + return n; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + std::size_t n = 1; + + visitor_fn(this); + + return n; + } + + virtual rp xform_layer(TransformFn /*xform_fn*/) override { + /* a layer is bounded by lambdas, don't enter them */ + return this; + } + + virtual void attach_envs(bp p) override; + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override; + + protected: + /** create string description for function signature, + * consistent with c++ expectation + **/ + static std::string assemble_type_str(TypeDescr lambda_td); + + /** @param lambda_type. function type for this lambda. + * We arbitrarily choose the form "Retval(*)(Args...)" + **/ + Lambda(const std::string & name, + TypeDescr lambda_type, + const rp & local_env, + const rp & body); + + /** compute free-variable set for this lambda **/ + std::set calc_free_variables() const; + + /** ensure at most one Variable instance with a particular name + * in this lambda, but ignore nested lambdas. + * + * Goal is to unify variables that can use the same binding + * path to determine memory location at runtime. + **/ + std::map> regularize_layer_vars(); + + /** compute derived members + * (type_str_, free_var_set_, captured_var_set_, layer_var_map_, + * nested_lambda_map_) + * once .body_ is established + **/ + void complete_assembly_from_body(); + + protected: + /** lambda name. Initially supporting only form like + * (define (foo x y z) + * (+ (* x x) (* y y) (* z z))) + * + * In any case need to supply names for distinct + * things-for-which-code-is-generated so that they can be linked etc. + **/ + std::string name_; + /** e.g. + * "double(double,double)" for function of two doubles that + * returns a double + **/ + std::string type_str_; + /** function body **/ + rp body_; + + /** free variables for this lambda **/ + std::set free_var_set_; + + /** variables that appear free in some nested lambda **/ + std::set captured_var_set_; + + /** map giving unique identity to each variable appearing in this layer. + * includes: + * - formal parameters + * - free variables in @ref body_ + * excludes: + * - any variables appearing in nested lambdas + * (whether formals or free variables) + **/ + std::map> layer_var_map_; + + /** all lambdas nested once inside this lambda's body **/ + std::map> nested_lambda_map_; + + /** established (once) by @ref attach_envs. + * + * @note data dependency on ancestor expressions that don't exist yet + * when Lambda constructor runs, so we need to assign @ref local_env_ + * later. + **/ + rp local_env_; + }; /*Lambda*/ + + inline rp + make_lambda(const std::string & name, + const std::vector> & argv, + const rp & body, + const rp & parent_env) + { + return Lambda::make(name, argv, body, parent_env); + } + + class LambdaAccess : public Lambda { + public: + static rp make(const std::string & name, + const std::vector> & argv, + const rp & body, + const rp & parent_env); + static rp make_empty(); + + /** assign body + compute derived members + * (see complete_assembly_from_body()) + **/ + void assign_body(const rp & body); + + private: + /** lambda_type, body can be null here, + * in which case fill in with assign methods + **/ + LambdaAccess(const std::string & name, + TypeDescr lambda_type, + const rp & local_env, + const rp & body); + + + }; + } /*namespace scm*/ +} /*namespace xo*/ + +/** end Lambda.hpp **/ diff --git a/xo-expression/include/xo/expression/LocalSymtab.hpp b/xo-expression/include/xo/expression/LocalSymtab.hpp new file mode 100644 index 00000000..6fd4e395 --- /dev/null +++ b/xo-expression/include/xo/expression/LocalSymtab.hpp @@ -0,0 +1,123 @@ +/* file LocalSymtab.hpp + * + * author: Roland Conybeare, Jun 2024 + */ + +#pragma once + +#include "SymbolTable.hpp" +#include "Variable.hpp" +#include "xo/reflect/TypeDescr.hpp" + +namespace xo { + namespace scm { + class Lambda; + + /** @brief LocalEnv + * + * @class Local environment for a lambda. + * Lists the Variables corresponding to this lambda's formal + * parameters, but also links to @ref Environment for + * innermost enclosing @ref Lambda. + **/ + class LocalSymtab : public SymbolTable { + public: + using TypeDescr = xo::reflect::TypeDescr; + + public: + static rp make_empty(); + /** named ctor idiom. Create instance with local variables per @p argv **/ + static rp make(const std::vector> & argv, + const rp & parent_env); + /** Create instance with single local variable @ap argv1 **/ + static rp make1(const rp & arg1, + const rp & parent_env); + /** runtime downcast. nullptr if @p x is not a LocalEnv instance **/ + static bp from(const bp & x) { return bp::from(x); } + + Lambda * origin() const { return origin_; } + const std::vector> & argv() const { return argv_; } + const rp& lookup_arg(int i) const { return argv_[i]; } + int n_arg() const { return argv_.size(); } + TypeDescr fn_arg(uint32_t i) const { return argv_[i]->valuetype(); } + + /** report binding path for a formal parameter. + * Returns sentinel if @p vname doesn't appear in @ref argv_ + **/ + binding_path lookup_local_binding(const std::string & vname) const; + + /** single-assign this environment's origin **/ + void assign_origin(Lambda * p) { + assert(origin_ == nullptr); + origin_ = p; + } + + /** single-assign this environment's parent **/ + void assign_parent(bp p); + + // ----- Environment ----- + + virtual bool is_global_env() const override { return false; } + + virtual binding_path lookup_binding(const std::string & vname) const override; + + virtual bp lookup_var(const std::string & vname) const override { + bp retval = this->lookup_local(vname); + + if (retval) + return retval; + + /* here: target not found in local vars, + * delegate to innermost ancestor + */ + return parent_env_->lookup_var(vname); + } + + virtual bp lookup_local(const std::string & vname) const override { + for (const auto & arg : argv_) { + if (arg->name() == vname) + return arg; + } + + return bp(); + } + + /** create/replace local variable @p target. + * Narrow use case: intended for when LocalEnv represents a top-level session environment. + **/ + virtual void upsert_local(bp target) override; + + virtual void print(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const print::ppindentinfo & ppii) const override; + + private: + LocalSymtab(const std::vector> & argv, const rp & parent_env); + + private: + /** Lambda for which this environment created. + * + * Invariant: + * @code + * origin_->local_env_ == this + * @endcode + **/ + Lambda * origin_ = nullptr; + + /** formal argument names. + * all variables in @ref argv_ have distinct names. + * if @c .lookup_binding(vname) returns a binding path with @c .i_link=0 and @c .j_slot=j + * then @c argv_[j]->name_ is @c vname. + **/ + std::vector> argv_; + + /** parent environment. A free variable in this lambda's + * body will be resolved by referring them to @ref parent_env_. + **/ + rp parent_env_; + }; + + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end LocalSymtab.hpp */ diff --git a/xo-expression/include/xo/expression/PrimitiveExpr.hpp b/xo-expression/include/xo/expression/PrimitiveExpr.hpp new file mode 100644 index 00000000..86ca5e03 --- /dev/null +++ b/xo-expression/include/xo/expression/PrimitiveExpr.hpp @@ -0,0 +1,219 @@ +/** @file PrimitiveExpr.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "PrimitiveExprInterface.hpp" +#include "pretty_expression.hpp" +#include "llvmintrinsic.hpp" +#include "xo/reflect/Reflect.hpp" +#include "xo/indentlog/print/quoted.hpp" + +extern "C" { + /* these symbols needed to link primitives */ + + /* see Primitive_f64::make() */ + double add2_f64(double x, double y); +}; + +namespace xo { + namespace scm { + /** @class PrimitiveExpr + * @brief syntax for a constant that refers to a known function. + * + * Two cases here: + * 1. (always) primitive refers to a compiled (C/C++) function that we can invoke at runtime + * 2. (sometimes) primitive also refers to a function that is supported directly in llvm + * (e.g. floating-point addition). In that case @ref intrinsic_ + * identifies that direct support, provided it knows at codegen time which primitive + * is being invoked + * + * In any case, a primitive serves as both declaration and definition + * (May be possible to relax this to declaration-only using null value_ as sentinel..?) + * + * @tparam FunctionPointer a function-pointer type, e.g. double(*)(double). + * Must be in this "canonical form". std::function + * won't work here. + **/ + template + class PrimitiveExpr: public PrimitiveExprInterface { + public: + using Reflect = xo::reflect::Reflect; + using TaggedPtr = xo::reflect::TaggedPtr; + using TypeDescr = xo::reflect::TypeDescr; + using fptr_type = FunctionPointer; + + public: + static rp make(const std::string & name, + FunctionPointer fnptr, + bool explicit_symbol_def, + llvmintrinsic intrinsic) { + TypeDescr fn_type = Reflect::require(); + + return new PrimitiveExpr(fn_type, name, fnptr, explicit_symbol_def, intrinsic); + } + + /** see classes below for intrinsics **/ + + FunctionPointer value() const { return value_; } + + TypeDescr value_td() const { return value_td_; } + + // ----- PrimitiveExprInterface ----- + + virtual TaggedPtr value_tp() const final override { + /* note: idk why, but need to spell this out in two steps with gcc 13.2 */ + const void * erased_cptr = &value_; + void * erased_ptr = const_cast(erased_cptr); + + return TaggedPtr(value_td_, erased_ptr); + } + virtual llvmintrinsic intrinsic() const override { return intrinsic_; } + virtual bool explicit_symbol_def() const override { return explicit_symbol_def_; } + virtual void_function_type function_address() const override { return reinterpret_cast(value_); } + + // ----- FunctionInterface ----- + + virtual std::string const & name() const override { return name_; } + virtual int n_arg() const override { return this->value_td()->n_fn_arg(); } + virtual TypeDescr fn_retval() const override { return this->value_td()->fn_retval(); } + virtual TypeDescr fn_arg(uint32_t i) const override { return this->value_td()->fn_arg(i); } + + // ----- Expression ----- + + virtual void display(std::ostream & os) const override { + os << "value_td()->short_name()) + << xtag("value", this->value()) + << ">"; + } + + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override { + /* 1. rtag instead of refrtag: + * print::quot() is a temporary rvalue; lifetime ends before control enters pretty_struct() + * + * 2. value cast to void*: + * we don't have pretty printer for native function pointers anyway + * + simplifies ppdetail_atomic + */ + return ppii.pps()->pretty_struct(ppii, "PrimitiveExpr", + refrtag("name", name_), + rtag("type", print::quot(this->valuetype()->short_name())), + refrtag("value", (void*)(this->value()))); + } + + private: + PrimitiveExpr(TypeDescr fn_type, + const std::string & name, + FunctionPointer fnptr, + bool explicit_symbol_def, + llvmintrinsic intrinsic) + : PrimitiveExprInterface(fn_type), + name_{name}, + value_td_{Reflect::require_function()}, + value_{fnptr}, + explicit_symbol_def_{explicit_symbol_def}, + intrinsic_{intrinsic} + { + if (!value_td_->is_function()) + throw std::runtime_error("PrimitiveExpr: expected function pointer"); + if (!value_td_->fn_retval()) + throw std::runtime_error("PrimitiveExpr: expected non-null function return value"); + } + + + private: + // from Expression: + // exprtype extype_ + + /** name of this primitive, e.g. '+', 'sqrt' **/ + std::string name_; + /** type description for FunctionPointer **/ + TypeDescr value_td_; + /** address of executable function **/ + FunctionPointer value_; + /** for LLVM: if true, use Jit.intern_symbol() to provide explicit binding. + * + * Not obvious what distinguishes functions like ::sin(), ::sqrt() + * (which work without this) from symbols like ::mul_i32(), which require it. + **/ + bool explicit_symbol_def_ = false; + /** invalid: generate call (IRBuilder::CreateCall) + * all others: generate direct use of LLVM intrinsic + **/ + llvmintrinsic intrinsic_; + }; /*PrimitiveExpr*/ + + /** adopt function @p x as a callable primitive function named @p name **/ + template + rp> + make_primitive(const std::string & name, + FunctionPointer x, + bool explicit_symbol_def, + llvmintrinsic intrinsic) + { + return PrimitiveExpr::make(name, x, explicit_symbol_def, intrinsic); + } + + // NOTE: see xo-reader/src/reader/progress_xs.cpp + // binding operators to primitive applications. + + /** builtin primitives :: i64 x i64 -> bool **/ + class PrimitiveExpr_cmp_i64 : public PrimitiveExpr { + public: + using PrimitiveExprType = PrimitiveExpr; + + public: + /** eq2_i64: compare two 64-bit integers for equality **/ + static rp make_cmp_eq2_i64(); + /** ne2_i64: compare two 64-bit integers for inequality **/ + static rp make_cmp_ne2_i64(); + /** lt2_i64: compare two 64-bit integers for lessthan **/ + static rp make_cmp_lt2_i64(); + /** lt2_i64: compare two 64-bit integers for lessthanorequal **/ + static rp make_cmp_le2_i64(); + /** gt2_i64: compare two 64-bit integers for greaterthan **/ + static rp make_cmp_gt2_i64(); + /** ge2_i64: compare two 64-bit integers for greaterthan **/ + static rp make_cmp_ge2_i64(); + }; + + /** builtin primitives :: i64 x i64 -> i64 **/ + class PrimitiveExpr_i64 : public PrimitiveExpr { + public: + using PrimitiveExprType = PrimitiveExpr; + + public: + /** add2_i64: add two 64-bit integers **/ + static rp make_add2_i64(); + /** sub2_i64: subtract two 64-bit integers **/ + static rp make_sub2_i64(); + /** mul2_i64: multiply two 64-bit integers **/ + static rp make_mul2_i64(); + /** div2_i64: divide two 64-bit integers **/ + static rp make_div2_i64(); + }; + + /** builtin primitives :: f64 x f64 -> f64 **/ + class PrimitiveExpr_f64 : public PrimitiveExpr { + public: + using PrimitiveExprType = PrimitiveExpr; + + public: + /** add2_f64: add two 64-bit floating-point numbers **/ + static rp make_add2_f64(); + /** sub2_f64: subtract two 64-bit floating-point numbers **/ + static rp make_sub2_f64(); + /** mul2_f64: multiply two 64-bit floating-point numbers **/ + static rp make_mul2_f64(); + /** div2_f64: divide two 64-bit floating-point numbers **/ + static rp make_div2_f64(); + }; + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end PrimitiveExpr.hpp **/ diff --git a/xo-expression/include/xo/expression/PrimitiveExprInterface.hpp b/xo-expression/include/xo/expression/PrimitiveExprInterface.hpp new file mode 100644 index 00000000..0fbd0ae3 --- /dev/null +++ b/xo-expression/include/xo/expression/PrimitiveExprInterface.hpp @@ -0,0 +1,82 @@ +/** @file PrimitiveExprInterface.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "ProcedureExprInterface.hpp" +#include "llvmintrinsic.hpp" +#include "xo/reflect/TaggedPtr.hpp" +#include + +namespace xo { + namespace scm { + class PrimitiveExprInterface : public ProcedureExprInterface { + public: + using TaggedPtr = xo::reflect::TaggedPtr; + using void_function_type = void (*)(); + + public: + explicit PrimitiveExprInterface(TypeDescr fn_type) + : ProcedureExprInterface(exprtype::primitive, fn_type) {} + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + /** @return executable function as tagged pointer. + * Load-bearing for xo-interpreter. + **/ + virtual TaggedPtr value_tp() const = 0; + + /** if true, Jit will try to explicitly symbol for this primitive + * (instead of looking it up in host process). + * Don't know if this works. + * Do know it's not needed for ::sin(), ::sqrt(). + * Do know that my extern "C" functions like ::mul_i32(), ::mul_f64() + * need something else. + **/ + virtual bool explicit_symbol_def() const = 0; + + /** function address for this primitive **/ + virtual void_function_type function_address() const = 0; + + /** get llvm intrinsic hint for this primitive **/ + virtual llvmintrinsic intrinsic() const = 0; + + // virtual const std::string & name() const; + // virtual int n_arg() const; + // virtual TypeDescr fn_retval() const; + // virtual TypeDescr fn_arg(uint32_t i) const; + + // ----- Expression ----- + + virtual std::set get_free_variables() const override { + return std::set(); + } + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + visitor_fn(this); + return 1; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + visitor_fn(this); + return 1; + } + + virtual rp xform_layer(TransformFn xform_fn) override { + return xform_fn(this); + } + + virtual void attach_envs(bp /*p*/) override {} + + private: + }; /*PrimitiveInterface*/ + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end PrimitiveInterface.hpp **/ diff --git a/xo-expression/include/xo/expression/ProcedureExprInterface.hpp b/xo-expression/include/xo/expression/ProcedureExprInterface.hpp new file mode 100644 index 00000000..62acf21a --- /dev/null +++ b/xo-expression/include/xo/expression/ProcedureExprInterface.hpp @@ -0,0 +1,33 @@ +/** @file FunctionExprInterface.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" +//#include + +namespace xo { + namespace scm { + class ProcedureExprInterface : public Expression { + public: + ProcedureExprInterface(exprtype extype, TypeDescr fn_type) + : Expression(extype, fn_type) {} + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + virtual const std::string & name() const = 0; + virtual int n_arg() const = 0; // { return this->value_td()->n_fn_arg(); } + virtual TypeDescr fn_retval() const = 0; // { return this->value_td()->fn_retval(); } + virtual TypeDescr fn_arg(uint32_t i) const = 0; // { return this->value_td()->fn_arg(i); } + + private: + }; /*FunctionInterface*/ + } /*namespace scm*/ +} /*namespace xo*/ + +/** end FunctionExprInterface.hpp **/ diff --git a/xo-expression/include/xo/expression/Sequence.hpp b/xo-expression/include/xo/expression/Sequence.hpp new file mode 100644 index 00000000..d8105999 --- /dev/null +++ b/xo-expression/include/xo/expression/Sequence.hpp @@ -0,0 +1,55 @@ +/** @file Sequence.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" + +namespace xo { + namespace scm { + class Sequence : public Expression { + public: + Sequence(const std::vector> & xv) + : Expression(exprtype::sequence, + xv[xv.size() - 1]->valuetype()), + expr_v_(xv) {} + + static rp make(const std::vector> & xv) { return new Sequence(xv); } + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + std::size_t size() const { return expr_v_.size(); } + const rp & operator[](std::size_t i) const { return expr_v_[i]; } + + // ----- from Expression ----- + + /** note: broken if .expr_v_ contains any def-exprs + * (will treat references to so-defined vars as free). + * must rewrite these first + **/ + virtual std::set get_free_variables() const override; + virtual std::size_t visit_preorder(VisitFn visitor_fn) override; + /** note: borken if .expr_v_ contains any def-exprs **/ + virtual std::size_t visit_layer(VisitFn visitor_fn) override; + virtual rp xform_layer(TransformFn visitor_fn) override; + virtual void attach_envs(bp parent) override; + + // ----- from GeneralizedExpression ---- + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override; + + private: + /** sequence of expressions; evaluate in left-to-right order. + **/ + std::vector> expr_v_; + }; + } /*namespace scm*/ + +} /*namespace xo*/ + +/** end Sequence.hpp **/ diff --git a/xo-expression/include/xo/expression/SymbolTable.hpp b/xo-expression/include/xo/expression/SymbolTable.hpp new file mode 100644 index 00000000..710a3c0f --- /dev/null +++ b/xo-expression/include/xo/expression/SymbolTable.hpp @@ -0,0 +1,68 @@ +/* file SymbolTable.hpp + * + * author: Roland Conybeare, Jun 2024 + */ + +#pragma once + +#include "xo/refcnt/Refcounted.hpp" +#include "Variable.hpp" +#include "binding_path.hpp" +#include "xo/indentlog/print/pretty.hpp" + +namespace xo { + + namespace scm { + class Expression; + + /** @class Environment + * @brief Abstract interface for tracking variable bindings + * + * When parsing (see xo-reader): rhs will always be a variable. + * When generating code (see xo-jit): rhs can be any expression, + * for example a Lambda. + **/ + class SymbolTable : public ref::Refcount { + public: + /** true if this is toplevel (global) environment. + * Toplevel environment doesn't have slot numbers. + * + * Variables that bind in the global environment have unique + * names, which we rely on instead of slot numbers. + **/ + virtual bool is_global_env() const = 0; + + /** lookup binding path for @p vname in this environment. + * + * Reports ingredients needed to address variable at runtime, + * in runtime analog of this environment + **/ + virtual binding_path lookup_binding(const std::string & vname) const = 0; + + /** lookup variable-expression @p vname in this environment. + * returns llvm::Value representing code that produces a value for vname + **/ + virtual bp lookup_var(const std::string & vname) const = 0; + + /** like @ref lookup_var but do not delegate to parent environment **/ + virtual bp lookup_local(const std::string & vname) const = 0; + + /** create/replace local variable @p target. + * Narrow use case: intended for when Environment represents a top-level session environment + **/ + virtual void upsert_local(bp target) = 0; + + virtual void print(std::ostream & os) const = 0; + virtual std::uint32_t pretty_print(const xo::print::ppindentinfo & ppii) const = 0; + }; + + inline std::ostream & + operator<< (std::ostream & os, const SymbolTable & x) { + x.print(os); + return os; + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end SymbolTable.hpp */ diff --git a/xo-expression/include/xo/expression/Variable.hpp b/xo-expression/include/xo/expression/Variable.hpp new file mode 100644 index 00000000..f94452cd --- /dev/null +++ b/xo-expression/include/xo/expression/Variable.hpp @@ -0,0 +1,98 @@ +/** @file Variable.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "Expression.hpp" +#include "binding_path.hpp" + +namespace xo { + namespace scm { + + /** @class Variable + * @brief syntax for a variable reference + **/ + class Variable : public Expression { + public: + /** Generate unique symbol-name beginning with @p prefix. + * Relies on static counter + **/ + static std::string gensym(const std::string & prefix); + + /** create expression representing a variable + * identified by @p name, that can take on values + * described by @p var_type. + **/ + static rp make(const std::string & name, + TypeDescr var_type) { + return new Variable(name, var_type); + } + + /** return copy of @p x: same var, different object identity **/ + static rp copy(bp x) { + return new Variable(x->name(), x->valuetype()); + } + + /** downcast from Expression **/ + static bp from(bp x) { + return bp::from(x); + } + + void assign_name(const std::string & name) { name_ = name; } + + const std::string & name() const { return name_; } + + virtual std::set get_free_variables() const override { + std::set retval; + retval.insert(this->name_); + return retval; + } + + virtual std::size_t visit_preorder(VisitFn visitor_fn) override { + visitor_fn(this); + return 1; + } + + virtual std::size_t visit_layer(VisitFn visitor_fn) override { + visitor_fn(this); + return 1; + } + + virtual rp xform_layer(TransformFn xform_fn) override { + return xform_fn(this); + } + + virtual void attach_envs(bp /*p*/) override; + + virtual void display(std::ostream & os) const override; + virtual std::uint32_t pretty_print(const ppindentinfo & ppii) const override; + + private: + Variable(const std::string & name, + TypeDescr var_type) + : Expression(exprtype::variable, var_type), + name_{name} {} + + private: + /** variable name **/ + std::string name_; + /** Eventually: navigate environment via this path to find runtime memory + * location for this variable. + * + * Establish via @ref attach_envs + **/ + binding_path path_; + }; /*Variable*/ + + inline rp + make_var(const std::string & name, + reflect::TypeDescr var_type) { + return Variable::make(name, var_type); + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end Variable.hpp **/ diff --git a/xo-expression/include/xo/expression/binding_path.hpp b/xo-expression/include/xo/expression/binding_path.hpp new file mode 100644 index 00000000..61e48e87 --- /dev/null +++ b/xo-expression/include/xo/expression/binding_path.hpp @@ -0,0 +1,29 @@ +/* file binding_path.hpp + * + * author: Roland Conybeare, Jul 2024 + */ + +#pragma once + +namespace xo { + namespace scm { + /** @class path + * + * @brief path from the *use* of a variable to the environment + * providing its location. + **/ + struct binding_path { + /** @of parent links to traverse. -1 if global. -2 if sentinel **/ + int i_link_ = -2; + /** for variables bound in some local environment: + * slot# within that environment. + * + * Ignored if @ref i_link_ is -1 + **/ + int j_slot_ = 0; + }; /*binding_path*/ + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end binding_path.hpp */ diff --git a/xo-expression/include/xo/expression/exprtype.hpp b/xo-expression/include/xo/expression/exprtype.hpp new file mode 100644 index 00000000..a32961ee --- /dev/null +++ b/xo-expression/include/xo/expression/exprtype.hpp @@ -0,0 +1,81 @@ +/** @file exprtype.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include +//#include + +namespace xo { + namespace scm { + /** @enum exprtype + * @brief enum to identify subclasses of xo::scm::Expression. + * + **/ + enum class exprtype { + /** sentinel value **/ + invalid = -1, + + /** literal constant. must satisfy both standard_layout_type + trivial **/ + constant, + /** a literal constant that refers to a linkable named function **/ + primitive, + /** variable/function definition **/ + define, + /** variable assignment **/ + assign, + /** function call **/ + apply, + /** function definition **/ + lambda, + /** variable reference **/ + variable, + /** if-then-else **/ + ifexpr, + /** sequence **/ + sequence, + /** type conversion **/ + convert, + + /** not an expression. comes last, counts entries **/ + n_expr + }; + + inline const char * + expr2str(exprtype x) + { + switch(x) { + case exprtype::invalid: return "?exprtype"; + case exprtype::constant: return "constant"; + case exprtype::primitive: return "primitive"; + case exprtype::define: return "define"; + case exprtype::assign: return "assign"; + case exprtype::apply: return "apply"; + case exprtype::lambda: return "lambda"; + case exprtype::variable: return "variable"; + case exprtype::ifexpr: return "if_expr"; + case exprtype::sequence: return "sequence"; + case exprtype::convert: return "convert"; + default: break; + } + + return "???exprtype???"; + } + + /** @brief number of built-in expression types, repr convenient for array sizing **/ + static constexpr std::size_t n_exprtype = static_cast(exprtype::n_expr); + + inline std::ostream & + operator<<(std::ostream & os, + exprtype x) + { + os << expr2str(x); + return os; + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end exprtype.hpp **/ diff --git a/xo-expression/include/xo/expression/llvmintrinsic.hpp b/xo-expression/include/xo/expression/llvmintrinsic.hpp new file mode 100644 index 00000000..fcc26734 --- /dev/null +++ b/xo-expression/include/xo/expression/llvmintrinsic.hpp @@ -0,0 +1,159 @@ +/** @file llvmintrinsic.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +//#include + +namespace xo { + namespace scm { + /** @enum llvminstrinsic + * @brief enum to identify an LLVM instrinsic, e.g. @c IRBuilder::CreateFAdd + * + * Associate an @c llvminstrinsic with an AST @c Primitive p. + * Later, in @c xo::jit::IrPipeline + * - when generating code for @c xo::scm::Apply + * - with *p* is in the function-call position + * can use the associated llvm instrinsic instead of generating a function call + * @c Primitive::value + * + * @note llvm will still sometimes need to use + * @c Primitive::value (and generate a function call sequence), + * for example when handling an @c xo::scm::Apply instance + * where the function position is a computed function. + * @endnote + * + * @note + * LLVM requires separate intrinsics for {ints, floats}. + * It does not need separate intrinsics for different sizes. + * For example IRBuilder::CreateAdd works for + * {8-bit, 16-bit, 32-bit, 64-bit, 128-bit} x {signed, unsigned} integers + * Integer division is an exception; need to choose between i_sdiv and i_udiv + * @endnote + * + * @note + * NSW stands for 'no signed wrap' -> poison value if overflow (costs more) + * NUW stands for 'no unsigned wrap' -> poison value if overflow (costs more) + * @endnote + * + * See: xo-jit/src/jit/MachPipeline.cpp + **/ + enum class llvmintrinsic { + // see /nix/store/x5yz...llvm-18.1.5-dev/include/llvm/IR/IRBuilder.h + + /** sentinel value **/ + invalid = -1, + + /** -> IRBuilder::CreateNeg (negate 1 integer) **/ + i_neg, + + /** -> IRBuilder::CreateAdd (add 2 integers, overflow silently) **/ + i_add, + + /** -> IRBuilder::CreateSub (subtract 2 integers, overflow silently) **/ + i_sub, + + /** -> IRBuilder::CreateMul (multiply 2 integers, overflow silently) **/ + i_mul, + + /** -> IRBuilder::CreateSdiv (divide 2 signed integers) **/ + i_sdiv, + + /** -> IRBuilder::CreateUdiv (divide 2 unsigned integers) **/ + i_udiv, + + /** -> IRBuilder::CreateICmpEQ (test integers for equality) **/ + i_eq, + + /** -> IRBuilder::CreateICmpNE (test integers for inequality) **/ + i_ne, + + /** -> IRBuilder::CreateICmpSGT (test signed integers for greater) **/ + i_sgt, + + /** -> IRBuilder::CreateICmpSGE (test signed integers for greater-or-equal) **/ + i_sge, + + /** -> IRBuilder::CreateICmpSLT (test signed integers for lesser) **/ + i_slt, + + /** -> IRBuilder::CreateCmpSLE (test signed integers for lesser-or-equal) **/ + i_sle, + + // TODO: unsigned comparisons + + /** -> IRBuilder::CreateFAdd (add 2 floating-point numbers) **/ + fp_add, + + /** -> IRBuilder::CreateFSub (subtract 2 floating-pointer numbers) **/ + fp_sub, + + /** -> IRBuilder::CreateFMul (multiply 2 floating-point numbers) **/ + fp_mul, + + /** -> IRBuilder::CreateFDiv (divide 2 floating-point numbers) **/ + fp_div, + + // TODO: floating-point comparisons + + /** + * want to do whatever llvm IR @c llvm.sqrt.f64 and friends do. + * Not sure if that's an always-available function of something else + **/ + fp_sqrt, + + /** WIP **/ + fp_pow, + + /** WIP **/ + fp_sin, + + /** WIP **/ + fp_cos, + + /** WIP **/ + fp_tan, + + /** not an intrinsic. comes last, counts entries **/ + n_intrinsic + }; + + inline const char * + llvmintrinsic2str(llvmintrinsic x) + { + switch(x) { + case llvmintrinsic::invalid: return "?llvminstrinsic"; + case llvmintrinsic::i_neg: return "i_neg"; + case llvmintrinsic::i_add: return "i_add"; + case llvmintrinsic::i_sub: return "i_sub"; + case llvmintrinsic::i_mul: return "i_mul"; + case llvmintrinsic::i_sdiv: return "i_sdiv"; + case llvmintrinsic::i_udiv: return "i_udiv"; + + case llvmintrinsic::i_eq: return "i_eq"; + case llvmintrinsic::i_ne: return "i_ne"; + case llvmintrinsic::i_sgt: return "i_sgt"; + case llvmintrinsic::i_sge: return "i_sge"; + case llvmintrinsic::i_slt: return "i_slt"; + case llvmintrinsic::i_sle: return "i_sle"; + + case llvmintrinsic::fp_add: return "fp_add"; + case llvmintrinsic::fp_sub: return "fp_sub"; + case llvmintrinsic::fp_mul: return "fp_mul"; + case llvmintrinsic::fp_div: return "fp_div"; + case llvmintrinsic::fp_sqrt: return "fp_sqrt"; + case llvmintrinsic::fp_pow: return "fp_pow"; + case llvmintrinsic::fp_sin: return "fp_sin"; + case llvmintrinsic::fp_cos: return "fp_cos"; + case llvmintrinsic::fp_tan: return "fp_tan"; + default: break; + } + + return "???llvmintrinsic???"; + } /*llvmintrinsic2str*/ + } /*namespace scm*/ +} /*namespace xo*/ + +/** end llvmintrinsic.hpp **/ diff --git a/xo-expression/include/xo/expression/pretty_expression.hpp b/xo-expression/include/xo/expression/pretty_expression.hpp new file mode 100644 index 00000000..066ce379 --- /dev/null +++ b/xo-expression/include/xo/expression/pretty_expression.hpp @@ -0,0 +1,31 @@ +/* @file pretty_expression.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "xo/indentlog/print/pretty.hpp" +#include "xo/refcnt/pretty_refcnt.hpp" +#include "Expression.hpp" + +namespace xo { + namespace print { + template<> + struct ppdetail { + static bool print_pretty(const ppindentinfo & ppii, + const xo::scm::GeneralizedExpression & x) { + return x.pretty_print(ppii); + } + }; + + template <> + struct ppdetail { + static bool print_pretty(const ppindentinfo & ppii, + const xo::scm::Expression & x) { + return x.pretty_print(ppii); + } + }; + + } +} diff --git a/xo-expression/include/xo/expression/pretty_localenv.hpp b/xo-expression/include/xo/expression/pretty_localenv.hpp new file mode 100644 index 00000000..779ce96c --- /dev/null +++ b/xo-expression/include/xo/expression/pretty_localenv.hpp @@ -0,0 +1,39 @@ +/* @file pretty_localenv.hpp */ + +#pragma once + +#include "xo/indentlog/print/pretty.hpp" +#include "xo/refcnt/pretty_refcnt.hpp" +#include "LocalSymtab.hpp" + +namespace xo { + namespace print { + template <> + struct ppdetail { + static bool print_pretty(const ppindentinfo & ppii, const xo::scm::SymbolTable & x) { + return x.pretty_print(ppii); + } + }; + + template <> + struct ppdetail { + static bool print_pretty(const ppindentinfo & ppii, const xo::scm::LocalSymtab & x) { + return x.pretty_print(ppii); + } + }; + + template <> + struct ppdetail { + static bool print_pretty(const ppindentinfo & ppii, const xo::scm::LocalSymtab* x) { + if (x) { + return x->pretty_print(ppii); + } else { + ppii.pps()->write("write(reflect::type_name()); + ppii.pps()->write(">"); + return ppii.pps()->has_margin(); + } + } + }; + } +} diff --git a/xo-expression/include/xo/expression/pretty_variable.hpp b/xo-expression/include/xo/expression/pretty_variable.hpp new file mode 100644 index 00000000..c53c11a3 --- /dev/null +++ b/xo-expression/include/xo/expression/pretty_variable.hpp @@ -0,0 +1,27 @@ +/* file pretty_variable.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "pretty_expression.hpp" +#include "Variable.hpp" + +namespace xo { + namespace print { + template <> + struct ppdetail { + static bool print_pretty(const ppindentinfo & ppii, const xo::scm::Variable & x) { + return x.pretty_print(ppii); + } + }; + + template <> + struct ppdetail { + static bool print_pretty(const ppindentinfo & ppii, const xo::scm::Variable * x) { + return x->pretty_print(ppii); + } + }; + } +} diff --git a/xo-expression/include/xo/expression/typeinf/TypeBlueprint.hpp b/xo-expression/include/xo/expression/typeinf/TypeBlueprint.hpp new file mode 100644 index 00000000..b3cac36e --- /dev/null +++ b/xo-expression/include/xo/expression/typeinf/TypeBlueprint.hpp @@ -0,0 +1,89 @@ +/** @file TypeBlueprint.hpp **/ + +#include "xo/refcnt/Refcounted.hpp" +#include "type_ref.hpp" +#include +#include + +namespace xo { + namespace scm { + class TypeBlueprint; + + /** map from a type variable, to contraints on the resolution of that variable **/ + using type_substitution_map = std::map>; + + /** @class TypeBlueprint + * @brief record constraints on a type variable. + * + * Within type unification, a TypeBlueprint represents + * current state of knowledge as to the resolution of a particular type. + * + * Structurally homologous to @ref xo::reflect::TypeDescr, + * but TypeDescr is intended to represent fully-defined types. + * Conversely TypeBlueprint instances will be abandoned once + * a corresponding TypeDescr exists. + **/ + class TypeBlueprint : public xo::ref::Refcount { + public: + using TypeDescr = xo::reflect::TypeDescr; + + public: + TypeBlueprint() = default; + + /** contruct blueprint for type_ref @p ref **/ + static rp make(const type_ref& ref); + /** contruct blueprint for type variable @p name. + * equivalent to @c make(type_ref(name, nullptr)) + **/ + static rp typevar(const type_var& name); + + /** compare two blueprints for equality. + * blueprints are equal iff (we know that) they refer to the same concrete type. + **/ + static bool equals(bp lhs, bp rhs); + + const type_ref& ref() const { return ref_; } + const type_var& id() const { return ref_.id(); } + TypeDescr td() const { return ref_.td(); } + + bool is_concrete() const { return ref_.is_concrete(); } + bool is_variable() const; + + /** upsert into @p *p_typevarset all unresolved type variables **/ + void upsert_typevars(std::set * p_typevar_set) const; + + /** apply substitutions in @p sub_map to this type **/ + bp substitute(const type_substitution_map& sub_map); + + /** replace with resolved type description. + * Promise: + * 1. ref().td() == @p td + * 2. this->is_concrete() == true + * 3. this->is_variable() == false + **/ + void resolve_to(TypeDescr td); + + /** write human-readable representation to stream @p os **/ + void display(std::ostream & os) const; + + private: + /** construct blueprint for @p ref **/ + explicit TypeBlueprint(const type_ref & ref); + + private: + /** name of the type being constrained here **/ + type_ref ref_; + + // additional descriptive info.. + }; + + inline std::ostream & + operator<<(std::ostream & os, const TypeBlueprint & x) { + x.display(os); + return os; + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end TypeBlueprint.hpp **/ diff --git a/xo-expression/include/xo/expression/typeinf/type_ref.hpp b/xo-expression/include/xo/expression/typeinf/type_ref.hpp new file mode 100644 index 00000000..89deec0e --- /dev/null +++ b/xo-expression/include/xo/expression/typeinf/type_ref.hpp @@ -0,0 +1,58 @@ +/** @file type_ref.hpp **/ + +#pragma once + +#include "xo/flatstring/flatstring.hpp" +#include "xo/reflect/TypeDescr.hpp" + +namespace xo { + namespace scm { + using prefix_type = xo::flatstring<8>; + using type_var = xo::flatstring<20>; + + /** @class type_ref + * @brief name and eventual resolution for type associated with an expression. + * + * Type inference / unification operates on + * @ref xo::scm::TypeBlueprint instances, see also. + **/ + class type_ref { + public: + using TypeDescr = xo::reflect::TypeDescr; + + public: + type_ref() = default; + type_ref(const type_var& id, TypeDescr td); + + /** if type not determined (@p td is nullptr), + * -> generate and store type variable name. + * otherwise type already resolvedn + **/ + static type_ref dwim(prefix_type prefix, TypeDescr td); + + /** generate a unique type-variable name, that begins with @p prefix **/ + static type_var generate_unique(prefix_type prefix); + + const type_var& id() const { return id_; } + TypeDescr td() const { return td_; } + + /** true iff type at this location has been resolved **/ + bool is_concrete() const; + + void resolve_to(TypeDescr td); + + private: + /** unique (likely generated) name for type at this location **/ + type_var id_; + /** description for concrete type, once resolved. + * may be null when type_ref created. + * expected to be immutable once established. + * note that TypeDescr itself may be incomplete, + * but not for inference purposes + **/ + TypeDescr td_; + }; + } /*namespace scm*/ +} /*namespace xo*/ + +/** end type_ref.hpp **/ diff --git a/xo-expression/include/xo/expression/typeinf/type_unifier.hpp b/xo-expression/include/xo/expression/typeinf/type_unifier.hpp new file mode 100644 index 00000000..f0ba659c --- /dev/null +++ b/xo-expression/include/xo/expression/typeinf/type_unifier.hpp @@ -0,0 +1,63 @@ +/** @file type_unifier.hpp **/ + +#include "type_ref.hpp" +#include "TypeBlueprint.hpp" +#include + +namespace xo { + namespace scm { + struct unify_result { + /** true iff unification success **/ + bool success_; + /** blueprint (possibly concrete) for unified type **/ + rp unified_; + /** if @ref success_ is false -> non-null source function + * in which contradiction detected + **/ + const char * error_src_function_ = nullptr; + /** if @ref success_ is false -> human-readable error description **/ + std::string error_description_; + }; + + std::ostream & operator<< (std::ostream & os, const unify_result & x); + + /** @class type_unifer + * @brief type unification algorithm + **/ + class type_unifier { + public: + type_unifier() = default; + + /** error message where unification would require both + * 1. equals(s1,t2) + * 2. t2 contains s1 + **/ + static unify_result occurs_error(const char * src_function, + bp t1, + bp t2, + bp s1, + bp s2); + + /** given fact that @p lhs and @p rhs must refer to + * the same type: + * 1. unify their type blueprints to get new blueprint U(lhs.rhs) + * 2. update @ref constraint_map_ so that typevar ids for + * @p lhs and @p rhs refer to U(lhs,rhs) + * 3. also update @ref constraint_map_ for any secondary unifications + * that are discovered + * 4. return error if unification is contradiction encountered. + **/ + unify_result unify(bp lhs, bp rhs); + + /** lookup type variable by @p name, to get resolution **/ + rp lookup(const type_var & name) const; + + private: + type_substitution_map constraint_map_; + }; + + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end type_unifier.hpp **/ diff --git a/xo-expression/src/expression/Apply.cpp b/xo-expression/src/expression/Apply.cpp new file mode 100644 index 00000000..7ab1089c --- /dev/null +++ b/xo-expression/src/expression/Apply.cpp @@ -0,0 +1,203 @@ +/* @file Apply.cpp */ + +#include "Apply.hpp" +#include "PrimitiveExpr.hpp" +#include "exprtype.hpp" +#include "pretty_expression.hpp" +#include "xo/indentlog/print/vector.hpp" +#include "xo/indentlog/print/pretty_vector.hpp" +#include + +namespace xo { + namespace scm { + rp + Apply::make(const rp & fn, + const std::vector> & argv) + { + /* extract result type from function type */ + TypeDescr fn_valuetype = fn->valuetype(); + + if (!fn_valuetype->is_function()) { + throw std::runtime_error + (tostr("Apply::make: found expression F in function position," + " with value-type FT where a function type expected", + xtag("FT", fn_valuetype->short_name()), + xtag("F", fn_valuetype))); + } + + TypeDescr fn_retval_type = fn_valuetype->fn_retval(); + + return new Apply(fn_retval_type, fn, argv); + } + + // ----- integer comparison ----- + + rp + Apply::make_cmp_eq_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_cmp_i64::make_cmp_eq2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_cmp_ne_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_cmp_i64::make_cmp_ne2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_cmp_lt_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_cmp_i64::make_cmp_lt2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_cmp_le_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_cmp_i64::make_cmp_le2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_cmp_gt_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_cmp_i64::make_cmp_gt2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_cmp_ge_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_cmp_i64::make_cmp_ge2_i64(), + {lhs, rhs}); + } + + // ----- integer arithmetic ----- + + rp + Apply::make_add2_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_i64::make_add2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_sub2_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_i64::make_sub2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_mul2_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_i64::make_mul2_i64(), + {lhs, rhs}); + } + + rp + Apply::make_div2_i64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_i64::make_div2_i64(), + {lhs, rhs}); + } + + // ----- floating point arithmetic ----- + + rp + Apply::make_add2_f64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_f64::make_add2_f64(), + {lhs, rhs}); + } + + rp + Apply::make_sub2_f64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_f64::make_sub2_f64(), + {lhs, rhs}); + } + + rp + Apply::make_mul2_f64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_f64::make_mul2_f64(), + {lhs, rhs}); + } + + rp + Apply::make_div2_f64(const rp & lhs, + const rp & rhs) + { + return Apply::make(PrimitiveExpr_f64::make_div2_f64(), + {lhs, rhs}); + } + + void + Apply::attach_envs(bp p) { + fn_->attach_envs(p); + + for (const auto & arg : argv_) + arg->attach_envs(p); + } + + void + Apply::display(std::ostream & os) const { + os << ""; + } + + std::uint32_t + Apply::pretty_print(const ppindentinfo & ppii) const + { + return ppii.pps()->pretty_struct(ppii, "Apply", + refrtag("fn", fn_), + refrtag("argv", argv_)); + +#ifdef OBSOLETE + ppstate * pps = ppii.pps(); + + if (ppii.upto()) { + if (!pps->print_upto("print_upto_tag("fn", fn_)) + return false; + + if (!pps->print_upto_tag("argv", argv_)) + return false; + + return true; + } else { + pps->write("newline_pretty_tag(ppii.ci1(), "fn", fn_); + pps->newline_pretty_tag(ppii.ci1(), "argv", argv_); + pps->write(">"); + + return false; + } +#endif + } + + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end Apply.cpp */ diff --git a/xo-expression/src/expression/AssignExpr.cpp b/xo-expression/src/expression/AssignExpr.cpp new file mode 100644 index 00000000..ce68014c --- /dev/null +++ b/xo-expression/src/expression/AssignExpr.cpp @@ -0,0 +1,103 @@ +/* file AssignExpr.cpp + * + * author: Roland Conybeare + */ + +#include "AssignExpr.hpp" +#include "pretty_expression.hpp" +#include "pretty_variable.hpp" +#include "xo/indentlog/print/tag.hpp" +#include + +namespace xo { + namespace scm { + rp + AssignExpr::make(const rp & lhs, + const rp & rhs) + { + return new AssignExpr(lhs, rhs); + } + + AssignExpr::AssignExpr(const rp & lhs, + const rp & rhs) + : Expression(exprtype::assign, rhs->valuetype()), + lhs_{lhs}, rhs_{rhs} + { + this->free_var_set_ = this->calc_free_variables(); + } + + std::set + AssignExpr::calc_free_variables() const + { + std::set retval = lhs_->get_free_variables(); + + std::set tmp = rhs_->get_free_variables(); + + for (const auto & name : tmp) + retval.insert(name); + + return retval; + } + + std::set + AssignExpr::get_free_variables() const { + return free_var_set_; + } + + std::size_t + AssignExpr::visit_preorder(VisitFn visitor_fn) { + std::size_t n = 1; + + visitor_fn(this); + + n += lhs_->visit_preorder(visitor_fn); + n += rhs_->visit_preorder(visitor_fn); + + return n; + } + + std::size_t + AssignExpr::visit_layer(VisitFn visitor_fn) { + std::size_t n = 1; + + visitor_fn(this); + + n += lhs_->visit_layer(visitor_fn); + n += rhs_->visit_layer(visitor_fn); + + return n; + } + + rp + AssignExpr::xform_layer(TransformFn xform_fn) { + this->lhs_ = Variable::from(lhs_->xform_layer(xform_fn)).promote(); + this->rhs_ = rhs_->xform_layer(xform_fn); + + return xform_fn(this); + } + + void + AssignExpr::attach_envs(bp p) { + lhs_->attach_envs(p); + rhs_->attach_envs(p); + } + + void + AssignExpr::display(std::ostream & os) const { + os << ""; + } + + std::uint32_t + AssignExpr::pretty_print(const ppindentinfo & ppii) const { + return ppii.pps()->pretty_struct(ppii, "AssignExpr", + refrtag("lhs", lhs_), + refrtag("rhs", rhs_)); + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end AssignExpr.cpp */ diff --git a/xo-expression/src/expression/CMakeLists.txt b/xo-expression/src/expression/CMakeLists.txt new file mode 100644 index 00000000..a07efea3 --- /dev/null +++ b/xo-expression/src/expression/CMakeLists.txt @@ -0,0 +1,30 @@ +# expression/CMakeLists.txt + +set(SELF_LIB xo_expression) +set(SELF_SRCS + GeneralizedExpression.cpp + Expression.cpp + DefineExpr.cpp + AssignExpr.cpp + Apply.cpp + Lambda.cpp + Variable.cpp + IfExpr.cpp + Sequence.cpp + GlobalSymtab.cpp + LocalSymtab.cpp + ConvertExpr.cpp + PrimitiveExpr.cpp + typeinf/type_ref.cpp + typeinf/type_unifier.cpp + typeinf/TypeBlueprint.cpp +) + +xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) +# note: deps here must also appear in cmake/xo_expressionConfig.cmake.in +xo_dependency(${SELF_LIB} reflect) +xo_dependency(${SELF_LIB} xo_flatstring) +#xo_dependency(${SELF_LIB} indentlog) +#xo_dependency(${SELF_LIB} subsys) + +# end CMakeLists.txt diff --git a/xo-expression/src/expression/ConvertExpr.cpp b/xo-expression/src/expression/ConvertExpr.cpp new file mode 100644 index 00000000..bb3f9099 --- /dev/null +++ b/xo-expression/src/expression/ConvertExpr.cpp @@ -0,0 +1,61 @@ +/* file ConvertExpr.cpp + * + * author: Roland Conybeare + */ + +#include "ConvertExpr.hpp" +#include "pretty_expression.hpp" + +namespace xo { + namespace scm { + rp + ConvertExpr::make(TypeDescr dest_type, + rp arg) + { + return new ConvertExpr(dest_type, + std::move(arg)); + } + + std::set + ConvertExpr::get_free_variables() const { + if (this->arg_) + return this->arg_->get_free_variables(); + else + return std::set(); + } + + void + ConvertExpr::display(std::ostream & os) const { + os << "valuetype()->short_name()) + << xtag("arg", arg_) + << ">"; + } + + std::uint32_t + ConvertExpr::pretty_print(const ppindentinfo & ppii) const { + return ppii.pps()->pretty_struct(ppii, "Convert", + rtag("dest_type", print::quot(this->valuetype()->short_name())), + refrtag("arg", arg_)); + } + + // ----- ConvertExprAccess ----- + + rp + ConvertExprAccess::make(TypeDescr dest_type, + rp arg) + { + return new ConvertExprAccess(dest_type, + std::move(arg)); + } + + rp + ConvertExprAccess::make_empty() { + return new ConvertExprAccess(nullptr /*dest_type*/, + nullptr /*arg*/); + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end ConvertExpr.cpp */ diff --git a/xo-expression/src/expression/DefineExpr.cpp b/xo-expression/src/expression/DefineExpr.cpp new file mode 100644 index 00000000..093503d8 --- /dev/null +++ b/xo-expression/src/expression/DefineExpr.cpp @@ -0,0 +1,124 @@ +/* file DefineExpr.cpp + * + * author: Roland Conybeare + */ + +#include "DefineExpr.hpp" +#include "Variable.hpp" +#include "pretty_expression.hpp" +#include + +namespace xo { + namespace scm { + rp + DefineExpr::make(std::string lhs_name, + rp rhs) + { + TypeDescr rhs_valuetype = nullptr; + + if (rhs) + rhs_valuetype = rhs->valuetype(); + + return new DefineExpr(rhs_valuetype, + std::move(lhs_name), + std::move(rhs)); + } /*make*/ + + DefineExpr::DefineExpr(TypeDescr rhs_valuetype, + std::string lhs_name, + rp rhs) + : Expression(exprtype::define, rhs_valuetype), + lhs_var_{Variable::make(lhs_name, rhs_valuetype)}, + rhs_{std::move(rhs)} + { + this->free_var_set_ = this->calc_free_variables(); + } + + const std::string & + DefineExpr::lhs_name() const { return lhs_var_->name(); } + + std::set + DefineExpr::calc_free_variables() const + { + std::set retval; + + if (rhs_) + retval = rhs_->get_free_variables(); + + /* but remove this variable */ + if (!this->lhs_name().empty()) + retval.erase(this->lhs_name()); + + return retval; + } /*calc_free_variables*/ + + void + DefineExpr::display(std::ostream & os) const { + os << "name()) + << xtag("rhs", rhs_) + << ">"; + } /*display*/ + + std::uint32_t + DefineExpr::pretty_print(const ppindentinfo & ppii) const + { + return ppii.pps()->pretty_struct(ppii, "Define", + //refrtag("type", this->valuetype()), // need pretty + refrtag("name", lhs_var_->name()), + refrtag("rhs", rhs_)); + } + + // ----- DefineExprAccess ----- + + rp + DefineExprAccess::make(std::string lhs_name, + rp rhs) + { + TypeDescr rhs_valuetype = nullptr; + + if (rhs) + rhs_valuetype = rhs->valuetype(); + + return new DefineExprAccess(rhs_valuetype, + std::move(lhs_name), + std::move(rhs)); + } + + rp + DefineExprAccess::make_empty() + { + return new DefineExprAccess(nullptr /*rhs_valuetype*/, + "" /*lhs_name*/, + nullptr /*rhs*/); + } + + void + DefineExprAccess::assign_lhs_name(const std::string & x) + { + this->lhs_var_->assign_name(x); + } + + void + DefineExprAccess::assign_rhs(const rp & x) + { + assert(x); + + this->rhs_ = x; + + if (x) { + if (lhs_var_ && !lhs_var_->valuetype()) { + this->lhs_var_->assign_valuetype(x->valuetype()); + } + + this->assign_valuetype(x->valuetype()); + } + + this->free_var_set_ = this->calc_free_variables(); + } + + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end DefineExpr.cpp */ diff --git a/xo-expression/src/expression/Expression.cpp b/xo-expression/src/expression/Expression.cpp new file mode 100644 index 00000000..16b31171 --- /dev/null +++ b/xo-expression/src/expression/Expression.cpp @@ -0,0 +1,11 @@ +/* @file Expression.cpp */ + +#include "Expression.hpp" + +namespace xo { + namespace scm { + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end Expression.cpp */ diff --git a/xo-expression/src/expression/GeneralizedExpression.cpp b/xo-expression/src/expression/GeneralizedExpression.cpp new file mode 100644 index 00000000..b0aa6faf --- /dev/null +++ b/xo-expression/src/expression/GeneralizedExpression.cpp @@ -0,0 +1,76 @@ +/* @file GeneralizedExpression.cpp */ + +#include "GeneralizedExpression.hpp" +#include "pretty_expression.hpp" +#include + +namespace xo { + namespace scm { + namespace { + using xo::scm::prefix_type; + + prefix_type exprtype2prefix(exprtype x) + { + switch (x) { + case exprtype::invalid: assert(false); break; + case exprtype::constant: return prefix_type::from_chars("k"); + case exprtype::primitive: return prefix_type::from_chars("pm"); + case exprtype::define: return prefix_type::from_chars("def"); + case exprtype::assign: return prefix_type::from_chars("="); + case exprtype::apply: return prefix_type::from_chars("@"); + case exprtype::lambda: return prefix_type::from_chars("lm"); + case exprtype::variable: return prefix_type::from_chars("var"); + case exprtype::ifexpr: return prefix_type::from_chars("if"); + case exprtype::sequence: return prefix_type::from_chars("seq"); + case exprtype::convert: return prefix_type::from_chars("cvt"); + case exprtype::n_expr: assert(false); break; + } + + return prefix_type::from_chars("?expr"); + } + } + + GeneralizedExpression::GeneralizedExpression(exprtype extype, + TypeDescr valuetype) + : extype_{extype}, + valuetype_ref_{type_ref::dwim(exprtype2prefix(extype), valuetype)} + {} + + GeneralizedExpression::GeneralizedExpression(exprtype extype, + prefix_type prefix, + TypeDescr valuetype) + : extype_{extype}, + valuetype_ref_{type_ref::dwim(prefix, valuetype)} + {} + + std::string + GeneralizedExpression::display_string() const { + return tostr(*this); + } + +#ifdef SUPERSEDED // currently all derived expression types support pretty printing + std::uint32_t + GeneralizedExpression::pretty_print(const ppindentinfo & ppii) const { + // Slooooow fallback for subtypes that don't implement pretty printing support + // Currently have support for: + // - Variable + // - Lambda + // - DefineExpr + // - Sequence + // - Apply + // - Primitive + // - IfExpr + + ppstate * pps = ppii.pps(); + std::uint32_t saved = pps->pos(); + pps->write(display_string()); + if (ppii.upto() && !pps->has_margin()) + return false; + + return ppii.upto() ? pps->scan_no_newline(saved) : true; + } +#endif + } /*namespace scm*/ +} /*namespace xo*/ + +/* end GeneralizedExpression.cpp */ diff --git a/xo-expression/src/expression/GlobalSymtab.cpp b/xo-expression/src/expression/GlobalSymtab.cpp new file mode 100644 index 00000000..b932152e --- /dev/null +++ b/xo-expression/src/expression/GlobalSymtab.cpp @@ -0,0 +1,61 @@ +/* file GlobalEnv.cpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#include "xo/indentlog/print/ppdetail_atomic.hpp" +#include "GlobalSymtab.hpp" +#include "Expression.hpp" + +namespace xo { + namespace scm { + GlobalSymtab::GlobalSymtab() = default; + + bp + GlobalSymtab::require_global(const std::string & vname, + bp expr) + { + this->global_map_[vname] = expr.get(); + + return expr; + } /*require_global*/ + + void + GlobalSymtab::upsert_local(bp target) { + // in practice: paraphrase of .require_global() + + this->global_map_[target->name()] = target.promote(); + } + + void + GlobalSymtab::print(std::ostream & os) const { + os << ""; + } + + std::uint32_t + GlobalSymtab::pretty_print(const xo::print::ppindentinfo & ppii) const + { + using xo::print::ppstate; + + ppstate * pps = ppii.pps(); + + if (ppii.upto()) { + if (!pps->print_upto("print_upto_tag("size", global_map_.size())) + return false; + pps->write(">"); + + return true; + } else { + pps->write("newline_pretty_tag(ppii.ci1(), "size", global_map_.size()); + pps->write(">"); + + return false; + } + } + } /*namespace scm*/ +} /*namespace xo*/ diff --git a/xo-expression/src/expression/IfExpr.cpp b/xo-expression/src/expression/IfExpr.cpp new file mode 100644 index 00000000..b08fe6e7 --- /dev/null +++ b/xo-expression/src/expression/IfExpr.cpp @@ -0,0 +1,102 @@ +/* @file IfExpr.cpp */ + +#include "IfExpr.hpp" +#include "pretty_expression.hpp" +#include "pretty_variable.hpp" +//#include "xo/indentlog/print/vector.hpp" + +namespace xo { + namespace scm { + auto IfExpr::check_consistent_valuetype(const rp & when_true, + const rp & when_false) -> TypeDescr + { + if (when_true->valuetype() != when_false->valuetype()) + return nullptr; + + return when_true->valuetype(); + } + + void IfExpr::establish_valuetype() + { + if (this->when_true_.get() && this->when_false_.get()) + this->assign_valuetype(check_consistent_valuetype(this->when_true_, this->when_false_)); + } + + rp + IfExpr::make(const rp & test, + const rp & when_true, + const rp & when_false) + { + /** TODO: verify test returns _boolean_ type **/ + + if (when_true->valuetype() != when_false->valuetype()) { + throw std::runtime_error + (tostr("IfExpr::make:" + " types {T1,T2} found for branches of if-expr" + " where equal types expected", + xtag("T1", when_true->valuetype()->canonical_name()), + xtag("T2", when_false->valuetype()->canonical_name()))); + } + + /* arbitrary choice here */ + auto ifexpr_type = when_true->valuetype(); + + return new IfExpr(ifexpr_type, + test, + when_true, + when_false); + } /*make*/ + + void + IfExpr::display(std::ostream & os) const { + os << ""; + } /*display*/ + + std::uint32_t + IfExpr::pretty_print(const ppindentinfo & ppii) const { + return ppii.pps()->pretty_struct(ppii, "IfExpr", + refrtag("test", test_), + refrtag("when_true", when_true_), + refrtag("when_false", when_false_)); + } + + rp + IfExprAccess::make(rp test, + rp when_true, + rp when_false) + { + auto ifexpr_type = check_consistent_valuetype(when_true, when_false); + + return new IfExprAccess(ifexpr_type, std::move(test), std::move(when_true), std::move(when_false)); + } + + rp + IfExprAccess::make_empty() + { + return new IfExprAccess(nullptr /*ifexpr_valuetype*/, + nullptr /*test*/, + nullptr /*when_true*/, + nullptr /*when_false*/); + } + + void + IfExprAccess::assign_when_true(rp x) + { + this->when_true_ = std::move(x); + } + + void + IfExprAccess::assign_when_false(rp x) + { + this->when_false_ = std::move(x); + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end IfExpr.cpp */ diff --git a/xo-expression/src/expression/Lambda.cpp b/xo-expression/src/expression/Lambda.cpp new file mode 100644 index 00000000..0196f52e --- /dev/null +++ b/xo-expression/src/expression/Lambda.cpp @@ -0,0 +1,410 @@ +/* @file Lambda.cpp */ + +#include "Lambda.hpp" +#include "exprtype.hpp" +#include "pretty_expression.hpp" +#include "pretty_variable.hpp" +#include "xo/reflect/TypeDescr.hpp" +#include "xo/reflect/function/FunctionTdx.hpp" +#include "xo/indentlog/print/vector.hpp" +#include "xo/indentlog/print/pretty_vector.hpp" +#include +#include + +namespace xo { + using xo::reflect::TypeDescr; + using xo::reflect::TypeDescrBase; + using xo::reflect::FunctionTdxInfo; + using std::stringstream; + + namespace scm { + TypeDescr + Lambda::assemble_lambda_td(const std::vector> & argv, + TypeDescr return_td) + { + assert(return_td != nullptr); + + std::vector arg_td_v; + { + arg_td_v.reserve(argv.size()); + + for (const auto & arg : argv) { + arg_td_v.push_back(arg->valuetype()); + } + } + + auto function_info + = FunctionTdxInfo(return_td, + arg_td_v, + false /*!is_noexcept*/); + + TypeDescr lambda_td + = TypeDescrBase::require_by_fn_info(function_info); + + return lambda_td; + } + + TypeDescr + Lambda::assemble_lambda_td(const std::vector> & argv, + TypeDescr explicit_return_td, + const rp & body) + { + if (!body) + return nullptr; + + if (explicit_return_td && body->valuetype() && (explicit_return_td != body->valuetype())) { + throw std::runtime_error(tostr("explicit lambda return type T1 conflicts with lambda body T2", + xtag("T1", explicit_return_td), + xtag("T2", body->valuetype()))); + } + + // TODO: unify(explicit_return_td, body->valuetype()) + + TypeDescr return_td = explicit_return_td ? explicit_return_td : body->valuetype(); + + return assemble_lambda_td(argv, return_td); + } + + std::string + Lambda::assemble_type_str(TypeDescr lambda_td) { + assert(lambda_td); + + std::stringstream ss; + + ss << lambda_td->fn_retval()->short_name() + << "("; + + for (std::size_t i = 0, n = lambda_td->n_fn_arg(); i < n; ++i) { + if (i > 0) + ss << ","; + ss << lambda_td->fn_arg(i)->short_name(); + } + ss << ")"; + + return ss.str(); + } + + rp + Lambda::make(const std::string & name, + TypeDescr lambda_td, + const rp & env, + const rp & body) + { + return new Lambda(name, lambda_td, env, body); + } + + rp + Lambda::make_from_env(const std::string & name, + const rp & env, + TypeDescr explicit_return_td, + const rp & body) + { + TypeDescr lambda_td = assemble_lambda_td(env->argv(), explicit_return_td, body); + + rp retval + = new Lambda(name, + lambda_td, + env, + body); + + /* need two-phase construction b/c pointer cycle */ + env->assign_origin(retval.get()); + + return retval; + } + + rp + Lambda::make(const std::string & name, + const std::vector> & argv, + const rp & body, + const rp & parent_env) + { + rp env = LocalSymtab::make(argv, parent_env); + + TypeDescr explicit_return_td = nullptr; + + return make_from_env(name, env, explicit_return_td, body); + } /*make*/ + + std::set + Lambda::calc_free_variables() const + { + std::set retval + = body_->get_free_variables(); + + /* but remove formals. */ + for (const auto & var : local_env_->argv()) + retval.erase(var->name()); + + return retval; + } /*calc_free_variables*/ + + std::map> + Lambda::regularize_layer_vars() + { + /* regularize local_env+body: make sure exactly one instance + * (i.e. with object identity) of a Variable appears + * within one layer of a lambda body. + * + * Here 'layer' means excluding appearance in any nested lambdas + * (i.e. whether or not such appearance would resolve to the same + * memory location). + * + * Motivation is to unify Variables that would use the same + * binding_path to resolve their runtime location. + */ + std::map> var_map; + + for (const auto & arg : local_env_->argv()) { + /* each arg name can appear at most once + * in a particular lambda's parameter list + */ + assert(var_map.find(arg->name()) == var_map.end()); + + var_map[arg->name()] = arg; + } + + this->body_ + = (body_->xform_layer + ([&var_map](bp x) -> rp + { + if (x->extype() == exprtype::variable) { + bp var = Variable::from(x); + + auto ix = var_map.find(var->name()); + if (ix == var_map.end()) { + /* add to var_map, copy to ensure Variable + * not shared with any other layer + */ + + var_map[var->name()] = Variable::copy(var); + + return var.get(); + } else { + /* substitute already-encountered var_map[] member */ + return ix->second.get(); + } + } else { + return x.get(); + } + })); + + return var_map; + } /*regularize_layer_vars*/ + + void + Lambda::complete_assembly_from_body() { + if (body_) { + TypeDescr explicit_return_td = nullptr; + TypeDescr lambda_td + = assemble_lambda_td(this->local_env_->argv(), explicit_return_td, body_); + + if (lambda_td) + this->type_str_ = assemble_type_str(lambda_td); + + this->layer_var_map_ = this->regularize_layer_vars(); + + this->free_var_set_ = this->calc_free_variables(); + + std::map> nested_lambda_map; + { + this->body_->visit_layer + ([&nested_lambda_map] + (bp expr) + { + if (expr->extype() == exprtype::lambda) { + bp lm = Lambda::from(expr); + + nested_lambda_map[lm->name()] = lm.get(); + } + }); + } + this->nested_lambda_map_ = std::move(nested_lambda_map); + + /* establish the set of captured local vars. + * These are any formal parameters that appear free in + * any layer of a nested lambda. + */ + std::set captured_var_set; + { + for (const auto & ix : nested_lambda_map_) { + std::set nested_free_var_set + = ix.second->get_free_variables(); + + for (const auto & jx : nested_free_var_set) { + /* check whether variable *jx is one of this lambda's + * formals + */ + auto bind = this->local_env_->lookup_local_binding(jx); + + if (bind.i_link_ == 0) { + /* yup, it's a formal parameter of this lambda */ + captured_var_set.insert(jx); + } + } + } + } + + this->captured_var_set_ = std::move(captured_var_set); + + /* in particular: + * - establish binding path (intrusively) for each variable + * assigns Variable::path_ + */ + this->body_->attach_envs(local_env_); + } + } + + Lambda::Lambda(const std::string & name, + TypeDescr lambda_td, + const rp & local_env, + const rp & body) + : ProcedureExprInterface(exprtype::lambda, lambda_td), + name_{name}, + body_{body}, + local_env_{local_env} + { +#ifdef OBSOLETE + stringstream ss; + ss << "double"; + ss << "("; + for (std::size_t i = 0, n = this->n_arg(); i < n; ++i) { + if (i > 0) + ss << ","; + ss << "double"; + } + ss << ")"; +#endif + + if (lambda_td) + this->type_str_ = assemble_type_str(lambda_td); + + /* ensure variables are unique within layer for this lambda */ + this->layer_var_map_ = this->regularize_layer_vars(); + + this->free_var_set_ = this->calc_free_variables(); + + std::map> nested_lambda_map; + { + this->body_->visit_layer + ([&nested_lambda_map] + (bp expr) + { + if (expr->extype() == exprtype::lambda) { + bp lm = Lambda::from(expr); + + nested_lambda_map[lm->name()] = lm.get(); + } + }); + } + this->nested_lambda_map_ = std::move(nested_lambda_map); + + /* establish the set of captured local vars. + * These are any formal parameters that appear free in + * any layer of a nested lambda. + */ + std::set captured_var_set; + { + for (const auto & ix : nested_lambda_map_) { + std::set nested_free_var_set + = ix.second->get_free_variables(); + + for (const auto & jx : nested_free_var_set) { + /* check whether variable *jx is one of this lambda's + * formals + */ + auto bind = this->local_env_->lookup_local_binding(jx); + + if (bind.i_link_ == 0) { + /* yup, it's a formal parameter of this lambda */ + captured_var_set.insert(jx); + } + } + } + } + + this->captured_var_set_ = std::move(captured_var_set); + + /* in particular: + * - establish binding path (intrusively) for each variable + * assigns Variable::path_ + */ + this->body_->attach_envs(local_env_); + + } /*ctor*/ + + void + Lambda::attach_envs(bp p) { + local_env_->assign_parent(p); + + /** establish a binding path for each variable **/ + } + + void + Lambda::display(std::ostream & os) const { + os << "argv()) + << xtag("body", body_) + << ">"; + } /*display*/ + + std::uint32_t + Lambda::pretty_print(const ppindentinfo & ppii) const + { + return ppii.pps()->pretty_struct(ppii, "Lambda", + refrtag("name", name_), + refrtag("argv", local_env_->argv()), + refrtag("body", body_)); + } + + // ----- Lambda Access ----- + + rp + LambdaAccess::make(const std::string & name, + const std::vector> & argv, + const rp & body, + const rp & parent_env) + { + TypeDescr explicit_return_td = nullptr; + TypeDescr lambda_td = assemble_lambda_td(argv, explicit_return_td, body); + rp env = LocalSymtab::make(argv, parent_env); + + rp retval + = new LambdaAccess(name, + lambda_td, + env, + body); + + /* need two-phase construction b/c pointer cycle */ + env->assign_origin(retval.get()); + + return retval; + } + + rp + LambdaAccess::make_empty() + { + return new LambdaAccess("" /*name*/, + nullptr /*lambda_td*/, + nullptr /*local_env*/, + nullptr /*body*/); + } + + LambdaAccess::LambdaAccess(const std::string & name, + TypeDescr lambda_td, + const rp & local_env, + const rp & body) + : Lambda(name, lambda_td, local_env, body) + {} + + void + LambdaAccess::assign_body(const rp & body) { + this->body_ = body; + + this->complete_assembly_from_body(); + } + } /*namespace scm*/ +} /*namespace xo*/ + +/* end Lambda.cpp */ diff --git a/xo-expression/src/expression/LocalSymtab.cpp b/xo-expression/src/expression/LocalSymtab.cpp new file mode 100644 index 00000000..db5f67b9 --- /dev/null +++ b/xo-expression/src/expression/LocalSymtab.cpp @@ -0,0 +1,135 @@ +/* file LocalSymtab.cpp + * + * author: Roland Conybeare + */ + +#include "LocalSymtab.hpp" +#include "pretty_variable.hpp" +#include "xo/indentlog/print/pretty_vector.hpp" +#include "xo/indentlog/print/vector.hpp" + + +namespace xo { + namespace scm { + rp + LocalSymtab::make_empty() { + return new LocalSymtab(std::vector>(), nullptr); + } + + rp + LocalSymtab::make(const std::vector> & argv, + const rp & parent_env) + { + return new LocalSymtab(argv, parent_env); + } + + rp + LocalSymtab::make1(const rp & arg1, + const rp & parent_env) + { + std::vector> argv = { arg1 }; + + return make(argv, parent_env); + } + + LocalSymtab::LocalSymtab(const std::vector> & argv, + const rp & parent_env) + : origin_{nullptr}, + argv_(argv), + parent_env_{parent_env} + { + constexpr bool c_debug_flag = true; + scope log(XO_DEBUG(c_debug_flag), xtag("this", (void*)this), xtag("argv", argv_)); + } + + binding_path + LocalSymtab::lookup_local_binding(const std::string & vname) const { + int j_slot = 0; + for (const auto & arg : argv_) { + if (arg->name() == vname) + return { 0 /*i_link*/, j_slot }; + ++j_slot; + } + + return { -2 /*i_link: sentinel*/, 0 }; + } /*lookup_local_binding*/ + + binding_path + LocalSymtab::lookup_binding(const std::string & vname) const { + { + auto local = this->lookup_local_binding(vname); + if (local.i_link_ == 0) + return local; + } + + auto free = parent_env_->lookup_binding(vname); + + if (free.i_link_ == -1) + return free; + else + return { free.i_link_ + 1, free.j_slot_ }; + } /*lookup_binding*/ + + void + LocalSymtab::assign_parent(bp p) { + if ((parent_env_.get() != nullptr) && (parent_env_.get() != p.get())) { + throw std::runtime_error(tostr("LocalSymtab::assign_parent(P2): already have established parent P1", + xtag("P1", parent_env_), + xtag("P2", p))); + + assert(false); + } + + parent_env_ = p.promote(); + } + + void + LocalSymtab::upsert_local(bp target) { + for (auto & var : this->argv_) { + if (var->name() == target->name()) { + /* replace existing variable. This may change its type */ + var = target.promote(); + return; + } + } + + /* control here: target not already present in this frame -> append */ + + this->argv_.push_back(target.promote()); + } + + void + LocalSymtab::print(std::ostream& os) const { + os << ""; + } + + std::uint32_t + LocalSymtab::pretty_print(const xo::print::ppindentinfo & ppii) const { + using xo::print::ppstate; + + ppstate * pps = ppii.pps(); + + if (ppii.upto()) { + if (!pps->print_upto("print_upto_tag("argv", argv_)) + return false; + pps->write(">"); + + return true; + } else { + pps->write("newline_pretty_tag(ppii.ci1(), "this", (void*)this); + pps->newline_pretty_tag(ppii.ci1(), "argv", argv_); + pps->write(">"); + + return false; + } + } + + } /*namespace scm*/ +} /*namespace xo*/ + +/* end LocalSymtab.cpp */ diff --git a/xo-expression/src/expression/PrimitiveExpr.cpp b/xo-expression/src/expression/PrimitiveExpr.cpp new file mode 100644 index 00000000..1cdc18a8 --- /dev/null +++ b/xo-expression/src/expression/PrimitiveExpr.cpp @@ -0,0 +1,296 @@ +/* @file PrimitiveExpr.cpp */ + +#include "PrimitiveExpr.hpp" +#include + +extern "C" { + /** code here is used in two context: + * 1. Fallback implementation under llvm. + * In practice will use llvm intrinsic instead. + * See xo-jit/src/jit/MachPipeline.cpp + * 2. Schematika interpreter (aspirational asof jul 2025, wip nov 2025) + * For schematika interpreter need to uplift + * these to obj::Primitive. + * See BuiltinPrimitives::install_interpreter_conversions() + * in xo-interpreter/src/BuiltinPrimitives.cpp + **/ + + bool + cmp_eq2_i64(std::int64_t x, std::int64_t y) { + return x == y; + } + + bool + cmp_ne2_i64(std::int64_t x, std::int64_t y) { + return x != y; + } + + bool + cmp_lt2_i64(std::int64_t x, std::int64_t y) { + return x < y; + } + + bool + cmp_le2_i64(std::int64_t x, std::int64_t y) { + return x <= y; + } + + bool + cmp_gt2_i64(std::int64_t x, std::int64_t y) { + return x > y; + } + + bool + cmp_ge2_i64(std::int64_t x, std::int64_t y) { + return x >= y; + } + + std::int64_t + add2_i64(std::int64_t x, std::int64_t y) { + return x + y; + } + + std::int64_t + sub2_i64(std::int64_t x, std::int64_t y) { + return x - y; + } + + std::int64_t + mul2_i64(std::int64_t x, std::int64_t y) { + return x * y; + } + + std::int64_t + div2_i64(std::int64_t x, std::int64_t y) { + return x / y; + } + + // ---------------------------------------------------------------- + + double + add2_f64(double x, double y) { + return x + y; + } + + double + sub2_f64(double x, double y) { + return x - y; + } + + double + mul2_f64(double x, double y) { + return x * y; + } + + double + div2_f64(double x, double y) { + return x / y; + } +} + +namespace xo { + namespace scm { + auto + PrimitiveExpr_cmp_i64::make_cmp_eq2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@cmp_eq2_i64", + &cmp_eq2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_eq); + + return s_retval; + } + + auto + PrimitiveExpr_cmp_i64::make_cmp_ne2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@cmp_ne2_i64", + &cmp_ne2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_ne); + + return s_retval; + } + + auto + PrimitiveExpr_cmp_i64::make_cmp_lt2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@cmp_lt2_i64", + &cmp_lt2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_slt); + + return s_retval; + } + + auto + PrimitiveExpr_cmp_i64::make_cmp_le2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@cmp_le2_i64", + &cmp_le2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_sle); + + return s_retval; + } + + auto + PrimitiveExpr_cmp_i64::make_cmp_gt2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@cmp_gt2_i64", + &cmp_gt2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_sgt); + + return s_retval; + } + + auto + PrimitiveExpr_cmp_i64::make_cmp_ge2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@cmp_ge2_i64", + &cmp_ge2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_sge); + + return s_retval; + } + + /* TODO: remaining integer arithmetic */ + + auto + PrimitiveExpr_i64::make_add2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@add2_i64", + &add2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_add); + + return s_retval; + } + + auto + PrimitiveExpr_i64::make_sub2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@sub2_i64", + &sub2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_sub); + + return s_retval; + } + + auto + PrimitiveExpr_i64::make_mul2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@mul2_i64", + &mul2_i64, + true /*explicit_symbol_def*/, + llvmintrinsic::i_mul); + + return s_retval; + } + + auto + PrimitiveExpr_i64::make_div2_i64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@div2_i64", + &div2_i64, + true /*explicit_symbol+def*/, + llvmintrinsic::i_sdiv); + return s_retval; + } + + // ----- floating-point arithmetic ----- + + auto + PrimitiveExpr_f64::make_add2_f64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@add2_f64", + &add2_f64, + true /*explicit_symbol_def*/, + llvmintrinsic::fp_add); + + return s_retval; + } + + auto + PrimitiveExpr_f64::make_sub2_f64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@sub2_f64", + &sub2_f64, + true /*explicit_symbol_def*/, + llvmintrinsic::fp_sub); + + return s_retval; + } + + auto + PrimitiveExpr_f64::make_mul2_f64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@mul2_f64", + &mul2_f64, + true /*explicit_symbol_def*/, + llvmintrinsic::fp_mul); + + return s_retval; + } + + auto + PrimitiveExpr_f64::make_div2_f64() -> rp + { + static rp s_retval; + + if (!s_retval) + s_retval = PrimitiveExpr::make("@div2_f64", + &div2_f64, + true /*explicit_symbol_def*/, + llvmintrinsic::fp_div); + + return s_retval; + } + + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end PrimitiveExpr.cpp */ diff --git a/xo-expression/src/expression/Sequence.cpp b/xo-expression/src/expression/Sequence.cpp new file mode 100644 index 00000000..55a7d525 --- /dev/null +++ b/xo-expression/src/expression/Sequence.cpp @@ -0,0 +1,122 @@ +/* @file Sequence.cpp */ + +#include "Sequence.hpp" +#include "pretty_expression.hpp" +#include + +namespace xo { + namespace scm { + std::set + Sequence::get_free_variables() const { + std::set retval; + + for (const auto & x : expr_v_) { + std::set free_vars; + free_vars = x->get_free_variables(); + + for (const auto & y : free_vars) + retval.insert(y); + } + + return retval; + } + + std::size_t + Sequence::visit_preorder(VisitFn visitor_fn) { + std::size_t n = 1; + + visitor_fn(this); + + for (const auto & x : expr_v_) + n += x->visit_preorder(visitor_fn); + + return n; + } + + std::size_t + Sequence::visit_layer(VisitFn visitor_fn) { + std::size_t n = 1; + + visitor_fn(this); + + for (const auto & x : expr_v_) + n += x->visit_layer(visitor_fn); + + return n; + } + + rp + Sequence::xform_layer(TransformFn xform_fn) { + for (std::size_t i = 0, n = expr_v_.size(); i < n; ++i) { + expr_v_[i] = expr_v_[i]->xform_layer(xform_fn); + } + + return xform_fn(this); + } + + void + Sequence::attach_envs(bp p) { + for (const auto & x : expr_v_) + x->attach_envs(p); + } + + void + Sequence::display(std::ostream & os) const { + os << ""; + } + + std::uint32_t + Sequence::pretty_print(const ppindentinfo & ppii) const + { + ppstate * pps = ppii.pps(); + + if (ppii.upto()) { + if (!pps->print_upto("has_margin()) + return false; + + std::string i_str = tostr("[", i, "]"); + if (!pps->print_upto_tag(i_str.c_str(), expr_i)) + return false; + ++i; + } + + if (!pps->has_margin()) + return false; + + pps->write(">"); + + return true; + } else { + pps->write("newline_pretty_tag(ppii.ci1(), + i_str.c_str(), + expr_i); + ++i; + } + + pps->write(">"); + return false; + } + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end Sequence.cpp */ diff --git a/xo-expression/src/expression/Variable.cpp b/xo-expression/src/expression/Variable.cpp new file mode 100644 index 00000000..b44a9053 --- /dev/null +++ b/xo-expression/src/expression/Variable.cpp @@ -0,0 +1,57 @@ +/* @file Variable.cpp */ + +#include "Variable.hpp" +#include "SymbolTable.hpp" +#include "pretty_expression.hpp" + +namespace xo { + namespace scm { + std::string + Variable::gensym(const std::string & prefix) { + static std::size_t s_counter = 0; + + ++s_counter; + + char buf[32]; + snprintf(buf, sizeof(buf), "%ld", s_counter); + + return prefix + std::string(buf); + } + + void + Variable::attach_envs(bp e) { + /** e makes accessible all enclosing lexical scopes **/ + if (this->path_.i_link_ == -2 /*sentinel*/) { + this->path_ = e->lookup_binding(this->name_); + } else { + /* have already established binding for this Variable */ + } + } /*attach_envs*/ + + void + Variable::display(std::ostream & os) const { + os << "valuetype()) + os << xtag("type", this->valuetype()->short_name()); + else + os << xtag("type", "nullptr"); + os << ">"; + } /*display*/ + + std::uint32_t + Variable::pretty_print(const ppindentinfo & ppii) const { + /* 1. rtag instead of refrtag: + * print::quot() is a temporary rvalue; lifetime ends before control enters pretty_struct() + */ + return ppii.pps()->pretty_struct(ppii, "Variable", + refrtag("name", name_), + rtag("type", print::unq(this->valuetype() + ? this->valuetype()->short_name() + : "nullptr"))); + } + } /*namespace scm*/ +} /*namespace xo*/ + + +/* end Variable.cpp */ diff --git a/xo-expression/src/expression/intrinsics.cpp b/xo-expression/src/expression/intrinsics.cpp new file mode 100644 index 00000000..27dcd83a --- /dev/null +++ b/xo-expression/src/expression/intrinsics.cpp @@ -0,0 +1,14 @@ +/* @file intrinsics.cpp */ + +#include "intrinsics.hpp" + +/* FIXME: don't know how to mangle symbols yet, + * so putting functions invoked from jit into global namespace + */ +extern "C" +int32_t +mul_i32(int32_t x, int32_t y) { + return x * y; +} + +/* end intrinsics.cpp */ diff --git a/xo-expression/src/expression/typeinf/TypeBlueprint.cpp b/xo-expression/src/expression/typeinf/TypeBlueprint.cpp new file mode 100644 index 00000000..4f1573ab --- /dev/null +++ b/xo-expression/src/expression/typeinf/TypeBlueprint.cpp @@ -0,0 +1,112 @@ +/** @file TypeBlueprint.cpp **/ + +#include "typeinf/TypeBlueprint.hpp" + +namespace xo { + namespace scm { + TypeBlueprint::TypeBlueprint(const type_ref & x) + : ref_{x} + {} + + rp + TypeBlueprint::make(const type_ref & ref) + { + return new TypeBlueprint(ref); + } + + rp + TypeBlueprint::typevar(const type_var & name) + { + return new TypeBlueprint(type_ref(name, nullptr)); + } + + bool + TypeBlueprint::equals(bp lhs, bp rhs) + { + // 1. two concrete blueprints are equal if they resolve to the same type. + // 2. two type variables are equal if they have the same unique name; + // but: once we introduce structural constraints will relax this + + if (lhs->is_concrete() && rhs->is_concrete()) + { + return lhs->td() == rhs->td(); + } + + if (lhs->id() == rhs->id()) + { + // typevar names are globally unique, + // so two typevars with the same name must refer to the same type + return true; + } + + // TODO: structural comparisons.. + + return false; + } + + bool + TypeBlueprint::is_variable() const + { + // TODO; + // if we have structural information about this type, + // e.g. vector[t'] or function(a' -> b'), + // then must return false here + + return !ref_.is_concrete(); + } + + void + TypeBlueprint::upsert_typevars(std::set * p_typevar_set) const + { + if (this->is_concrete()) { + return; + } + + // TODO: handle structural types + + p_typevar_set->insert(ref_.id()); + } + bp + TypeBlueprint::substitute(const type_substitution_map& sub_map) + { + bp subject = this; + + // loop here should only run once. + // we collapse sub_map whenever we extend it. + // + while(!subject->is_concrete()) { + auto ix = sub_map.find(subject->id()); + + if (ix == sub_map.end()) + break; + + subject = ix->second.get(); + } + + // TODO: also want to update the whole chain, + // so that everything refers to final subjectc + + return subject; + } + + void + TypeBlueprint::resolve_to(TypeDescr td) + { + ref_.resolve_to(td); + } + + void + TypeBlueprint::display(std::ostream & os) const + { + os << "canonical_name()); + os << ">"; + } + + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end TypeBlueprint.cpp **/ diff --git a/xo-expression/src/expression/typeinf/type_ref.cpp b/xo-expression/src/expression/typeinf/type_ref.cpp new file mode 100644 index 00000000..b6512337 --- /dev/null +++ b/xo-expression/src/expression/typeinf/type_ref.cpp @@ -0,0 +1,61 @@ +/** @file type_ref.cpp **/ + +#include "typeinf/type_ref.hpp" + +namespace xo { + namespace scm { + + type_ref::type_ref(const type_var& id, TypeDescr td) + : id_{id}, td_{td} + {} + + bool type_ref::is_concrete() const { return td_ != nullptr; } + + type_ref + type_ref::dwim(prefix_type prefix, TypeDescr td) + { + if (td) { + /** type resolved, type variable not needed **/ + return type_ref(type_var(), td); + } else { + /** type not resolved, assign a unique type variable **/ + return type_ref(generate_unique(prefix), td); + } + } + + auto + type_ref::generate_unique(xo::scm::prefix_type prefix) -> xo::scm::type_var + { + static uint32_t s_counter = 0; + + s_counter = (s_counter + 1) % 100000000; + + char buf [type_var::fixed_capacity]; + int n = snprintf(buf, sizeof(buf), "%s:%u", prefix.c_str(), s_counter); + (void)n; + + assert(n < static_cast(type_var::fixed_capacity)); + + // not necessary, but remove all doubt + // max: + // 7 chars for prefix + // 8 chars for u32 % 1000000000 + // + buf [type_var::fixed_capacity - 1] = '\0'; + + return buf; + } + + void + type_ref::resolve_to(TypeDescr td) + { + assert(!td_); + + this->td_ = td; + } + + } /*namespace scm*/ +} /*namespace xo*/ + + +/** end type_ref.cpp **/ diff --git a/xo-expression/src/expression/typeinf/type_unifier.cpp b/xo-expression/src/expression/typeinf/type_unifier.cpp new file mode 100644 index 00000000..77046789 --- /dev/null +++ b/xo-expression/src/expression/typeinf/type_unifier.cpp @@ -0,0 +1,230 @@ +/** @file type_unifier.cpp + * + * author: Roland Conybeare, Jul 2025 + **/ + +#include "typeinf/type_unifier.hpp" +#include "xo/indentlog/print/tag.hpp" + +namespace xo { + namespace scm { + std::ostream & + operator<< (std::ostream & os, + const unify_result & x) + { + os << ""; + return os; + } + + unify_result + type_unifier::occurs_error(const char * src_function, + bp t1, + bp t2, + bp s1, + bp s2) + { + // unification implies some infinite type, + // e.g. unify a' with (i64 -> 'a) + // would imply type (i64 -> i64 -> i64 -> ...) + return { + .success_ = false, + .unified_ = nullptr, + .error_src_function_ = src_function, + .error_description_ = tostr("attempting unify(T1,T2) with T1 -> S1, T2 -> S2", + ": occurs check failed with S1 occuring in S2", + xrefrtag("T1", t1), + xrefrtag("T2", t2), + xrefrtag("S1", s1), + xrefrtag("S2", s2)) + }; + }; + + unify_result + type_unifier::unify(bp lhs, bp rhs) + { + /** if we already have substitutions for either of {lhs, rhs}, use them **/ + + auto lhs1 = lhs->substitute(constraint_map_); + auto rhs1 = rhs->substitute(constraint_map_); + + /** reminder: + * 1. lhs1, rhs1 need not be in constraint_map, + * 2. lhs1, rhs1 need not be distinct from lhs, rhs respectively + **/ + + if (TypeBlueprint::equals(lhs1, rhs1)) { + // blueprints are already equivalent on their face. + // this recognizes matching concrete types. + // + // return the lexicographically earlier id as canonical representative + bp canonical = (lhs1->id() < rhs1->id()) ? lhs1 : rhs1; + + return { + .success_ = true, + .unified_ = canonical.promote(), + .error_src_function_ = nullptr, + .error_description_ = "" + }; + } + + assert(lhs1->id() != rhs1->id()); + + constexpr const char * c_self_name = "type_unifier::unify"; + + bp canonical; + + /** if both lhs1 and rhs1 are type variables, + * pick the lexicographically earlier one as canonical name. + * (already know they're distinct because did not satisfy equality test above) + * + * prefer the canonical name as rhs target of all substitutions + * from known-to-be-equivalent typevars. + **/ + if (lhs1->is_variable()) + { + if (rhs1->is_variable()) + { + // haven't resolved anything yet, but we do know + // that type variables lhs,rhs,lhs1,rhs1 must refer to the same type + + if (lhs1->ref().id() < rhs1->ref().id()) { + canonical = lhs1; + constraint_map_[rhs1->id()] = lhs1.promote(); + } else { + canonical = rhs1; + constraint_map_[lhs1->id()] = rhs1.promote(); + } + } else if (rhs1->is_concrete()) { + canonical = (lhs1->id() < rhs1->id()) ? lhs1 : rhs1; + + // update lhs, lhs1 to refer to resolved rhs1. + // rhs would already have been resolved + assert(rhs->td() == rhs1->td()); + + lhs1->resolve_to(rhs1->td()); + if (lhs->id() != lhs1->id()) + lhs->resolve_to(rhs1->td()); + } else { + // 1. lhs1->is_variable() + // 2. !rhs1->is_variable() && !rhs1->is_concrete() + // + // therefore need occurs check for lhs1 appearing in rhs1 + + std::set rhs1_typevar_set; + rhs1->upsert_typevars(&rhs1_typevar_set); + + if (rhs1_typevar_set.contains(lhs1->id())) { + return type_unifier::occurs_error(c_self_name, + lhs, rhs, lhs1, rhs1); + } + + // TODO: some sort of recursive unification here + assert(false); + } + } else if (rhs1->is_variable()) + { + assert(!rhs1->is_concrete()); + + if (lhs1->is_concrete()) + { + canonical = (lhs1->id() < rhs1->id()) ? lhs1 : rhs1; + + // update rhs, rhs1 to refer to resolved lhs1. + // lhs would already have been resolved + assert(lhs->td() == lhs1->td()); + + rhs1->resolve_to(lhs1->td()); + if (rhs->td() != rhs1->td()) + rhs->resolve_to(lhs1->td()); + } else + { + // 1. !lhs1->is_variable() && !lhs1->is_concrete() + // 2. rhs1->is_variable() + // + // Need occurs check for rhs1 appearing in lhs1 + + std::set lhs1_typevar_set; + lhs1->upsert_typevars(&lhs1_typevar_set); + + if (lhs1_typevar_set.contains(rhs1->id())) { + return type_unifier::occurs_error(c_self_name, + rhs, lhs, rhs1, lhs1); + } + + // TODO: some sort of recursive unification here + assert(false); + } + } else if (lhs1->is_concrete() && rhs1->is_concrete()) + { + /* we already know lhs1 != rhs1 -> unification failure */ + return { + .success_ = false, + .unified_ = nullptr, + .error_src_function_ = c_self_name, + .error_description_ = tostr("attempting unify(T1,T2) with T1 -> S1, T2 -> S2", + ": incompatible concrete types S1,S2", + xrefrtag("T1", lhs), + xrefrtag("T2", rhs), + xrefrtag("S1", lhs1), + xrefrtag("S2", rhs1)) + }; + } + + // TODO: recursive unification for structural types, function types etc. + + if (canonical) + { + constraint_map_[lhs1->id()] = canonical.promote(); + constraint_map_[rhs1->id()] = canonical.promote(); + + if (!constraint_map_.contains(lhs1->id())) + constraint_map_[lhs1->id()] = canonical.promote(); + if (!constraint_map_.contains(rhs1->id())) + constraint_map_[rhs1->id()] = canonical.promote(); + + return { + .success_ = true, + .unified_ = canonical.promote(), + .error_src_function_ = nullptr, + .error_description_ = "" + }; + } + + assert(false); + + return { + .success_ = false, + .unified_ = nullptr, + .error_src_function_ = c_self_name, + .error_description_ = tostr("attempting unify(T1,T2) with T1 -> S1, T2 -> S2", + "supposedly-unreachable case for S1,S2", + xrefrtag("T1", lhs), + xrefrtag("T2", rhs), + xrefrtag("S1", lhs1), + xrefrtag("S2", rhs1)) + }; + } + + rp + type_unifier::lookup(const type_var & name) const + { + auto ix = constraint_map_.find(name); + + if (ix != constraint_map_.end()) { + return ix->second; + } else { + return nullptr; + } + } + + } /*namespace scm*/ +} /*namespace xo*/ + +/** end type_unifier.cpp **/ diff --git a/xo-expression/utest/CMakeLists.txt b/xo-expression/utest/CMakeLists.txt new file mode 100644 index 00000000..19dbb944 --- /dev/null +++ b/xo-expression/utest/CMakeLists.txt @@ -0,0 +1,10 @@ +# build unittest expression/utest + +set(SELF_EXE utest.expression) +set(SELF_SRCS + expression_utest_main.cpp + type_unifier.test.cpp) + +xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS}) +xo_self_dependency(${SELF_EXE} xo_expression) +xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2) diff --git a/xo-expression/utest/expression_utest_main.cpp b/xo-expression/utest/expression_utest_main.cpp new file mode 100644 index 00000000..7ca3b66a --- /dev/null +++ b/xo-expression/utest/expression_utest_main.cpp @@ -0,0 +1,6 @@ +/* file expression_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end expression_utest_main.cpp */ diff --git a/xo-expression/utest/type_unifier.test.cpp b/xo-expression/utest/type_unifier.test.cpp new file mode 100644 index 00000000..054f4173 --- /dev/null +++ b/xo-expression/utest/type_unifier.test.cpp @@ -0,0 +1,247 @@ +/* @file expression.text.cpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#include "xo/expression/typeinf/type_unifier.hpp" +#include "xo/reflect/Reflect.hpp" +#include + +namespace xo { + namespace ut { + // rehearser copied from xo_tokenizer/utest/tokenizer.test.cpp + + /** Two-pass test harness. + * + * First pass - verify test assertions. + * Second pass only if first pass failed. + * On second pass, enable verbose logging + **/ + struct rehearser { + rehearser(std::uint32_t att = 0) : attention_{att} {} + + /* expect at most one iterator to exist per TestRehearser instance **/ + struct iterator { + explicit iterator(rehearser* parent) : parent_{parent} {} + + iterator& operator++(); + std::uint32_t operator*() { return parent_->attention_; } + + bool operator==(const iterator& ix2) const { + return (parent_ == ix2.parent_); + } + + rehearser* parent_ = nullptr; + std::uint32_t attention_ = 0; + + }; + + bool is_first_pass() const { return attention_ == 0; } + bool is_second_pass() const { return attention_ == 1; } + bool enable_debug() const { return is_second_pass(); } + + iterator begin() { return iterator(this); } + iterator end() { return iterator(nullptr); } + + public: + /** pass number: 0 or 1 **/ + std::uint32_t attention_ = 0; + /** @brief set to true when test starts; false if first pass fails **/ + bool ok_flag_ = true; + }; + + auto rehearser::iterator::operator++() -> iterator& + { + if (parent_) + ++(parent_->attention_); + + if (parent_->ok_flag_ && (parent_->attention_ == 1)) { + /* skip 2nd pass */ + ++(parent_->attention_); + } + + if (parent_->attention_ == 2) + parent_ = nullptr; + + return *this; + } + + /* use this instead of REQUIRE(expr) in context of a test_rehearser + * REQUIRE(true) in first pass so we count assertions + */ +# define REHEARSE(rehearser, expr) \ + if (rehearser.is_first_pass()) { \ + REQUIRE(true); \ + bool _f = (expr); \ + rehearser.ok_flag_ = rehearser.ok_flag_ && _f; \ + } else { \ + REQUIRE(expr); \ + } + + /* note: trivial REQUIRE() call in else branch bc we still want + * catch2 to count assertions when verification succeeds + */ +# define REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr) \ + if (catch_flag) { \ + REQUIRE((expr)); \ + } else { \ + REQUIRE(true); \ + ok_flag &= (expr); \ + } + +# define REQUIRE_ORFAIL(ok_flag, catch_flag, expr) \ + REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr); \ + if (!ok_flag) \ + return ok_flag + + + using xo::scm::type_unifier; + using xo::scm::TypeBlueprint; + using xo::scm::unify_result; + using xo::scm::type_ref; + using xo::scm::type_var; + using xo::reflect::Reflect; + using xo::reflect::TypeDescr; + + namespace { + struct testcase_ufy { + /* using lambda's here to ensure don't share state between test loop iterations below */ + std::function()> lhs_; + std::function()> rhs_; + + bool expect_unify_ok_ = false; + type_var expect_unify_id_; + bool expect_unify_concrete_ = false; + std::string expect_concrete_typename_; + bool expect_unify_variable_ = false; + }; + + std::vector + s_testcase_v = { + /* unify two variables */ + { + .lhs_ = [](){ return TypeBlueprint::typevar(type_var::from_chars("a")); }, + .rhs_ = [](){ return TypeBlueprint::typevar(type_var::from_chars("b")); }, + .expect_unify_ok_ = true, + .expect_unify_id_{type_var::from_chars("a")}, + .expect_unify_concrete_ = false, + .expect_concrete_typename_ = "", + .expect_unify_variable_ = true, + }, + /* unify two variables (different order) */ + { + .lhs_ = [](){ return TypeBlueprint::typevar(type_var::from_chars("b")); }, + .rhs_ = [](){ return TypeBlueprint::typevar(type_var::from_chars("a")); }, + .expect_unify_ok_ = true, + .expect_unify_id_{type_var::from_chars("a")}, + .expect_unify_concrete_ = false, + .expect_concrete_typename_ = "", + .expect_unify_variable_ = true, + }, + /* unify a variable with a concrete type */ + { + .lhs_ = [](){ return TypeBlueprint::typevar(type_var::from_chars("a")); }, + .rhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("b"), + Reflect::require())); }, + .expect_unify_ok_ = true, + .expect_unify_id_{type_var::from_chars("a")}, + .expect_unify_concrete_ = true, +#ifdef __APPLE__ + .expect_concrete_typename_ = "long long", +#else + .expect_concrete_typename_ = "long int", +#endif + .expect_unify_variable_ = false, + }, + /* same, but reverse order */ + { + .lhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("a"), + Reflect::require())); }, + .rhs_ = [](){ return TypeBlueprint::typevar(type_var::from_chars("b")); }, + .expect_unify_ok_ = true, + .expect_unify_id_{type_var::from_chars("a")}, + .expect_unify_concrete_ = true, +#ifdef __APPLE__ + .expect_concrete_typename_ = "long long", +#else + .expect_concrete_typename_ = "long int", +#endif + .expect_unify_variable_ = false, + }, + /* matching concrete types */ + { + .lhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("a"), + Reflect::require())); }, + .rhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("b"), + Reflect::require())); }, + .expect_unify_ok_ = true, + .expect_unify_id_{type_var::from_chars("a")}, + .expect_unify_concrete_ = true, + .expect_concrete_typename_ = "bool", + .expect_unify_variable_ = false, + }, + + /* conflicting concrete types */ + { + .lhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("a"), + Reflect::require())); }, + .rhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("b"), + Reflect::require())); }, + .expect_unify_ok_ = false, + // remainder ignored + .expect_unify_id_{}, + .expect_unify_concrete_{}, + .expect_concrete_typename_{}, + .expect_unify_variable_{}, + } + }; + } + + TEST_CASE("unifier", "[type-unification]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const testcase_ufy & testcase = s_testcase_v[i_tc]; + + rehearser rh; + CHECK(rh.ok_flag_ == true); + + for (auto _ : rh) { + /* this loop problematic because iterations aren't independent: + * TypeBlueprint instances modified in place by unification + */ + + scope log(XO_DEBUG2(rh.enable_debug(), "unifier")); + + auto lhs = testcase.lhs_(); + auto rhs = testcase.rhs_(); + + log && log(xtag("i_tc", i_tc), + xtag("lhs", lhs), + xtag("rhs", rhs)); + + type_unifier unifier; + + auto ur = unifier.unify(lhs, rhs); + + log && log(xtag("ur", ur)); + + REHEARSE(rh, ur.success_ == testcase.expect_unify_ok_); + if (testcase.expect_unify_ok_) { + REHEARSE(rh, ur.unified_.get() != nullptr); + if (ur.unified_) { + REHEARSE(rh, ur.unified_->id() == testcase.expect_unify_id_); + REHEARSE(rh, ur.unified_->is_concrete() == testcase.expect_unify_concrete_); + if (ur.unified_->is_concrete()) { + REHEARSE(rh, ur.unified_->td()->canonical_name() == testcase.expect_concrete_typename_); + } + REHEARSE(rh, ur.unified_->is_variable() == testcase.expect_unify_variable_); + } + } + //REHEARSE(rh, ur.error_description_ == testcase.expect_unify_error_); + } + } + } + + + } /*namespace ut*/ +} /*namespace xo*/