Skip to content

Adding Type Initialization Grouping and Ordering #1378

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

grantnelson-wf
Copy link
Collaborator

@grantnelson-wf grantnelson-wf commented Jul 2, 2025

When the JS packages are being setup, type instances could require type information from packages which have not been setup yet. For example, list.List[Cat] could be defined inside the cat package and import the list package. The type instance List[cat.Cat] is initialized inside the list package. This will cause a failure because cat.Cat hasn't been setup yet since it requires list to be setup first as an import. We can't move List[Cat] to the cat package because List[T] may access unexposed data inside the list package.

This is the first part of two (or more) tickets. To solve the type initialization, each declaration will be given a group number. There will be $n$ groups and each group number is $g_i$ where $0 \le i < n$. For any group, $g_i$, the prior groups, $\{g_0 ... g_{i-1}\}$, will have to be initialized first. All the declarations with the same group number are part of the same group. However within that group, the declarations will still need to be initialized in the current ordering where imports are done before importers and types are initialized before they are used in pointers, slices, etc.

This PR determines the grouping and order of the groups, then sets the group number for each Decl. This PR mainly consists of two new packages: sequencer and grouper. The sequencer is a generalized tool for determining ordering and grouping of items depending on the items' dependencies. The grouper stores information about the declarations' types so that it can use the sequencer to group and order the declarations based on type.

Following ticket will use that group number to populate a "startup map" where the key is the group number and the value is a set of functions that need to be run. Each function will initialize all the Decls for a particular group for a single package. Then once all the packages have been added, the runtime will call each group of functions in order.

@grantnelson-wf grantnelson-wf self-assigned this Jul 2, 2025
@grantnelson-wf grantnelson-wf marked this pull request as ready for review July 2, 2025 22:03
@nevkontakte
Copy link
Member

It may be that I'm missing something, but this seems more complicated than I expected. To keep things concrete, I made a small reproduction example, which is attached: 049-generic-circle.tar.gz

It's a very minimal illustration of the problem you've been working on:

// main.go
package main

import "gencicrle/foo"

func main() {
	e := foo.Entity{}
	println(e.Ref.Next)
}

// foo/foo.go
package foo

import "gencicrle/bar"

type Entity struct {
	Ref bar.Bar[Entity]
}

// bar/bar.go
package bar

type Bar[G any] struct {
	Next *G
}

If I run it as-is, I get this error:

$ GOPHERJS_EXPERIMENT="generics" gopherjs build . && node 049-generic-circle.js
/home/aleks/tmp/049-generic-circle/049-generic-circle.js:2644
        ptrType = $ptrType($packages["gencicrle/foo"].Entity);
                                                      ^

TypeError: Cannot read properties of undefined (reading 'Entity')
    at /home/aleks/tmp/049-generic-circle/049-generic-circle.js:2644:48
    at Object.<anonymous> (/home/aleks/tmp/049-generic-circle/049-generic-circle.js:2654:3)
    at Object.<anonymous> (/home/aleks/tmp/049-generic-circle/049-generic-circle.js:2707:4)
    at Module._compile (node:internal/modules/cjs/loader:1730:14)
    at Object..js (node:internal/modules/cjs/loader:1895:10)
    at Module.load (node:internal/modules/cjs/loader:1465:32)
    at Function._load (node:internal/modules/cjs/loader:1282:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
    at node:internal/main/run_main_module:36:49

Node.js v22.16.0

This is expected because there is a circular dependency in the generated code, let's take a look at it, I marked the interesting line with // EXCEPTION HERE:

$packages["gencicrle/bar"] = (function() {
	var $pkg = {}, $init, Bar, ptrType;
	Bar = {};
	Bar[0 /* gencicrle/foo.Entity */] = $newType(0, $kindStruct, "bar.Bar[gencicrle/foo.Entity]", true, "gencicrle/bar", true, function(Next_) {
		this.$val = this;
		if (arguments.length === 0) {
			this.Next = ptrType.nil;
			return;
		}
		this.Next = Next_;
	});
	ptrType = $ptrType($packages["gencicrle/foo"].Entity); // EXCEPTION HERE
	$pkg.Bar = Bar;
	Bar[0 /* gencicrle/foo.Entity */].init("", [{prop: "Next", name: "Next", embedded: false, exported: true, typ: ptrType, tag: ""}]);
	$init = function() {
		$pkg.$init = function() {};
		/* */ var $f, $c = false, $s = 0, $r; if (this !== undefined && this.$blk !== undefined) { $f = this; $c = true; $s = $f.$s; $r = $f.$r; } s: while (true) { switch ($s) { case 0:
		/* */ } return; } if ($f === undefined) { $f = { $blk: $init }; } $f.$s = $s; $f.$r = $r; return $f;
	};
	$pkg.$init = $init;
	return $pkg;
})();
$packages["gencicrle/foo"] = (function() {
	var $pkg = {}, $init, bar, Entity, ptrType;
	bar = $packages["gencicrle/bar"];
	Entity = $newType(0, $kindStruct, "foo.Entity", true, "gencicrle/foo", true, function(Ref_) {
		this.$val = this;
		if (arguments.length === 0) {
			this.Ref = new bar.Bar[0 /* gencicrle/foo.Entity */].ptr(ptrType.nil);
			return;
		}
		this.Ref = Ref_;
	});
	ptrType = $ptrType(Entity);
	$pkg.Entity = Entity;
	Entity.init("", [{prop: "Ref", name: "Ref", embedded: false, exported: true, typ: bar.Bar[0 /* gencicrle/foo.Entity */], tag: ""}]);
	$init = function() {
		$pkg.$init = function() {};
		/* */ var $f, $c = false, $s = 0, $r; if (this !== undefined && this.$blk !== undefined) { $f = this; $c = true; $s = $f.$s; $r = $f.$r; } s: while (true) { switch ($s) { case 0:
		$r = bar.$init(); /* */ $s = 1; case 1: if($c) { $c = false; $r = $r.$blk(); } if ($r && $r.$blk !== undefined) { break s; }
		/* */ } return; } if ($f === undefined) { $f = { $blk: $init }; } $f.$s = $s; $f.$r = $r; return $f;
	};
	$pkg.$init = $init;
	return $pkg;
})();

We are creating a composite type definition ptrType = $ptrType($packages["gencicrle/foo"].Entity), which we would later need to initialize the reflection info about struct type: Bar[0 /* gencicrle/foo.Entity */].init("", [{prop: "Next", name: "Next", embedded: false, exported: true, typ: ptrType, tag: ""}]);

Note that if you look closely on how Bar is initialized, it happens in two phases, same with the Entity:

// $package['bar'] = ...
// Create type's object:
Bar[0 /* gencicrle/foo.Entity */] = $newType(0, $kindStruct, "bar.Bar[gencicrle/foo.Entity]", true, "gencicrle/bar", true, function(Next_) { /* omitted for brevity */ });
// Initialize extended type information:
Bar[0 /* gencicrle/foo.Entity */].init("", [{prop: "Next", name: "Next", embedded: false, exported: true, typ: ptrType, tag: ""}]);

// $package['foo'] = ...
// Create type's object:
Entity = $newType(0, $kindStruct, "foo.Entity", true, "gencicrle/foo", true, function(Ref_) { /* omitted for brevity */ });
// Initialize extended type information:
Entity.init("", [{prop: "Ref", name: "Ref", embedded: false, exported: true, typ: bar.Bar[0 /* gencicrle/foo.Entity */], tag: ""}]);

Two important observations are:

  • In order to create a type object, you don't need any other types that it may depend on to be initialized.
  • When initializing the type, we just need a valid reference to the type object, but we don't really care if it is initialized yet.

So our problem would be solved if we simply delay calls to the <type>.init() in all packages until all type objects have been created. As a proof of concept, I moved then inside $init(), which is called before main():

    $packages["gencicrle/bar"] = (function () {
        var $pkg = {}, $init, Container, sliceType;
        Container = {};
        Container[0 /* gencicrle/foo.Entity */] = $newType(0, $kindStruct, "bar.Container[gencicrle/foo.Entity]", true, "gencicrle/bar", true, function (Items_) {
            this.$val = this;
            if (arguments.length === 0) {
                this.Items = sliceType.nil;
                return;
            }
            this.Items = Items_;
        });
        $pkg.Container = Container;
        $init = function () {
            sliceType = $sliceType($packages["gencicrle/foo"].Entity); // MOVED THIS
            Container[0 /* gencicrle/foo.Entity */].init("", [{ prop: "Items", name: "Items", embedded: false, exported: true, typ: sliceType, tag: "" }]); // MOVED THIS

            $pkg.$init = function () { };
		/* */ var $f, $c = false, $s = 0, $r; if (this !== undefined && this.$blk !== undefined) { $f = this; $c = true; $s = $f.$s; $r = $f.$r; } s: while (true) {
                switch ($s) {
                    case 0:
                    /* */
} return;
            } if ($f === undefined) { $f = { $blk: $init }; } $f.$s = $s; $f.$r = $r; return $f;
        };
        $pkg.$init = $init;
        return $pkg;
    })();
    $packages["gencicrle/foo"] = (function () {
        var $pkg = {}, $init, bar, Entity, sliceType;
        bar = $packages["gencicrle/bar"];
        Entity = $newType(0, $kindStruct, "foo.Entity", true, "gencicrle/foo", true, function (Ref_) {
            this.$val = this;
            if (arguments.length === 0) {
                this.Ref = new bar.Container[0 /* gencicrle/foo.Entity */].ptr(sliceType.nil);
                return;
            }
            this.Ref = Ref_;
        });
        sliceType = $sliceType(Entity);
        $pkg.Entity = Entity;
        $init = function () {
            Entity.init("", [{ prop: "Ref", name: "Ref", embedded: false, exported: true, typ: bar.Container[0 /* gencicrle/foo.Entity */], tag: "" }]); // MOVED THIS
            $pkg.$init = function () { };
		/* */ var $f, $c = false, $s = 0, $r; if (this !== undefined && this.$blk !== undefined) { $f = this; $c = true; $s = $f.$s; $r = $f.$r; } s: while (true) {
                switch ($s) {
                    case 0:
                        $r = bar.$init(); /* */ $s = 1; case 1: if ($c) { $c = false; $r = $r.$blk(); } if ($r && $r.$blk !== undefined) { break s; }
                    /* */
} return;
            } if ($f === undefined) { $f = { $blk: $init }; } $f.$s = $s; $f.$r = $r; return $f;
        };
        $pkg.$init = $init;
        return $pkg;
    })();

Full source code: 049-generic-circle.js.gz

After which it runs correctly:

$ node 049-generic-circle.js
<ref *1> {
  '$val': [Circular *1],
  Items: <ref *2> typ {
    '$array': [],
    '$offset': 0,
    '$length': 0,
    '$capacity': 0,
    '$val': [Circular *2]
  }
}

Now, I cheated a little bit here, because I also moved sliceType = $sliceType($packages["gencicrle/foo"].Entity) into $init because I know its actual value wouldn't be needed until then. That said, I think it would generally work: the variables the anonymous types are assigned to should be captured by all the closures and later assignment should not be an issue; though that needs to be properly tested.

Anyway, all of this is a long way of saying, that I don't feel like we need a complex ordering algorithm to determine the order the decls need to be initialized in. I think simply grouping the type object creation and type initialization should give us what we want.

@grantnelson-wf
Copy link
Collaborator Author

grantnelson-wf commented Jul 24, 2025

I think I've figured out what you (@nevkontakte) were thinking and think I may have a solution where the "cheat" is done programmatically. I'm breaking up the DeclCode into more specific types of declarations and adding a $finishSetup method.

While trying to understand what you meant I learned about how JS handles variables in closures (like those for constructors) and that was the "ohhhh, I get it" moment for me. Those variables are in places where we don't have to initialize them until later since they won't be used for a while. I was trying to get every type initialized prior to it being used anywhere, including in a closure, but in our JS you don't have to do that.

I'm testing my code-up of your solution and will have a new PR up soon with that work and I'll close this PR (unless I run into something that may require type ordering still, then I'll leave it open so we can discuss alternatives more).

@grantnelson-wf
Copy link
Collaborator Author

Here is the WIP (#1380) that I'm running tests on. You can peek at it and tell me if that's what you were thinking or wait. Either way, once I'm confident I got it working (and move over some tests from the this PR to that one), I'll let you know when it's ready for review

@grantnelson-wf grantnelson-wf deleted the orderInit branch July 24, 2025 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants