xo-expression xo-reader: type unifier + misc improvements

This commit is contained in:
Roland Conybeare 2025-07-26 17:28:41 -04:00
commit 75b74918b7
31 changed files with 1005 additions and 76 deletions

View file

@ -26,7 +26,7 @@ xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets
# ----------------------------------------------------------------
add_subdirectory(example)
#add_subdirectory(utest)
add_subdirectory(utest)
# ----------------------------------------------------------------
# docs targets depend on all the other library/utest targets

View file

@ -2,5 +2,6 @@
include(CMakeFindDependencyMacro)
find_dependency(reflect)
find_dependency(xo_flatstring)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
check_required_components("@PROJECT_NAME@")

View file

@ -31,7 +31,7 @@ namespace xo {
exprtype extype() const { return extype_; }
TypeDescr valuetype() const { return valuetype_; }
/** write human-readable representation to stream **/
/** 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;

View file

@ -40,6 +40,7 @@ namespace xo {
**/
static rp<Lambda> make_from_env(const std::string & name,
const rp<LocalEnv> & env,
TypeDescr explicit_return_td,
const rp<Expression> & body);
/** downcast from Expression **/
@ -105,6 +106,7 @@ namespace xo {
* and body expression @p body
**/
static TypeDescr assemble_lambda_td(const std::vector<rp<Variable>> & argv,
TypeDescr explicit_return_td,
const rp<Expression> & body);
/** create string description for function signature,

View file

@ -0,0 +1,89 @@
/** @file TypeBlueprint.hpp **/
#include "xo/refcnt/Refcounted.hpp"
#include "type_ref.hpp"
#include <map>
#include <set>
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<type_var, rp<TypeBlueprint>>;
/** @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<TypeBlueprint> make(const type_ref& ref);
/** contruct blueprint for type variable @p name.
* equivalent to @c make(type_ref(name, nullptr))
**/
static rp<TypeBlueprint> 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<TypeBlueprint> lhs, bp<TypeBlueprint> 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<type_var> * p_typevar_set) const;
/** apply substitutions in @p sub_map to this type **/
bp<TypeBlueprint> 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 **/

View file

@ -2,18 +2,48 @@
#pragma once
#include "xo/flatstring/flatstring.hpp"
#include "xo/reflect/TypeDescr.hpp"
namespace xo {
namespace scm {
using type_var = xo::flatstring<11>;
using prefix_type = xo::flatstring<8>;
using type_var = xo::flatstring<16>;
/** @class type_ref
* @brief name and eventual resolution for type associated with an expression.
*
*
* Type inference / unification operates on @ref xo::scm::TypeTemplate instances, see also.
**/
struct type_ref {
class type_ref {
public:
using TypeDescr = xo::reflect::TypeDescr;
public:
type_ref() = default;
type_ref(const type_var& id, 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*/

View file

@ -0,0 +1,60 @@
/** @file type_unifier.hpp **/
#include "type_ref.hpp"
#include "TypeBlueprint.hpp"
#include <map>
namespace xo {
namespace scm {
struct unify_result {
/** true iff unification success **/
bool success_;
/** blueprint (possibly concrete) for unified type **/
rp<TypeBlueprint> 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<TypeBlueprint> t1,
bp<TypeBlueprint> t2,
bp<TypeBlueprint> s1,
bp<TypeBlueprint> 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<TypeBlueprint> lhs, bp<TypeBlueprint> rhs);
private:
type_substitution_map constraint_map_;
};
} /*namespace scm*/
} /*namespace xo*/
/** end type_unifier.hpp **/

View file

@ -15,10 +15,15 @@ set(SELF_SRCS
LocalEnv.cpp
ConvertExpr.cpp
Primitive.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)

View file

@ -20,6 +20,7 @@ namespace xo {
namespace ast {
TypeDescr
Lambda::assemble_lambda_td(const std::vector<rp<Variable>> & argv,
TypeDescr explicit_return_td,
const rp<Expression> & body)
{
if (!body)
@ -39,8 +40,16 @@ namespace xo {
}
}
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())
auto function_info
= FunctionTdxInfo(body->valuetype(),
= FunctionTdxInfo(explicit_return_td ? explicit_return_td : body->valuetype(),
arg_td_v,
false /*!is_noexcept*/);
@ -72,9 +81,10 @@ namespace xo {
rp<Lambda>
Lambda::make_from_env(const std::string & name,
const rp<LocalEnv> & env,
TypeDescr explicit_return_td,
const rp<Expression> & body)
{
TypeDescr lambda_td = assemble_lambda_td(env->argv(), body);
TypeDescr lambda_td = assemble_lambda_td(env->argv(), explicit_return_td, body);
rp<Lambda> retval
= new Lambda(name,
@ -96,7 +106,9 @@ namespace xo {
{
rp<LocalEnv> env = LocalEnv::make(argv, parent_env);
return make_from_env(name, env, body);
TypeDescr explicit_return_td = nullptr;
return make_from_env(name, env, explicit_return_td, body);
} /*make*/
std::set<std::string>
@ -168,8 +180,9 @@ namespace xo {
void
Lambda::complete_assembly_from_body() {
if (body_) {
TypeDescr explicit_return_td = nullptr;
TypeDescr lambda_td
= assemble_lambda_td(this->local_env_->argv(), body_);
= assemble_lambda_td(this->local_env_->argv(), explicit_return_td, body_);
if (lambda_td)
this->type_str_ = assemble_type_str(lambda_td);
@ -328,32 +341,6 @@ namespace xo {
refrtag("name", name_),
refrtag("argv", local_env_->argv()),
refrtag("body", body_));
#ifdef OBSOLETE
ppstate * pps = ppii.pps();
if (ppii.upto()) {
if (!pps->print_upto("<Lambda"))
return false;
if (!pps->print_upto_tag("name", name_))
return false;
if (!pps->print_upto_tag("argv", local_env_->argv()))
return false;
if (!pps->print_upto_tag("body", body_))
return false;
pps->write(">");
return true;
} else {
pps->write("<Lambda");
pps->newline_pretty_tag(ppii.ci1(), "name", name_);
pps->newline_pretty_tag(ppii.ci1(), "argv", local_env_->argv());
pps->newline_pretty_tag(ppii.ci1(), "body", body_);
pps->write(">");
return false;
}
#endif
}
// ----- Lambda Access -----
@ -364,7 +351,8 @@ namespace xo {
const rp<Expression> & body,
const rp<Environment> & parent_env)
{
TypeDescr lambda_td = assemble_lambda_td(argv, body);
TypeDescr explicit_return_td = nullptr;
TypeDescr lambda_td = assemble_lambda_td(argv, explicit_return_td, body);
rp<LocalEnv> env = LocalEnv::make(argv, parent_env);
rp<LambdaAccess> retval

View file

@ -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>
TypeBlueprint::make(const type_ref & ref)
{
return new TypeBlueprint(ref);
}
rp<TypeBlueprint>
TypeBlueprint::typevar(const type_var & name)
{
return new TypeBlueprint(type_ref(name, nullptr));
}
bool
TypeBlueprint::equals(bp<TypeBlueprint> lhs, bp<TypeBlueprint> 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<type_var> * p_typevar_set) const
{
if (this->is_concrete()) {
return;
}
// TODO: handle structural types
p_typevar_set->insert(ref_.id());
}
bp<TypeBlueprint>
TypeBlueprint::substitute(const type_substitution_map& sub_map)
{
bp<TypeBlueprint> 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 << "<TypeBlueprint";
os << xtag("id", id());
if (td())
os << xtag("td", td()->canonical_name());
os << ">";
}
} /*namespace scm*/
} /*namespace xo*/
/** end TypeBlueprint.cpp **/

View file

@ -0,0 +1,48 @@
/** @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; }
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);
assert(n < static_cast<int>(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 **/

View file

@ -0,0 +1,218 @@
/** @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 << "<unify_result"
<< xtag("success", x.success_)
<< xtag("unified", x.unified_);
if (x.error_src_function_)
os << xtag("error_src_function", x.error_src_function_);
if (!x.error_description_.empty())
os << xtag("error_description", x.error_description_);
os << ">";
return os;
}
unify_result
type_unifier::occurs_error(const char * src_function,
bp<TypeBlueprint> t1,
bp<TypeBlueprint> t2,
bp<TypeBlueprint> s1,
bp<TypeBlueprint> 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<TypeBlueprint> lhs, bp<TypeBlueprint> 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<TypeBlueprint> 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<TypeBlueprint> 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<type_var> 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<type_var> 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))
};
}
} /*namespace scm*/
} /*namespace xo*/
/** end type_unifier.cpp **/

View file

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

View file

@ -0,0 +1,6 @@
/* file expression_utest_main.cpp */
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
/* end expression_utest_main.cpp */

View file

@ -0,0 +1,239 @@
/* @file expression.text.cppreference
*
* author: Roland Conybeare, Jul 2025
*/
#include "xo/expression/typeinf/type_unifier.hpp"
#include "xo/reflect/Reflect.hpp"
#include <catch2/catch.hpp>
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<rp<TypeBlueprint>()> lhs_;
std::function<rp<TypeBlueprint>()> 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<testcase_ufy>
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<std::int64_t>())); },
.expect_unify_ok_ = true,
.expect_unify_id_{type_var::from_chars("a")},
.expect_unify_concrete_ = true,
.expect_concrete_typename_ = "long int",
.expect_unify_variable_ = false,
},
/* same, but reverse order */
{
.lhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("a"),
Reflect::require<std::int64_t>())); },
.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,
.expect_concrete_typename_ = "long int",
.expect_unify_variable_ = false,
},
/* matching concrete types */
{
.lhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("a"),
Reflect::require<bool>())); },
.rhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("b"),
Reflect::require<bool>())); },
.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<bool>())); },
.rhs_ = [](){ return TypeBlueprint::make(type_ref(type_var::from_chars("b"),
Reflect::require<char>())); },
.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*/

View file

@ -13,10 +13,10 @@ namespace xo {
struct ppstate; // see pretty.hpp
struct ppindentinfo;
// Defining this means ppdetail_atomic is not used.
// In that case where not explicitly specialized ppdetail will revert to ordinary printing for a type,
// instead of giving compile-time error about missing template specialization of ppdetail.
#define ppdetail_atomic ppdetail
// Defining this means ppdetail_atomic is not used.
// In that case where not explicitly specialized ppdetail will revert to ordinary printing for a type,
// instead of giving compile-time error about missing template specialization of ppdetail.
//#define ppdetail_atomic ppdetail
struct ppindentinfo {
ppindentinfo(ppstate * pps, std::uint32_t ci0, std::uint32_t indent_width, bool upto)

View file

@ -440,7 +440,8 @@ namespace xo {
ppstate &
ppstate::newline_pretty_tag(std::uint32_t ci, Name && name, Value && value)
{
newline_indent(ci);
this->newline_indent(ci);
this->current_indent_ = ci;
this->pretty(rtag(name, value));
return *this;

View file

@ -17,7 +17,7 @@ a. create DefineExpr with TypeVariable.
// compose these into Expressions.
//
struct TypeTemplateRef {
struct type_ref {
bool is_concrete() const { return td_ && td_->is_concrete(); }
// generated name, so we can map between types. Don't want to create TypeDescr
@ -29,10 +29,10 @@ struct TypeTemplateRef {
// what we know about a type
//
struct TypeTemplate : public Refcounted {
struct TypeBlueprint : public Refcounted {
static bool equal(bp<TypeTemplate> lhs, bp<TypeTemplate> rhs);
TypeTemplateRef ref_;
type_ref ref_;
// additional descriptive info...
};

View file

@ -42,7 +42,7 @@ namespace xo {
/** formal parameter name **/
std::string name_;
/** type description for variable @p name **/
TypeDescr td_;
TypeDescr td_ = nullptr;
};
inline std::ostream &

View file

@ -12,15 +12,20 @@
namespace xo {
namespace scm {
/**
* lambda ( name(1) : type(1), ..., ) body-expr ;
* ^ ^ ^ ^
* | | | |
* lm_0 lm_1 lm_2 lm_3
* @text
*
* lambda ( name(1) : type(1), ..., ) : type body-expr ;
* ^ ^ ^ ^ ^ ^
* | | | | lm_4 lm_5
* | | | lm_3
* lm_0 lm_1 lm_2
*
* lm_0 --on_lambda_token()--> lm_1
* lm_1 --on_formal_arglist()--> lm_2
* lm_2 --on_expr()--> lm_3
* lm_3 --on_semicolon_token()--> (done)
* lm_5 --on_semicolon_token()--> (done)
*
* @endtext
**/
enum class lambdastatetype {
invalid = -1,
@ -29,6 +34,8 @@ namespace xo {
lm_1,
lm_2,
lm_3,
lm_4,
lm_5,
n_lambdastatetype
};
@ -60,12 +67,18 @@ namespace xo {
virtual void on_lambda_token(const token_type & tk,
parserstatemachine * p_psm) override;
virtual void on_typedescr(TypeDescr td,
parserstatemachine * p_psm) override;
virtual void on_formal_arglist(const std::vector<rp<Variable>> & argl,
parserstatemachine * p_psm) override;
virtual void on_expr(bp<Expression> expr,
parserstatemachine * p_psm) override;
virtual void on_expr_with_semicolon(bp<Expression> expr,
parserstatemachine * p_psm) override;
virtual void on_leftbrace_token(const token_type & tk,
parserstatemachine * p_psm) override;
virtual void on_colon_token(const token_type & tk,
parserstatemachine * p_psm) override;
virtual void on_semicolon_token(const token_type & tk,
parserstatemachine * p_psm) override;
@ -81,6 +94,9 @@ namespace xo {
/** lambda environment (for formal parameters) **/
rp<LocalEnv> local_env_;
/** explicit return type (if supplied) **/
TypeDescr explicit_return_td_ = nullptr;
/** body expression **/
rp<Expression> body_;

View file

@ -16,5 +16,6 @@ namespace xo {
return p->pretty_print(ppii);
}
};
} /*namespace print*/
} /*namespace xo*/

View file

@ -14,5 +14,10 @@ namespace xo {
struct ppdetail<xo::scm::parserstatemachine> {
static bool print_pretty(const ppindentinfo & ppii, const xo::scm::parserstatemachine & x);
};
template<>
struct ppdetail<xo::scm::parserstatemachine*> {
static bool print_pretty(const ppindentinfo & ppii, const xo::scm::parserstatemachine * x);
};
}
} /*namespace xo*/

View file

@ -26,10 +26,14 @@ namespace xo {
virtual void on_expr(bp<Expression> expr,
parserstatemachine * p_psm) override;
virtual void on_expr_with_semicolon(bp<Expression> expr,
parserstatemachine * p_psm) override;
virtual void on_rightbrace_token(const token_type & tk,
parserstatemachine * p_psm) override;
virtual void print(std::ostream & os) const override;
private:
sequence_xs();

View file

@ -229,8 +229,7 @@ namespace xo {
expect_expr_xs::on_expr(bp<Expression> expr,
parserstatemachine * p_psm)
{
constexpr bool c_debug_flag = true;
scope log(XO_DEBUG(c_debug_flag));
scope log(XO_DEBUG(p_psm->debug_flag()));
log && log(xtag("exstype", this->exs_type_),
xtag("expr", expr.promote()));
@ -244,8 +243,7 @@ namespace xo {
expect_expr_xs::on_expr_with_semicolon(bp<Expression> expr,
parserstatemachine * p_psm)
{
constexpr bool c_debug_flag = true;
scope log(XO_DEBUG(c_debug_flag));
scope log(XO_DEBUG(p_psm->debug_flag()));
log && log(xtag("exstype", this->exs_type_),
xtag("expr", expr.promote()));

View file

@ -109,8 +109,7 @@ namespace xo {
{
/* returning type description to something that wants it */
constexpr bool c_debug_flag = true;
scope log(XO_DEBUG(c_debug_flag));
scope log(XO_DEBUG(p_psm->debug_flag()));
log && log(xtag("exstype",
p_psm->top_exprstate().exs_type()));
@ -171,8 +170,7 @@ namespace xo {
exprstate::on_colon_token(const token_type & tk,
parserstatemachine * p_psm)
{
constexpr bool c_debug_flag = true;
scope log(XO_DEBUG(c_debug_flag));
scope log(XO_DEBUG(p_psm->debug_flag()));
constexpr const char * c_self_name = "exprstate::on_colon";
const char * exp = get_expect_str();

View file

@ -4,6 +4,7 @@
*/
#include "exprstatestack.hpp"
#include "pretty_exprstatestack.hpp"
#include <cstdint>
namespace xo {

View file

@ -5,6 +5,7 @@
#include "exprstatestack.hpp"
#include "expect_formal_arglist_xs.hpp"
#include "expect_expr_xs.hpp"
#include "expect_type_xs.hpp"
#include "xo/expression/Lambda.hpp"
namespace xo {
@ -20,6 +21,8 @@ namespace xo {
case lambdastatetype::lm_1: return "lm_1";
case lambdastatetype::lm_2: return "lm_2";
case lambdastatetype::lm_3: return "lm_3";
case lambdastatetype::lm_4: return "lm_4";
case lambdastatetype::lm_5: return "lm_5";
default: break;
}
@ -47,9 +50,11 @@ namespace xo {
lambda_xs::get_expect_str() const
{
/*
* lambda (x : f64) { ... } ;
* ^ ^ ^ ^
* | | | lm_3
* lambda (x : f64) : f64 { ... } ;
* ^ ^ ^ ^ ^ ^
* | | | | | lm_5
* | | | | lm_4:expect_expression
* | | | lm_3
* | | lm_2
* | lm_1:
* expect_expression
@ -63,8 +68,12 @@ namespace xo {
case lambdastatetype::lm_1:
return "lambda-params";
case lambdastatetype::lm_2:
return "lambda-body";
return "colon|lambda-body";
case lambdastatetype::lm_3:
return "type";
case lambdastatetype::lm_4:
return "lambda-body";
case lambdastatetype::lm_5:
return "semicolon";
}
@ -94,24 +103,12 @@ namespace xo {
p_psm->push_envframe(local_env_);
expect_expr_xs::start(p_psm);
//expect_expr_xs::start(p_psm);
} else {
exprstate::on_formal_arglist(argl, p_psm);
}
}
void
lambda_xs::on_expr(bp<Expression> expr,
parserstatemachine * p_psm)
{
if (lmxs_type_ == lambdastatetype::lm_2) {
this->lmxs_type_ = lambdastatetype::lm_3;
this->body_ = expr.promote();
} else {
exprstate::on_expr(expr, p_psm);
}
}
void
lambda_xs::on_expr_with_semicolon(bp<Expression> expr,
parserstatemachine * p_psm)
@ -120,11 +117,74 @@ namespace xo {
this->on_semicolon_token(token_type::semicolon(), p_psm);
}
void
lambda_xs::on_colon_token(const token_type & tk,
parserstatemachine * p_psm)
{
constexpr const char * c_self_name = "lambda_xs::on_colon_token";
if (lmxs_type_ == lambdastatetype::lm_2) {
this->lmxs_type_ = lambdastatetype::lm_3;
expect_type_xs::start(p_psm);
/* control reenters via .on_typedescr() */
} else {
this->illegal_input_on_token(c_self_name, tk, this->get_expect_str(), p_psm);
}
}
void
lambda_xs::on_leftbrace_token(const token_type & tk,
parserstatemachine * p_psm)
{
constexpr const char * c_self_name = "lambda_xs::on_leftbrace_token";
if (lmxs_type_ == lambdastatetype::lm_2)
this->lmxs_type_ = lambdastatetype::lm_4;
if (lmxs_type_ == lambdastatetype::lm_4) {
expect_expr_xs::start(p_psm);
/* want { to start expr sequence, that finishes on matching } */
p_psm->on_leftbrace_token(token_type::leftbrace());
} else {
this->illegal_input_on_token(c_self_name, tk, this->get_expect_str(), p_psm);
}
}
void
lambda_xs::on_typedescr(TypeDescr td,
parserstatemachine * p_psm)
{
constexpr const char * c_self_name = "lambda_xs::on_typedescr";
if (lmxs_type_ == lambdastatetype::lm_3) {
this->lmxs_type_ = lambdastatetype::lm_4;
this->explicit_return_td_ = td;
expect_expr_xs::start(p_psm);
/* control reenters via .on_expr() or .on_expr_with_semicolon() */
} else {
this->illegal_input_on_type(c_self_name, td, this->get_expect_str(), p_psm);
}
}
void
lambda_xs::on_expr(bp<Expression> expr,
parserstatemachine * p_psm)
{
constexpr const char * c_self_name = "lambda_xs::on_expr";
if (lmxs_type_ == lambdastatetype::lm_4) {
this->lmxs_type_ = lambdastatetype::lm_5;
this->body_ = expr.promote();
} else {
this->illegal_input_on_expr(c_self_name, expr, this->get_expect_str(), p_psm);
}
}
void
lambda_xs::on_semicolon_token(const token_type & tk,
parserstatemachine * p_psm)
{
if (lmxs_type_ == lambdastatetype::lm_3) {
if (lmxs_type_ == lambdastatetype::lm_5) {
/* done! */
std::unique_ptr<exprstate> self = p_psm->pop_exprstate();
@ -134,7 +194,7 @@ namespace xo {
/* top env frame recorded arguments to this lambda */
p_psm->pop_envframe();
rp<Lambda> lm = Lambda::make_from_env(name, local_env_, body_);
rp<Lambda> lm = Lambda::make_from_env(name, local_env_, explicit_return_td_, body_);
p_psm->top_exprstate().on_expr(lm, p_psm);
p_psm->top_exprstate().on_semicolon_token(tk, p_psm);

View file

@ -141,6 +141,7 @@ namespace xo {
rp<Expression> lambda
= Lambda::make_from_env(lambda_name,
local_env_,
nullptr /*explicit_return_td*/,
expr);
rp<Expression> result

View file

@ -36,5 +36,19 @@ namespace xo {
}
}
bool
ppdetail<xo::scm::parserstatemachine *>::print_pretty(const ppindentinfo & ppii, const xo::scm::parserstatemachine * x)
{
if (x) {
return ppdetail<xo::scm::parserstatemachine>::print_pretty(ppii, *x);
} else {
if (ppii.upto()) {
return ppii.pps()->print_upto("<nullptr>");
} else {
ppii.pps()->write("<nullptr>");
return false;
}
}
}
} /*namespace print*/
} /*namespace xo*/

View file

@ -36,8 +36,7 @@ namespace xo {
sequence_xs::on_expr(bp<Expression> expr,
parserstatemachine * p_psm)
{
constexpr bool c_debug_flag = true;
scope log(XO_DEBUG(c_debug_flag));
scope log(XO_DEBUG(p_psm->debug_flag()));
log && log(xtag("expr", expr.promote()));
@ -93,6 +92,14 @@ namespace xo {
}
}
void
sequence_xs::on_expr_with_semicolon(bp<Expression> expr,
parserstatemachine * p_psm)
{
/* sequence continues until right brace */
this->on_expr(expr, p_psm);
}
void
sequence_xs::on_rightbrace_token(const token_type & /*tk*/,
parserstatemachine * p_psm)
@ -107,6 +114,11 @@ namespace xo {
p_psm->top_exprstate().on_expr(expr, p_psm);
}
void
sequence_xs::print(std::ostream & os) const {
os << "<sequence_xs" << xtag("expr_v.size", expr_v_.size()) << ">";
}
} /*namespace scm*/
} /*namespace xo*/

View file

@ -518,11 +518,21 @@ namespace xo {
}; /*TypeDescrBase*/
inline std::ostream &
operator<<(std::ostream & os, TypeDescrBase const & x) {
operator<<(std::ostream & os, const TypeDescrBase & x) {
x.display(os);
return os;
} /*operator<<*/
inline std::ostream &
operator<<(std::ostream & os, const TypeDescrBase * p) {
if (p)
p->display(os);
else
os << "<nullptr>";
return os;
}
/* tag to drive overload resolution */
struct reflected_types_printer {};