Values and Var
Most data in an x2c program has an ordinary C type: int, double, char *,
or a struct you declared.
Some code has to hold whatever the caller supplied. The element of a
heterogeneous list, the value stored behind a map key, Error detail, the
argument to a routine that prints anything: none of those have one static
type. In plain C you would write a tagged union by hand and a switch for every
operation. x2c provides that type: Var.
A value that carries its own type
A Var is a single 64-bit value holding a payload plus a tag that names what
the payload is. Assigning a typed value into a Var boxes it, and the tag
records where the value came from.
Var count = 42;
Var name = "ada";
Var setting = <verbose>;
Var numbers = %(1 2 3);
printf("%s %s %s %s\n",
count.tag().str(), name.tag().str(),
setting.tag().str(), numbers.tag().str());
i32 string symbol list
Three queries read the tag. Var.tag returns the exact family, as above.
Var.kind returns the coarse category: <integer>, <floating>, <symbol>,
<object>, <pointer>, <reference>, or <void>. Use it when a whole group
of tags is treated alike. value is i32 tests for one specific tag.
Every Var has the same size and shape whatever it holds, so heterogeneous
containers work. List, Array, and Map all store Var, so a list can mix
a number, a String, and a Symbol.
Var count = 42;
Var name = "ada";
List mixed = %($count $name verbose);
foreach(Var item, mixed)
printf("%s = %s\n", item.tag().str(), item);
i32 = 42
string = ada
symbol = verbose
A Var is eight bytes, and most families keep their payload inside those
bits with no allocation. Five native families cannot: long,
unsigned long, long long, unsigned long long, and long double.
Boxing one of those allocates a small immutable box in the active scope, so
the boxed value follows the scope lifetime described in
scopes and lifetime. Those five tags are spelled <long>,
<ulong>, <llong>, <ullong>, and <ldouble>. Their payload widths follow
the corresponding native C types.
The complete tag set and the mapping from native source types to tags are in the language reference.
Getting the value back out
Assigning a Var to a typed target unboxes it, and the compiler inserts the
conversion for you.
Var boxed = 42;
int native = boxed;
double widened = boxed;
printf("%d %f\n", native, widened);
42 42.000000
Var.convert performs every numeric conversion, and compiler-inserted
unboxing calls it before using the target’s extractor. int native = boxed;
and an explicit conversion behave the same way.
A Var argument unboxes the same way at any call whose prototype x2c has
parsed. x2c does not read system headers, so the runtime declares the C99
<math.h> functions itself; a Var holding a number goes straight into one
with the ordinary include:
#include <math.h>
int main(void) {
Var ratio = 2.5;
printf("%f\n", sin(ratio));
return 0;
}
0.598472
The scalar-named readers, Var.int, Var.short, Var.uchar, and their
siblings, work the same way. A matching tag reads its payload directly, and
any other numeric tag converts through Var.convert, so a reader call and
an assignment agree:
Var small = (unsigned char) 44;
int converted = small;
int reader = small.int();
printf("tag=%s converted=%d reader=%d\n",
small.tag().str(), converted, reader);
tag=u8 converted=44 reader=44
A nonnumeric source raises the same structured Error either way. The raw
payload readers, Var.integer, Var.floating, and the wide *_value
readers, are silent low-level decoders that return zero for a tag they do
not handle.
A crossing to a pointer-shaped runtime type has nothing to convert. The payload
either has that tag or it does not. Assigning to List, Map, Array,
Symbol, or a type with a declared protocol Var(T) conversion uses that
type’s exact reader, and a wrong tag yields NULL. An alias of such a type,
declared with class or typedef, uses the reader of its nearest ancestor
that has one. A String destination reads a String payload as itself,
displays any other value through .str(), and reads void and Null as NULL.
The explicit readers .list(), .map(), .array(), .block(), .buffer(),
.bytes(), .file(), .as_iter(), and .string() extract their payload and
raise <no-convert> for any other tag. Their _payload forms, such as
.list_payload(), read the payload or NULL without raising, as an assignment
does:
Var listy = %(1 2 3);
String text = listy;
List same = listy;
printf("text=%s same=%d\n", text, same != NULL);
text=( 1 2 3 ) same=1
Call .list() when a value must be a List, so a wrong tag is reported
where it is read. Var.pointer is the unchecked read, for a raw native
pointer that has no tag to check.
Formatting dynamic values
String interpolation displays a Var with Var.str, whether it holds a
number, a String, or another dynamic value:
Var key = 23, val = 3.14, answer = "hello";
puts(%"output: ($key: $val) = $answer");
A numeric segment of an interpolated String renders through its nearest
declared T_str converter when it has one and through Var.str otherwise.
Converter lookup follows the source’s typedef chain, and an exact converter
wins. A segment already typed Var renders through Var.str whatever tag it
holds:
int count = 2;
Var value = 3.14;
String counted = %"processed $count items"; /* processed 2 items */
String read = %"v = $value"; /* v = 3.140000 */
A printf-family call with a single static format literal can use the format to request native numeric arguments:
Var key = 23, val = 3.14, answer = "hello";
printf("output: (%d: %.2lf) = %s\n", key, val, answer);
For numeric conversions, the compiler inserts the checked Var.convert
path used by typed assignment and then passes the format’s promoted C vararg
type. A Var supplied for %s uses Var.str. This applies to printf,
fprintf, sprintf, snprintf, and the printf methods on String, File,
and Buffer.
The format must be a direct static literal whenever a variadic argument is a
Var. Automatic lowering covers integer, floating, %c, %s, width, and
precision arguments with the hh, h, l, ll, and L modifiers. Use an
explicit native conversion for j, z, t, %p, %n, wide
character/string conversions, positional formats, or a dynamic format. Calls
containing only native arguments keep C’s printf behavior.
Null and void
Null is the all-zero Var: the null pointer, or the external nil. It is
ordinary data. You may store it in a List, an Array, or a Map, and
iterating a collection can return it.
void is the all-ones Var. APIs return it to report that no value is
available: an index out of range, an absent key, or an exhausted iterator.
No collection or iterator holds it. You cannot push it into an Array,
store it as a Map key or value, or receive it as a successful iterator
payload. In an expression, lowercase void is the literal for this
sentinel. In a declaration, cast, sizeof, or parameter list, void keeps
its C meaning.
You can inspect the sentinel without it becoming data. Two void operands
are equal with == and identical with ===, and != and !== are their
inverses. A sentinel and any ordinary value are unequal. The same rules
apply to Var.equal, Var.fallback_equal, Var.same, and the four
comparison operations accepted by Var.binary. Display and readable output
both spell the sentinel void.
void is what you get from an out-of-range Array or List read and from
a missing key in a plain Map. A String read out of range returns -1, a
packed typed Array bracket is unchecked, and a typed Map raises
<bad-arg> for a missing key:
List items = %(10 20 30);
Var present = items[1];
Var missing = items[7];
printf("present=%d missing=%d\n", present is void, missing is void);
present=0 missing=1
A missing Map key reads the same way, but Map also offers a status-bearing
form that keeps presence separate from the payload. Prefer it:
Map settings = {width: 80};
Var height;
if (settings.try_get(<height>, height)) {
int value = height;
printf("height is %d\n", value);
} else {
printf("height is unset\n");
}
height is unset
Null is a value that happens to be zero. NULL converts to the same value
when an x2c crossing requires a Var:
Var absent = (Var) { .u64 = 0 };
Var also_absent = NULL;
List row = %($absent 1);
printf("null=%d void=%d truthy=%d len=%d\n",
absent.is_null(), absent is void, absent.truth(), row.len());
null=1 void=0 truthy=0 len=2
In a condition, Null is false, and a truthiness test on void raises
<void-op>. A matching filtered catch
receives that error; without one, the default abort terminates the program.
Do not write if (maybe_missing) on a value that might be void. Test it
with Var.is_void, or use the status-bearing API and never hold a void.
The representation of void and its exclusion from collections and iterators
are consistent. Its meaning in a particular API can vary: missing,
exhausted, or invalid. Read that API’s description before interpreting the
result.
Truthiness
When a condition’s static type participates in a protocol with truth, the
compiler applies that member. Dynamic Var uses Var.truth; built-in
Strings, Lists, Arrays, Maps, and Buffers use their concrete content
rule. This covers if, while, do, classic for, ?:, unary !, and each
participating operand of && and ||. Short-circuit evaluation is untouched.
Each operand is converted only when C would evaluate it, so the right-hand
side of && does not run when the left is false.
Var name = "";
Var full = "ada";
if (!name) printf("no name\n");
if (full && full is <string>) printf("name=%s\n", full);
no name
name=ada
False: numeric zero, Symbol zero, Null and null pointers, the canonical
empty String and empty List, and empty Array, Map, Block, Bytes, or
Buffer values. True: everything else nonzero or nonnull, including NaN and
the infinities, which are numbers. A nonnull Iter is true even when it is
exhausted. The condition tests the iterator pointer; it does not try to advance
it.
Arithmetic on Var
If either operand of +, -, *, /, %, <<, >>, &, ^, or |
is a Var and the other is a Var or statically numeric, both operands
are boxed as Var, the runtime performs the operation, and the result is a
Var. You write C syntax; the compiler inserts the boxing.
Var count = 10;
Var total = count * 3 + 1;
double scaled = count * 2.5;
printf("total=%s scaled=%f\n", total, scaled);
total=31 scaled=25.000000
Expressions with no Var operand stay native C and make no runtime calls.
Integer operands use integer promotion; arithmetic keeps the promoted type’s
low bits, so overflow wraps to that type’s width. Floating operands work in
the widest participating floating family with host behavior. Remainder, the
shifts, and the bitwise operators reject a floating operand. There is no
dynamic unary numeric operator, and no dynamic ++ or --.
A statically nonnumeric peer is a compile-time error:
Var count = 10;
String label = "items";
Var bad = count + label; /* xform: dynamic numeric operators require
numeric operands */
A Var that happens to hold a String passes the static check and fails
at run time instead.
Comparisons have their own rule. A comparison of two numeric Vars behaves
as the same comparison between their native values in C, after the usual
arithmetic conversions. === and !== on numbers require the same type and
the same bits. Other Var operands compare through Var.equal, Var.same,
and Var.compare.
Var three = 3, wide = 3L, nan = 0.0 / 0.0;
printf("3 == 3L: %d 3 === 3L: %d -1 < 1u: %d\n",
three == wide, three === wide, (Var) -1 < 1u);
printf("NaN == NaN: %d NaN === NaN: %d\n", nan == nan, nan === nan);
3 == 3L: 1 3 === 3L: 0 -1 < 1u: 0
NaN == NaN: 0 NaN === NaN: 1
Map keys and sorting keep the numeric types apart; see
Collections. See
the language reference for the dispatch.
Handling dynamic-operation failures
Var.convert, Var.binary, Var.truth, Var.update, and Var.postfix
raise cause-specific Errors. Their default policies fail fast, and
compiler-inserted conversions raise the same causes. Install a filtered
catch where your code can recover from one of them.
Var quotient = void;
try quotient = Var.binary(10, </>, 0);
catch %(div-zero *):
printf("no quotient: division by zero\n");
no quotient: division by zero
Var narrowed = Var.convert(300, <u8>);
int value = narrowed;
printf("%s holds %d\n", narrowed.tag().str(), value);
u8 holds 44
Integer-to-integer conversion keeps the destination width’s low bits and
does not detour through floating point, so 300 narrowed to <u8> is 44.
Float-to-integer conversion truncates toward zero, and raises
<conv-range> for NaN, the infinities, and anything not representable.
Other dynamic-operation causes include <div-zero>,
<bad-shift>, <no-convert>, <bad-op>, <bad-types>, and <void-op>;
the full list and their exact meanings are in
the language reference.
Compound assignment
Numeric compound assignment works when either side involves a Var. The
compiler takes the address of the lvalue once, and the runtime loads,
computes, converts to the target’s declared family, and stores only after
every step has succeeded. A failed operation or conversion leaves the target
unchanged.
Var total = 1;
total += 5;
int native = 7;
Var delta = 10;
native += delta;
int shown = total;
printf("total=%d native=%d\n", shown, native);
total=6 native=17
Both directions work: a Var lvalue with any numeric operand, and a native
scalar lvalue updated by a Var. C indexing and member access are fine,
since those are addressable lvalues. Enum and bitfield targets are excluded.
Array and Map indices support compound assignment too. The compiler uses
the collection’s update operation and evaluates the receiver, index, and
right operand once:
Array counts = [1, 2, 3];
counts[0] += 1;
Var shown = counts[0];
printf("%s\n", shown);
2
Numeric map[key] += value also initializes a missing key from value;
other indexed updates require an existing destination. List and String
indices cannot be updated. The language reference
describes the supported operations and conversion rules.
When to use Var
Use Var when the dynamic type is part of the problem. Use a static type
otherwise.
Use Var when:
- the value is heterogeneous: a
Listelement, aMapkey or value, orErrordetail; - the tag itself is what your code inspects or reports;
- you are crossing a boundary with no target type, such as a variadic call or a generic callback signature.
Prefer a plain C or runtime type when:
- The static type already says what the function takes.
long checksum(String)tells a reader more thanVar checksum(Var), and the compiler checks it. AVarmoves those type errors from translation time to run time. - The code is on a hot path. A dynamic operation calls into the runtime,
which validates the encoding and decodes both tags. The native operation
is one machine instruction. Boxing a
longor along doublealso allocates a box in the active scope. - A call already takes a
Varparameter. Pass yourString,Symbol, collection, or scalar straight in, and the compiler converts it. A localVardeclared only to pass the value adds a name and hides the type.
Written out, the two cases look like this:
/* Var fits here: the tag is the answer. */
String describe(Var value) {
return value.tag();
}
/* Var would only erase a contract the caller already knows. */
int clamp(int value, int low, int high) {
return value < low ? low : value > high ? high : value;
}
Where to go next
- Symbols and atoms for the
<name>values used above as tags and outcome codes. - Strings, lists, arrays, and maps for the containers that
store
Var. - Iteration for
Iter,try_next, and the exhaustion rule thatvoidparticipates in. - Scopes and lifetime for what owns a boxed wide value.
- Idioms for practical usage, and the language reference for the full rules.
The generated Var, conversion, and operation references list every callable by API tier.