user2341104
user2341104

Reputation:

Solving the circular dependency conundrum "elegantly"

So I am developing a programming language which compiles to bytecode for VM execution and also to C as an intermediate language for compiling to native binary. I chose C because it is low level enough and portable, saving a tremendous amount of effort by reusing existing compilers and not having to write compilers to assembly for each and every different platform and its oddities.

But existing compilers come with their drawbacks, one of which is the circular dependency issue. I want to solve circular dependencies in an elegant way (unlike C/C++) without awkward forward declarations, without having to use pointers and extra indirection and wasted memory, without having to separate declarations from definitions and so on... In other words, take this issue away from the developer like some programming languages do.

The way I see it, current C/C++ compilers' main problem with this is they cannot "look into the future" even though it is not really future, since the programmer intent is already expressed in code, my compiler does not have that issue, it is not unaware of anything beyond some certain point of parsing progress, it knows the sizes of objects with circular dependencies and can calculate the appropriate offsets and such.

I've already implemented "faked" inheritance which simply does "inline expansion" of inherited structures' members, so I am thinking I can also use the same approach to actually fake aggregation as well. In the most basic and simple example:

typedef struct {
    int a;
} A;

typedef struct {
    A a;
    int b;
} B;

becomes:

typedef struct {
    int A_a;
    int b;
} B;

and the compiler does a bit of "translation":

B b;
b.a.a = 7;

becomes:

b.A_a = 7;

And in the same fashion all structures are collapsed down to a single root structure which only contains primitive types. This way there are absolutely no types used in structures whose sizes are not known in advance so the order of definition becomes irrelevant. Naturally, this mess is hidden away from the user and is only for the compiler's "eyes to see" while the user side is being kept structured and readable. And it goes without saying, but the binary footprint is preserved for compatibility with regular C/C++ code, the collapsed structure is binary identical to a regular structure that would use aggregation or inheritance.

So my question is: Does this sound like a sound idea? Anything that could go wrong I am missing?

EDIT: I only aim to solve the C/C++ related difficulties with circular dependencies, not the "chicken or egg" logical paradox. Obviosly it is impossible for two objects to contain each-other without leading to some form of infinite loop.

Upvotes: 4

Views: 337

Answers (1)

Fred Foo
Fred Foo

Reputation: 363757

You cannot safely use pointers to the substructures because you cannot get pointers to "compatible types" by pointing to the primitive members. E.g. after

struct Foo {
    short a;
    int b;
};

struct Bar {
    struct Foo foo;
};

struct Bar bar;

the pointers &bar.foo and &bar.foo.a have different types and cannot be used interchangeably. They also cannot be cast to each other's types without violating the strict aliasing rule, triggering undefined behavior.

The problem can be avoided by inlining the entire struct definition each time:

struct Bar {
    struct { short a; int b; } foo;
};

Now &bar.a is a pointer to struct {short; int;} which is a compatible type for struct Foo.

(There may also be padding/alignment differences between struct-typed members and primitive members, but I couldn't find an example of these.

Upvotes: 1

Related Questions