Inserting to end of a CONS list
Posted: Sat May 27, 2023 10:34 pm
I am presently trying to go through the Make A Lisp process as a confidence-building exercise, and after a bit of a wrong turn (involving an ugly abuse of C++ std::vector), I have taken the classic Lisp approach of representing the code as a CONS list rather than trying to use just strings as the 'official' process seems to be recommending.
For those unfamiliar, a CONS cell or CONS pair is a structure consisting of a pair of typeless pointers - typeless in the sense that they can point to any Lisp object, including other CONS pairs. While it can be used just to hold a pair of objects, the more common use is to form a singly-linked list; the first pointer (called CAR for historical reasons, or head) points to the first element of the list, while the second (historically called CDR, or tail), points to another CONS pair. These pairs are then chained together until the end of the list, which is indicated by a NULL in the CDR element. For example, the list (42 69 613) would be represented as:
This is a surprisingly versatile structure, as you could use it to construct trees and other complex data structures implicitly, but that's not really relevant right now. My problem is with my implementation, and specifically, with how I am implementing the add (insert to the end of the list - I don't want to call it 'append' as that has a particular meaning in Lisp) operation.
In short, I don't seem to be overwriting the pointer I am trying to insert to.
The relevant parts of the class and type declarations (with irrelevant part elided with the '[...]') are as follows:
The add() method is as follows:
Now, this works correctly for adding the first and second elements to a list, but fails to insert elements past the second. My test code (using the doctest library header) is:
I also tried using a while() loop rather than recursion, and got more or less the same result. My best guess is that I am somehow writing to a local copy of the CDR rather than the one I am trying to modify, but given that I am operating on pointers, this should not be the case I would think. I am at a loss as to why this isn't working.
Any suggestions as to what I am overlooking?
For those unfamiliar, a CONS cell or CONS pair is a structure consisting of a pair of typeless pointers - typeless in the sense that they can point to any Lisp object, including other CONS pairs. While it can be used just to hold a pair of objects, the more common use is to form a singly-linked list; the first pointer (called CAR for historical reasons, or head) points to the first element of the list, while the second (historically called CDR, or tail), points to another CONS pair. These pairs are then chained together until the end of the list, which is indicated by a NULL in the CDR element. For example, the list (42 69 613) would be represented as:
This is a surprisingly versatile structure, as you could use it to construct trees and other complex data structures implicitly, but that's not really relevant right now. My problem is with my implementation, and specifically, with how I am implementing the add (insert to the end of the list - I don't want to call it 'append' as that has a particular meaning in Lisp) operation.
In short, I don't seem to be overwriting the pointer I am trying to insert to.
The relevant parts of the class and type declarations (with irrelevant part elided with the '[...]') are as follows:
Code: Select all
typedef std::shared_ptr<Env_Symbol> EnvSymbolPtr;
typedef std::shared_ptr<Environment> EnvPtr;
// provide a type list for the different subclasses of MalObject.
// I felt that this was more useful for my purposes than trying
// to use typeid().
enum MalType
{
MAL_OBJECT,
[...]
MAL_COLLECTION, MAL_PAIR,
[...]
};
// an array of strings representing the type names.
extern std::string mal_type_name[];
// shortcuts for the two most frequently used pointer types.
typedef std::shared_ptr<MalObject> MalPtr;
typedef std::shared_ptr<MalPair> PairPtr;
class MalObject
{
public:
MalObject(MalType type): m_type(type) {};
virtual std::string to_str() {return "";};
MalType type() {return m_type;};
std::string type_name() {return mal_type_name[m_type];};
virtual size_t size() {return 1;};
// type predicates
[...]
virtual bool is_null() {return false;};
virtual bool is_pair() {return false;};
virtual bool is_list() {return false;};
[,,,]
// return values of the subclasses
[...]
virtual PairPtr as_pair() {throw InvalidConversionException(mal_type_name[m_type], mal_type_name[MAL_PAIR]); return nullptr;};
protected:
MalType m_type;
};
[...]
class MalCollection: public MalObject
{
public:
MalCollection(MalType type): MalObject(type) {};
virtual bool is_collection() {return true;};
};
class MalPair: public MalCollection
{
public:
MalPair(MalPtr car=nullptr, MalPtr cdr=nullptr): MalCollection(MAL_PAIR), m_car(car), m_cdr(cdr) {};
virtual std::string to_str();
virtual std::string to_str_continued();
virtual bool is_null() {return (m_car == nullptr && m_cdr == nullptr);};
virtual bool is_pair() {return true;};
virtual bool is_list() {return (m_cdr == nullptr || m_cdr->is_pair());};
virtual PairPtr as_pair() {return std::make_shared<MalPair>(m_car, m_cdr);};
virtual size_t size();
virtual MalPtr operator[](size_t index);
virtual MalPtr car() {return m_car;};
virtual MalPtr cdr() {return m_cdr;};
virtual void add(MalPtr addition); // inserts an element at the end of the list
protected:
MalPtr m_car, m_cdr;
};
Code: Select all
void MalPair::add(MalPtr addition)
{
if (m_car == nullptr)
{
if (m_cdr == nullptr)
{
// insert to the first element of the list
m_car = addition;
}
else if (m_cdr->type() == MAL_PAIR)
{
// recurse over the list
m_cdr->as_pair()->add(addition);
}
else
{
throw ImproperListException(this->to_str());
}
}
else if (m_cdr == nullptr)
{
// insert to the end of the list
m_cdr = std::make_shared<MalPair>(addition);
}
else
{
if (m_cdr->type() == MAL_PAIR)
{
// recurse over the list
m_cdr->as_pair()->add(addition);
}
else
{
throw ImproperListException(this->to_str());
}
}
}
Now, this works correctly for adding the first and second elements to a list, but fails to insert elements past the second. My test code (using the doctest library header) is:
Code: Select all
TEST_CASE("appending single element to list")
{
PairPtr test = std::make_shared<MalPair>();
CHECK(test->to_str() == "()");
test->add(std::make_shared<MalSymbol>("foo"));
CHECK(test->to_str() == "(foo)");
test->add(std::make_shared<MalKeyword>("bar"));
CHECK(test->to_str() == "(foo :bar)");
// fails with both of the following tests
test->add(std::make_shared<MalInteger>(69));
CHECK(test->to_str() == "(foo :bar 69)");
test->add(std::make_shared<MalRational>(mpq_class("17/23")));
CHECK(test->to_str() == "(foo :bar 69 17/23)");
}
Any suggestions as to what I am overlooking?