Type ‣ Union ᴾᴴᴾ - chung-leong/zigar GitHub Wiki
JavaScript | PHP
A union in the Zig language is a data type that can hold one of multiple variables, typically of different types. In PHP, it behaves like an object whose properties are all inactive, save for one:
const std = @import("std");
pub const Number = union(enum) {
integer: i32,
big_integer: i64,
decimal: f64,
complex: S0,
};
pub const a: Number = .{ .integer = 123 };
pub const b: Number = .{ .big_integer = 1234567890 };
pub const c: Number = .{ .decimal = 0.12345 };
pub const d: Number = .{ .complex = .{ .re = 1, .im = 2 } };
pub var v: Number = .{ .big_integer = 8888 };
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
debug_zval_dump($m->a->integer);
debug_zval_dump($m->b->big_integer);
debug_zval_dump($m->c->decimal);
debug_zval_dump($m->d->complex);
int(123)
int(1234567890)
float(0.12345)
object(S0)#38 (2) refcount(2){
["re"]=>
float(1)
["im"]=>
float(2)
}
In the example above, the only active property for $m->a is integer. For $m->b, it's
big_integer. What happens when you access an inactive property depends on the kind of union
involved. There are three kinds: tagged union, bare union, and extern union.
Tagged union
A tagged unions uses an enum to keep track of which of its field is active. You can obtain the current tag by casting the union to its tag type:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
debug_zval_dump($m->Number->__tag($m->a) === $m->Number->__tag->integer);
echo "{$m->Number->__tag($m->a)}\n";
bool(true)
integer
When you read an inactive field/property of a tagged union, you get null:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
debug_zval_dump($m->b->big_integer);
debug_zval_dump($m->b->integer);
int(1234567890)
NULL
This behave is different from that in Zig itself, where such access would trigger a panic when runtime safety is active. This deviation was decided upon because it allows the use of PHP's null coalescing operator:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
$c = $m->c;
echo $c->integer ?? $c->big_integer ?? $c->decimal, "\n";
0.12345
Assignment to an inactive field would trigger an error:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
debug_zval_dump($m->v->big_integer);
debug_zval_dump($m->v->integer);
try {
$m->v->integer = 1234;
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
int(8888)
NULL
unable to write field 'integer' in union 'Number': access of union field 'integer' while field 'big_integer' is active (zig)
In order to switch to a different field, you need to assign to the union itself:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
debug_zval_dump($m->v->integer);
$m->v = [ 'integer' => 1234 ];
debug_zval_dump($m->v->integer);
NULL
int(1234)
Like a struct, a union provides an iterator for its properties. This iterator will always yield a single entry:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
foreach ($m->c as $tag => $value) {
echo "$tag => $value\n";
}
decimal => 0.12345
When using a switch statement to determine the course of action, remember that the tag you get from the iterator is a string and not an enum object:
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
foreach ([$m->a, $m->b, $m->c, $m->d] as $number) {
foreach ($number as $tag => $value) {
switch ($tag) {
case $m->Number->__tag->integer:
echo "This is not reachable\n";
break;
case 'integer':
echo "Do something with integer\n";
break;
case 'big_integer':
echo "Do something with big integer\n";
break;
case 'decimal':
echo "Do something with decimal number\n";
break;
case 'complex':
echo "Do something with complex number\n";
break;
}
}
}
Do something with integer
Do something with big integer
Do something with decimal number
Do something with complex number
Another way of obtaining the active tag is by casting the union into a string:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
foreach ([$m->a, $m->b, $m->c, $m->d] as $number) {
switch ((string) $number) {
case 'integer':
echo "Do something with integer\n";
break;
case 'big_integer':
echo "Do something with big integer\n";
break;
case 'decimal':
echo "Do something with decimal number\n";
break;
case 'complex':
echo "Do something with complex number\n";
break;
}
}
Do something with integer
Do something with big integer
Do something with decimal number
Do something with complex number
You can also use the == operator to check which tag is active:
<?php
$m = zigar_use(__DIR__ . '/tagged-union-example-1.zig');
foreach ([$m->a, $m->b, $m->c, $m->d] as $number) {
if ($number == 'integer') {
debug_zval_dump($number->integer);
} else if ($number == 'big_integer') {
debug_zval_dump($number->big_integer);
} else if ($number == 'decimal') {
debug_zval_dump($number->decimal);
} else if ($number == 'complex') {
debug_zval_dump($number->complex);
}
}
int(123)
int(1234567890)
float(0.12345)
object(S0)#38 (2) refcount(2){
["re"]=>
float(1)
["im"]=>
float(2)
}
Bare union
A bare union does not have a tag indicating which of its fields is active. You must rely on some external mean.
const std = @import("std");
pub const Number = union {
integer: i32,
big_integer: i64,
decimal: f64,
complex: S0,
};
pub const a: Number = .{ .integer = 123 };
pub const b: Number = .{ .big_integer = 1234567890 };
pub const c: Number = .{ .decimal = 0.12345 };
pub const d: Number = .{ .complex = .{ .re = 1, .im = 2 } };
<?php
$m = zigar_use(__DIR__ . '/bare-union-example-1.zig');
debug_zval_dump($m->a->integer);
debug_zval_dump($m->b->big_integer);
debug_zval_dump($m->c->decimal);
debug_zval_dump($m->d->complex);
int(123)
int(1234567890)
float(0.12345)
object(S0)#33 (2) refcount(2){
["re"]=>
float(1)
["im"]=>
float(2)
}
In the example above, we know which field is active for each union only by looking at the source code. In an actual program, this information would need be stored in some variables, somewhere.
When optimization level is Debug or ReleaseSafe, the Zig compiler would add a hidden tag to a
bare union to enable runtime safety checks. Zigar also uses this information to warn you when your
code accesses invalid data:
<?php
$m = zigar_use(__DIR__ . '/bare-union-example-1.zig');
try {
debug_zval_dump($m->b->big_integer);
debug_zval_dump($m->b->integer);
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
int(1234567890)
unable to read field 'integer' in union 'Number': access of union field 'integer' while field 'big_integer' is active (zig)
This check is turned off when optimize is set to ReleaseFast or ReleaseSmall:
<?php
$m = zigar_use(__DIR__ . '/bare-union-example-1.zig', [
'optimize' => 'ReleaseSmall'
]);
try {
debug_zval_dump($m->b->big_integer);
debug_zval_dump($m->b->integer);
debug_zval_dump($m->b->decimal);
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
int(1234567890)
int(1234567890)
float(6.09957582E-315)
The absence of a tag makes the iterator of a bare union practically useless, since it always
returns entries of all fields. The special property __plain likewise yields nonsensical
results.
<?php
$m = zigar_use(__DIR__ . '/bare-union-example-1.zig');
foreach ($m->b as $tag => $value) {
echo "$tag => ";
print_r($value);
echo "\n";
}
print_r($m->b->__plain);
integer => 1234567890
big_integer => 1234567890
decimal => 6.0995758190772E-315
complex => S0 Object
(
[re] => 6.0995758190772E-315
[im] => 0
)
stdClass Object
(
[integer] => 1234567890
[big_integer] => 1234567890
[decimal] => 6.0995758190772E-315
[complex] => stdClass Object
(
[re] => 6.0995758190772E-315
[im] => 0
)
)
Another major shortcoming of bare unions is that pointers within them are not accessible:
const std = @import("std");
const IntegerOrTextT = union(enum) {
number: i32,
text: []const u8,
};
const IntegerOrTextB = union {
number: i32,
text: []const u8,
};
pub fn getT(allocator: std.mem.Allocator, text: bool) !IntegerOrTextT {
return if (text)
.{ .text = try allocator.dupe(u8, "Hello") }
else
.{ .number = 1234 };
}
pub fn getB(allocator: std.mem.Allocator, text: bool) !IntegerOrTextB {
return if (text)
.{ .text = try allocator.dupe(u8, "Hello") }
else
.{ .number = 1234 };
}
<?php
$m = zigar_use(__DIR__ . '/bare-union-example-2.zig');
try {
debug_zval_dump($m->getT(false)->number);
debug_zval_dump($m->getT(true)->text->__string);
debug_zval_dump($m->getB(false)->number);
debug_zval_dump($m->getB(true)->text->__string);
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
int(1234)
string(5) "Hello" refcount(1)
int(1234)
unable to read field '__string' in pointer '[]const u8': pointer is inaccessible because it's in an untagged union (zig)
In the example above, Zigar is able to tell whether IntegerOrTextT.text is a valid pointer thanks
to the presence of a tag. The status of IntegerOrTextB.text is unknown, on the other hand. It
might be a valid address--or it might be 1234. This uncertainty means that throwing an error is the
only reasonable action to take.
Extern union
An extern union is like a bare union, except there is no check even when optimize is Debug:
const std = @import("std");
pub const Number = extern union {
integer: i32,
big_integer: i64,
decimal: f64,
};
pub const a: Number = .{ .integer = 123 };
pub const b: Number = .{ .big_integer = 1234567890 };
pub const c: Number = .{ .decimal = 0.12345 };
<?php
$m = zigar_use(__DIR__ . '/extern-union-example-1.zig');
try {
debug_zval_dump($m->b->big_integer);
debug_zval_dump($m->b->integer);
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
int(1234567890)
int(1234567890)
Note the absence of the complex field in the example above. We're forced to remove it because an
extern union cannot contain a non-extern struct.