I see two basic ways here
Allow (immutable) initialization of objects independent on order of definition
If one wants data structures with references or cycles one simply could spell out the cycles/references explicitly and assign the intermediate objects a name. This is the way all programmers are used to. The ordinary inline expression syntax only allows for tree-like data structures. I would not change that.
In go-like syntax this could look like (this is just a syntax example)
type A struct {
Bref *B
}
type B struct {
Aref *A
}
const Aobj = A{Bref: &Bobj}
const Bobj = B{Aref: &Aobj}
Note: There is no fundamental reason why the above could not work. Currently in go this fails, because go allows arbitrary code expressions in initialization and tries to figure out the initialization order. However for the plain system-defined constructor A{...} or B{...} the order of initialization does not matter and it would, in this special case, be ok to accept this program as legal go.
But you were only asking for the syntax how this could look like. And this is one example.
Also one could envision to define local constants this way, too. Key is to give up the ordering of the definitions.
Two stage initialization/construction
This is not directly related on how to literally specify cyclic data structures, but I think it is important nontheless and completemts the other above.
Clearly, one can construct non-tree datastructures with deeply immutable types when one allows the objects to be mutable in the construction phase.
Or in other words have a two stage construction.
First create the object with all fields initialized with default values and then secondly fill in the correct values (perhaps this is even user-code).
Just after these two phases the object would become immutable.
Of course this has the complication that the constructor code has to take care that dependent objects may be not fully constructed yet.
In C++ it is already almost like this. Upon entry of the constructor the 'this' pointer points to an allocated object. In the body of the constructor some recursive algorithm could run, which then uses the 'this' pointer to create cyclic references even though the object is not fully constructed yet.
Key enabler here would be to allow the constructor (and only the constructor) to modify the fields even for immutable types.
Similar thing exists in Python with frozen dataclasses. Since frozen dataclasses are not really a first-class immutable type and just emulate immutability one can in the constructor indeed set the fields. But that just highlights the general problem on how to extend or subclass immutable types.
Remark
BTW your question is missing an important detail. You do not specify what kind of "immutability" you refer to.
Is it "shallow immutability" or "deep immutability" [1]?
Python: tuple itself can contain mutable objects and these objects can be mutated -- they just cannot be replaced in the tuple.
The shallow immutability already solves the covariance / contravariance problems of container types. But the deep immutability is needed for the thread safety. This needs to be taken into account in your design.
x = x[0] = [...], even though it still relies on mutation behind the scenes. $\endgroup$