cwal

whcl: Working with Variables
Login

whcl: Working with Variables

(⬑Table of Contents) (builtins)

Doc sections:

Declaring Variables

Variables in WHCL (1) always have to be declared before use and (2) are always local to the scope they are declared in. They can be resolved by any newer scope which is pushed while a declaration is active.

Variables are created using the decl builtin command:

decl X   ; # creates X with the undefined value
decl Y 1 ; # creates Y with a value of 1

Variables can be made "const," meaning that they be neither replaced nor unset:

decl -const X 1 ; # const vars require a value
decl -const Y object {a 1 b 2} ; # Y's _contents_ may be modified
incr X ; # will fail because X is const

If the value part of a declation is itself a builtin command, decl calls it an forwards all remaining arguments on the line to it. That permits:

decl o [object a b c d]

To be written more succintly as:

decl o object a b c d

Sidebar: see this section for why decl can treat builtins specially in this regard but cannot do the same for non-builtin commands.

When declaring multiple values, this alternate syntax may be simpler:

decl {
  X 1
  Y 2
}

Noting that that syntax does not support commands (builtin or otherwise) as values unless they're wrapped in [...].

The "simple" form of decl evaluates to the value it sets, so:

assert 1 == [decl x 1]

The {...} syntax always evaluates to undefined.

Dereferencing Variables

To "dereference" a variable means to extract its referenced value. Variables, in and of themselves, cannot be passed around in the scripting environment. Instead, we can pass around their names (in the form of identifiers) or the values they refer to:

decl a 123
echo a  ; # an identifier, which is normally treated like an unquoted string
echo $a ; # the value referred to by a, i.e. 123

To access object properties we use the more-or-less conventional square brackets:

decl o object {a 1 b {c 2}}
echo $o      ; # the value of (a whole object)
echo $o[a]   ; # the value 1 (o's "a" property)
echo $o[b][c]; # the value 2 ("b" property of the "c" property)

When these docs refer to "dereferencing" a variable, they simply mean to access its underlying value.

Modifying Variables

The most straightforward way to modify variables is the set builtin:

decl X
set X 1
set Y 2 ; # will fail because Y has not been decl'd.

set shares decl's ability to have a builtin command as its second argument:

decl A
set A proc {} {...}

set evaluates to the value it sets, so:

decl x
assert 1 === [set x 1]

set also shares decl's ability to set object properties:

set $anObject[propertyName] value

And it supports setting such properties as "const", meaning that further calls to set them will fail:

set -const $anObject[propertyName] value

The -const flag is only permitted when setting object properties, not when setting scope-level variables. It will fail if the property is already const.

One particularly common change made to variables is incrementing or decrementing them, for which there are shortcuts:

decl x 1
assert 2 == [incr x]
assert 5 == [incr x 3]
assert 4 == [decr x]
assert 2 == [decr x 2]
assert 2 == $x

Both incr and decr default to modifying the variable named by their first argument by 1. The optional second argument may be an integer value.

Note that the second argument to incr and decr may be a negative value, thus incr x -1 and decr x 1 behave identically.

Destroying Variables

It's not normally necessary, but it is possible to destroy a non-const variable from the scope that variable lives in. WHCL does not permit script code to unset variables from older scopes (though C code can).

decl x 1
assert [info is-local x]
unset x
assert ![info is-local x]

unset accepts any number of local variable names or properties of arbitrary objects or arrays:

decl o object {a 1 b 2}
decl a array (1 2 3)
assert 1 == $o[a]
unset $o[a]
assert undefined == $o[a]
assert 2 == $a[1]
unset $a[1]; # clears the entry but does NOT change the array's size
assert undefined == $a[1]
unset $o[a]; # no such member - silently ignored
unset o a
assert ![info is-local a]
assert ![info is-local o]
unset a; # will fail because `a` no longer exists

unset evaluates to the undefined value.

Variable and Property Aliases (proprefs)

ACHTUNG: as of 2022-04-03, this support is brand new and it's not clear whether it will have any undesired side effects which we can neither live with nor mitigate. i.e. it's possible that this support will be removed. Very hopefully not. So far so good. Even too good, which makes me sceptical.

The alias builtin command creates values which, when fetched, actually fetch the value of a different variable or object property. Likewise, when modified, they update the "remote" copy. These are colloquially known as property refs, a.k.a. proprefs.

alias has the following uses:

It accepts the following optional flags:

It accepts an optional 2nd argument of a function. Passing that in changes the semantics somewhat:

Exceptions triggered by such functions are, of course, propagated normally.

In the context of a property interceptor function call, this is the original container of the property access (the one the alias refers to). For scope-level vars, that this will be the object which stores all vars for that scope. (Sidebar: scope-level vars are implemented as an Object for which property lookup semantics are slightly different than most objects.)

Once a propref is in place, it cannot be directly detected as a reference from script code (and seeing the difference requires using special-case APIs in C code). The scripting engine's bits which do the translation between references and what they point to happens very close to the deepest levels of the cwal API, outside of whcl's sight.

alias is intended to be used in just about any place where a value is acceptable. For complete examples see the alias unit test script.

The intent is that the distinction between alias and "real properties" is seamless in script code, but certain C-level APIs necessarily treat proprefs as plain opaque values instead of as redirections. Such APIs/operations include, but is not strictly limited to:

Proprefs have the ability to enable a function to refer back to the original reference of a variable:

decl o object x 0
decl x 1
proc f {} {
    set y 2; # y is a call-local var aliasing x
    set z 1; # z is a call-local var aliasing o.x
} using {
    /* The magic happens here... */
    y [alias x]
    z [alias o.x]
}
f
assert 2 == $x
assert 1 == o.x

That capability should be used carefully, though, because referencing a variable or property that way will cause the whole property container to remain alive as long as the reference (which just really points to one variable or property) remains alive. That is, if a function like the above one lived in a scope which had 10 values, creating a propref to x would indirectly hold references to the 9 other variables in that scope. Those "extra" variables would hang around in their parent container until (at least) the propref is cleaned up. Such constructs are still subject to being vacuum'd up, but are cyclic, so are inherently immune to sweeping.

In short: functions which are local to a given small scope, and will not propagate out of it, have no down-sides with regards to the above, but a function which propagates out of a scope normally won't want to take a reference to the whole containing scope with it (keeping in mind that its own variable key/value pair is part of that very scope).