When i deleted 2 lines a Dependency Loop appeared and I cannot solve it

There is a restriction, the Translation Unit cannot be a pointer in the observer params
https://github.com/EvilAlliance/Yotz/blob/a443cff3400bd6c1138efb2343d5a63688513e1e/src/TypeCheck/Expression.zig#L318

The loop appeared after this coomit https://github.com/EvilAlliance/Yotz/commit/a443cff3400bd6c1138efb2343d5a63688513e1e
Here are the 2 line I mentioned before

  • I tried to move the observer params to another file
  • TypeCheck.Scope in the Translation Unit being a pointer

I am try to avoid using a pointer to Translation Unit, because I make to many allocations.

And why did deleting this 2 lines causes a dependency loop and it did not happened before

I’ve had trouble with dependency loops and tuples too. There’s a whole class of problems like this.

If you add a declaration to Globals.zig, like this:

const spurious_observer = TypeCheck.Observer = .{};

comptime {
     _  = spurious_observer;
}

Does that solve the problem?

No, it does not work, I added the init if it could change something but it didnt.
And that observer probably will be deleted because is no longer used, the one it is gonna stay is the one from scopeGlobal.zig.

If I make Translation Unit a pointer in the Observer Params solves the problem, but it is not the desired behavior, It is a pointer just because.

Your imports are forming big ol’ loopy circles. That’s allowed in Zig, but it’s not a great idea to be this devoted to the practice, and this is why.

Somehow you’re forcing semantic analysis to know something about a type, before it’s possible for the type to exist. That might be due to a limitation in current-year comptime evaluation, or it might be that what you’re expressing is literally an impossible type of self-reference, I can’t tell.

I’d suggest that you go through and break as many of those cycles as you can. It’s likely that one of those will fix the problem.

Some time ago I tried to see how zig project are divided and how do I make imports.
That is why I try to join them in the mod.zig,
Is there a place I could use as reference?

What I’m suggesting is more specific than that. Somehow, your cyclic import system has introduced a circular dependency, type construction cannot continue because it relies on a type which cannot exist at the time semantic analysis needs it to.

You have a lot of cyclic imports, and not just short cycles either. Just get rid of as many of those as you can, and you’ll either fix the problem, or find it by the process of elimination.

Current comptime semantic analysis will have more such problems than is strictly required, Zig is a work in progress. But it’s also possible to write out types which are logically impossible, and I can’t tell which situation your code is in right now because the dependency graph is all over the place.

Could you please post the error you’re getting, with full reference trace? I tried looking and I’m not understanding which two lines you’re talking about.

Aside:

pub const ObserverParams = std.meta.Tuple(&.{ TranslationUnit, Allocator, Parser.NodeIndex, ?*Report.Reports });

Should be:

pub const ObserverParams = struct { TranslationUnit, Allocator, Parser.NodeIndex, ?*Report.Reports };
1 Like
 $ zig build -freference-trace=200
install
└─ install yot
   └─ compile exe yot Debug native 1 errors
src/TypeCheck/Expression.zig:318:5: error: dependency loop detected
pub const ObserverParams = struct { TranslationUnit, Allocator, Parser.NodeIndex, Parser.NodeIndex, ?*Report.Reports };
~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    VTable: src/TypeCheck/Scope/Scope.zig:6:20
    TypeCheck.Scope.Scope: src/TypeCheck/Scope/Scope.zig:4:16
    Scope: src/TypeCheck/mod.zig:6:27
    TranslationUnit: src/TranslationUnit.zig:17:17
    TranslationUnit: src/TypeCheck/Expression.zig:330:33
    toInferLater: src/TypeCheck/Expression.zig:67:27
    comptime: src/TypeCheck/Expression.zig:321:46
    Expression: src/TypeCheck/Scope/ScopeGlobal.zig:140:28
    TypeCheck.Scope.ScopeGlobal: src/TypeCheck/Scope/ScopeGlobal.zig:4:37
    ScopeGlobal: src/TypeCheck/mod.zig:4:33
    main: src/main.zig:101:32
    callMain [inlined]: /usr/lib/zig/std/start.zig:622:29
    callMainWithArgs [inlined]: /usr/lib/zig/std/start.zig:587:20
    posixCallMainAndExit: /usr/lib/zig/std/start.zig:542:36
    _start: /usr/lib/zig/std/start.zig:429:40
    comptime: /usr/lib/zig/std/start.zig:95:63
    start: /usr/lib/zig/std/std.zig:110:27
    comptime: /usr/lib/zig/std/std.zig:181:9
error: the following command failed with 1 compilation errors:
/usr/bin/zig build-exe -freference-trace=200 -ODebug --dep clap --dep BollYotz -Mroot=/home/pedro/Project/Yotz/src/main.zig -ODebug -Mclap=/home/pedro/.cache/zig/p/clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTS
z5H7e/clap.zig -MBollYotz=/home/pedro/Project/Yotz/BollYotz/main.zig --cache-dir .zig-cache --global-cache-dir /home/pedro/.cache/zig --name yot --zig-lib-dir /usr/lib/zig/ --listen=-

Build Summary: 2/5 steps succeeded; 1 failed
install transitive failure
└─ install yot transitive failure
   └─ compile exe yot Debug native 1 errors

error: the following build command failed with exit code 1:
.zig-cache/o/63aa21d95bb7c09b86c3cd9575056589/build /usr/bin/zig /usr/lib/zig /home/pedro/Project/Yotz .zig-cache /home/pedro/.cache/zig --seed 0x9589e884 -Z5277cf04a1391fcb -freference-trace=200

I’ve managed to simplify the dependency loop a little I think, but not yet solved.

 $ zig build -freference-trace=200
install
└─ install yot
   └─ compile exe yot Debug native 1 errors
src/Scope/ScopeGlobal.zig:137:5: error: dependency loop detected
pub const ObserverParams = struct { @import("../TranslationUnit.zig"), @import("std").mem.Allocator, Parser.NodeIndex, Parser.NodeIndex, ?*Report.Reports };
~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    VTable: src/Scope/Scope.zig:6:20
    Scope.Scope: src/Scope/Scope.zig:4:16
    Scope: src/Scope/mod.zig:1:27
    TranslationUnit: src/TranslationUnit.zig:17:13
    ObserverParams: src/Scope/ScopeGlobal.zig:137:45
    Scope.ScopeGlobal: src/Scope/ScopeGlobal.zig:4:37
    Global: src/Scope/mod.zig:2:28
    main: src/main.zig:101:28
    callMain [inlined]: /usr/lib/zig/std/start.zig:622:29
    callMainWithArgs [inlined]: /usr/lib/zig/std/start.zig:587:20
    posixCallMainAndExit: /usr/lib/zig/std/start.zig:542:36
    _start: /usr/lib/zig/std/start.zig:429:40
    comptime: /usr/lib/zig/std/start.zig:95:63
    start: /usr/lib/zig/std/std.zig:110:27
    comptime: /usr/lib/zig/std/std.zig:181:9
error: the following command failed with 1 compilation errors:
/usr/bin/zig build-exe -freference-trace=200 -ODebug --dep clap --dep BollYotz -Mroot=/home/pedro/Project/Yotz/src/main.zig -ODebug -Mclap=/home/pedro/.cache/zig/p/clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTS
z5H7e/clap.zig -MBollYotz=/home/pedro/Project/Yotz/BollYotz/main.zig --cache-dir .zig-cache --global-cache-dir /home/pedro/.cache/zig --name yot --zig-lib-dir /usr/lib/zig/ --listen=-

Build Summary: 2/5 steps succeeded; 1 failed
install transitive failure
└─ install yot transitive failure
   └─ compile exe yot Debug native 1 errors

error: the following build command failed with exit code 1:
.zig-cache/o/63aa21d95bb7c09b86c3cd9575056589/build /usr/bin/zig /usr/lib/zig /home/pedro/Project/Yotz .zig-cache /home/pedro/.cache/zig --seed 0xdbf5d647 -Z5681ff627b124d1a -freference-trace=200

I’m not sure if this problem is related to this issue

1 Like

It could be the same, instead of a function returning a type, is it the VTable

You could experiment with type erasing some of the pointers, to break the cycle.

It also might be possible to change some code to not depend on some type and thus break the cycle. (For example I had a case where I only needed the size of the type not the entire type, so through calculating the needed size in another (more manual) way, I could avoid the dependency on the type which caused the cycle, so avoid dependencies on full types when you really just need that type to calculate something)

I think type erasure is a pretty good tool to avoid creating too crazy type-dependency cycles, then you could possible separate your implementation into parts that are type erased and other parts which aren’t but make use of the former.

Also instead of each thing being given a bunch of types and calculating other derived types from that you may have to create a single type-family-function that gets all the input types and then calculates the correct types step by step, when writing things like this it becomes pretty obvious when you have some impossible cycle somewhere and also whether there is some trick to break it (like the case where just the size needed to match).

Another tip would be to choose when and what has to be typed vs can be untyped, for example you could have some pointers type erased and then write a function that is given the actual type as a parameter, thus the type only needs to be known by the callsite of that function. (This is one way you could get rid of some of the dependencies)

Potentially.

But let’s look at the stack trace:

src/Scope/ScopeGlobal.zig:137:5: error: dependency loop detected
pub const ObserverParams = struct { @import("../TranslationUnit.zig"), @import("std").mem.Allocator, Parser.NodeIndex, Parser.NodeIndex, ?*Report.Reports };
~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    VTable: src/Scope/Scope.zig:6:20
    Scope.Scope: src/Scope/Scope.zig:4:16
    Scope: src/Scope/mod.zig:1:27
    TranslationUnit: src/TranslationUnit.zig:17:13
    ObserverParams: src/Scope/ScopeGlobal.zig:137:45
    Scope.ScopeGlobal: src/Scope/ScopeGlobal.zig:4:37

You’re inside ObserverParams, trying to define… ObserverParams. Obviously that can’t happen, infinite regress.

1 Like

wouldn’t this need to be

const spurious_observer: TypeCheck.Observer = .{};

comptime {
     _  = &spurious_observer;
}
1 Like

I haven’t looked at the code, but, usually, the fastest and hackiest way of getting rid of a dependency loop is to set a parameter type to anytype.

1 Like

Why do you say ā€œshould beā€ here? Because I’m pretty sure this original solution is why there’s no dependency loop. in ObserverParams = std.meta.Tuple(&.{ TranslationUnit, Allocator, Parser.NodeIndex, ?*Report.Reports }); the tuple .{ TranslationUnit, Allocator, Parser.NodeIndex, ?*Report.Reports } exists before ObserverParams, and it’s pointer is location is passed into std.meta. Thus; because it’s already resolved, prior to trying to resolve ObserverParams, the compiler doesn’t need to look them up to resolve ObserverParams. I think…

I only moderately confident about this… so either way, @EvilAlliance the zig way to solve this is make these types, an argument that generates a type for you. Expression.zig would become a function, and take any set of params. Then you don’t need to call

comptime {
    expected = util.getTupleFromParams(...);
    ...
}

at all, because every call carries that information with it. The compiler will resolve it for you, and you don’t have to worry about missing it because the type system will prevent you from passing the wrong, or incompatible type.

1 Like

Instead of making the Translation Unit a pointer, and have to allocate in the heap each time I prefer making the observer params in the scope a opaque pointer, the type safety is a little lost, but is a little less than the other.

And yes the problem there was translation unti, I think that the bug mentioned is the cause because to have the size of the VTable it is not needed to know the size of a parameter

Update: I fount a way to keep the Type Safety, I can make the argument of the method the observer params, but the rest as *anyopaque

using @ptrCast() removes type safety. So I don’t think you’re keeping type safety with this solution.

1 Like

Indeed, but based on my analysis of the source code, this dependency chain ultimately simplifies to this:

const ObserverParams = struct {TranslationUnit};
const TranslationUnit = struct {scope: Scope};
const Scope= struct {
    ptr: *anyopaque,
    vtable: *const Vtable,
    pub fn waitingFor(self: Scope, args: ObserverParams) void {
        try self.vtable.waitingFor(self.ptr, args);
    }
};
const Vtable = struct {
    waitingFor: *const fn (ctx: *anyopaque, args: ObserverParams) void,
};

An equivalent piece of code with a similar ā€˜circular dependency’ would look something like this.

const A = struct { B };
const B = struct {
    ptr: *anyopaque,
    doFunc: *const fn (ctx: *anyopaque, args: A) void,
    pub fn do(self: B, args: A) void {
        return self.doFunc(self.ptr, args);
    }
};
test "deploop" {
    try std.testing.expect(A == struct { B });
}

But the above test can pass successfully.