(⬑Table of Contents)
typeinfo()
: Value Meta-info Queries
typeinfo()
It is often useful to be able to ask questions about the type of a
value, e.g. to figure out if it's safe to call it like a function or
see if it's a string or a number. This capability is provided by the
typeinfo
keyword, which is capable of querying all sorts of
information about types and some non-type-related metadata (typeinfo
is a slight misnomer - it perhaps ought to have been called valinfo
or valueinfo
).
It is used like this:
const answer = typeinfo(TAG EXPR);
Where TAG
is a pseudo-keyword (listed below) used only in the context
of typeinfo
(they are normal identifiers in all other contexts). The
operand is normally an arbitrary expression, but some tags expect a
specific type of token, e.g. an identifier, or no operand at all. Each
tag tells typeinfo
which tidbit of metadata to resolve to regarding
the value being queried, and most resolve to a boolean. While this is
splitting hairs, note that the query is applied to the result
of its
expression operand, not to the expression itself. i.e. its operand does
get evaluated (except when short-circuiting is in effect).
typeinfo
is a function-like keyword, but does not start a new
scope in its argument list like a function call does. It is most often
used for function parameter validation and/or dispatching. For
example, basic argument validation might look like:
function(a,b){
affirm typeinfo(isstring a);
affirm typeinfo(isnumeric b);
…
};
Tip: such uses should prefer
affirm
overassert
, so that failure throws an exception instead of a fatal error.Design note: this keyword originally did not use parenthesis, but experimentation showed that that could lead to some potentially ambiguous interpretations, e.g.
typeinfo xyz def || abc
would (for consistency with other keywords) need to parse the||
as part oftypeinfo
's RHS, but most real uses would probably intend(typeinfo xyz def) || abc
. The decision to leave out a comma between the tag and expression was made after experimentation showed the comma to just add noise. The parenthesis, while adding noise, also serve to disambiguate the operand from any other RHS part.
typeinfo()
Tags
The current tags are listed in full below but first a description of
their naming conventions is in order. The tags are all lower-case.
Most start with "is" or "has", and both of those have similar meanings
best explained with an example: Consider the isarray and hasarray
tags: an Array value (created via an array literal or the C-level
cwal_new_array()
) "is" an array, whereas a non-array value which
includes an Array in its prototype chain "has" an array (and can be
treated like one for many purposes). In s2, the "integer property is an
array index" behaviour of property lookup applies to both types of
arrays: "real" ones and values which inherit an array (in which case the
inherited array is the one the index operations are applied to!). The
relation between "is" and "has" is subtly different from the inherits
operator, which checks whether a value contains a certain exact value
within its prototype chain, whereas these tests (mostly) check for
type-level attributes, not attributes of concrete instances. e.g. what
makes an array an Array is really the fact that it has the C-level
type ID CWAL_TYPE_ARRAY
. Whether or not it behaves like an array in
s2 depends largely on its prototype, where most script-visible behavior
is implemented (modulo the array-append operator and
integer-property-key case, which are handled in one of s2's lowest
abstraction-layer levels and use the same logic the "hasarray" tag
uses).
On 20200111, typeinfo
tag names were extended to offer hyphenated
forms to take advantage of a new feature which allows identifiers, in
limited contexts, to contain hyphens. These are functionally identical
to their older variants but may be more readable, especially for the
longer tags. e.g. typeinfo(cannew)
and typeinfo(can-new)
are
equivalent. The list below shows both forms because the older form
is prevalent in client-side scripts and is not going anywhere anytime
soon (it's not deprecated).
The tags and what they resolve to are listed here in alphabetical order below. All of them expect an expression after the tag unless noted otherwise (some require an identifier):
cannew
can-new
true
if the operand is legal for use as an operand for the new
keyword, else false
.
hasarray
has-array
true
if the operand is an array or has an array in its prototype chain.
hasbuffer
has-buffer
true
if the operand is a buffer or has a buffer in its prototype chain.
hasenum
has-enum
true
if the operand is an enum or has an enum in its prototype chain.
hasexception
has-exception
true
if the operand is an exception or has an exception in its prototype chain.
hashash
has-hash
true
if the operand is a hash or has a hash in its prototype chain.
Achtung: though enums may internally be hashes,
this predicate and is-hash
evaluate to false
for enums because
enums are not treated as hashes in all contexts.
hasnative
has-native
true
if the operand is a native or has a native in its prototype chain.
hasobject
has-object
true
if the operand is an object or has an Object in its prototype chain.
hasprototype
has-prototype
true
if the operand has a prototype. Booleans, null
, and undefined
have no prototypes, nor do the top-most prototypes in any chain, and
prototypes may be (and often are) removed from various objects.
isarray
is-array
true
if the operand is an array, i.e. created via an array literal or
the C-level cwal_new_array()
.
isbool
is-bool
true
if the operand is a bool.
isbuffer
is-buffer
true
if the operand is a buffer.
iscallable
is-callable
true
if the operand is "callable" like a function.
i.e. if it is a function or has a function in its prototype chain.
iscontainer
is-container
true
if the operand is a container, i.e. can hold its own properties.
isdeclared IDENTIFIER
is-declared IDENTIFIER
true
if the given identifier names a declared (in-scope)
variable/const. Note that this tag accepts only a single identifier,
not an arbitrary expression.
isderefable
is-derefable
true
if the operand is legal for use with the property access
operators (X.Y
and X[Y]
). Note that numbers and strings are
derefable but are not containers - property lookups resolve through
their prototype(s).
isdouble
is-double
true
if the operand is (strictly) a double.
isenum
is-enum
true
if the operand is an enum.
isexception
is-exception
true
if the operand is an exception.
isfunction
is-function
true
if the operand is a function. (Note: there is
no hasfunction tag, but iscallable
does what that tag would do.)
ishash
is-hash
true
if the operand is a hash, i.e. created via a hash
literal, new s2.Hash()
(or equivalent), or the C-level
cwal_new_hash()
.
Achtung: though enums may internally be hashes,
this predicate and has-hash
evaluate to false
for enums because
enums are not treated as hashes in all contexts.
isinteger
is-integer
true
if the operand is (strictly) an integer.
isiterating
is-iterating
Equivalent to
typeinfo(isiteratinglist EXPR) || typeinfo(isiteratingprops EXPR)
.
isiteratinglist
is-iterating-list
true
if the operand is a data type which has a
list and is currently iterating over it. This includes arrays,
tuples, and hash tables when iterating over their hash properties
(as opposed to their object properties).
isiteratingprops
is-iterating-props
true
if the operand is a data type which can
contain object-level properties and is currently iterating over
those properties (as opposed to hashtable entries).
islist
is-list
true
if the operand is an array or tuple. It is
equivalent to typeinfo(isarray X) || typeinfo(istuple X)
. Hashes
also manage a list, but they are not considered lists for this
purpose (though they are for isiteratinglist
and
mayiteratelist
).
islocal IDENTIFIER
is-local IDENTIFIER
true
if the given identifier is the name of a var/const declared in
the current (local) scope. Be aware that if(typeinfo(islocal x))
is
not generally useful because if()
starts a new scope at its opening
parenthesis. Similarly, for(var x…; …; …) {typeinfo(islocal x)...}
is not useful because for()
loops start a new scope after the first
;
and on each iteration. Because of that, the expected usages of
this query are something like:
typeinfo(islocal foo) || (var foo = ...);
typeinfo(islocal bar) && (foo = bar);
// Emulating a longer if(...):
typeinfo(islocal bar) && scope {...};
isnative
is-native
true
if the operand is a Native. Natives are the
generic type used to bind things for which cwal has no built-in type
equivalent. e.g. instances of s2's PathFinder class
are Natives and several of the loadable modules make use of
Natives.
isnewing [EXPR = this]
is-newing [EXPR = this]
true
only if the expression's value (default is the current "this"
value) was created by the new
keyword for a
constructor call and that value's constructor is currently
running. i.e. its intended use is typeinfo(isnewing)
from inside
constructor methods intended for use with the new
keyword. If this
evaluates to false
, the constructor was not called via new
, which
means its call-time this
is likely different than a constructor
expects. Constructors can use this test to change behaviours (or throw
an error) depending on whether they are being new
'd or not.
Semantics changed on 20171206: prior to that, isnewing
did not work
in a subscope of a constructor. e.g. if(typeinfo(isnewing)){...}
would previously never trigger because the X
in if(X)
is evaluated
in a subscope.
isnumber
is-number
true
if the operand is (strictly) an integer or a double. Contrast
with...
isnumeric
is-numeric
true
if the operand is an integer, a double, a boolean, or a
numeric-format string (one parseable by
parseNumber()
).
isobject
is-object
true
if the operand is an Object, i.e. created via an object literal
or the C-level cwal_new_object()
.
isstring
is-string
true
if the operand is a string.
istuple
is-tuple
true
if the operand is a Tuple value.
isunique
is-unique
true
if operand is a Unique-type value. In s2, currently the only
way to create these is via enums (their entries are are of this type)
or from C code (using cwal_new_unique()
).
name
Evaluates to the type's name, just like the deprecated typename keyword (which this tag replaces).
If an object has or inherits a property named __typename
,
that property's value is used by this query.
This is one of the few contexts where referencing an unknown
identifier will not cause an error - it results in the string
"undefined"
instead. So "undefined"===typeinfo(name foo)
can be
used to see if foo
is declared, but it will also be "undefined"
if
foo
has the undefined
value. Type name fetching cannot tell the
difference between is-not-defined vs. is-undefined, but
isdeclared
and islocal
can.
mayiterate
may-iterate
Equivalent to typeinfo(mayiterateprops EXPR) || typeinfo(mayiteratelist EXPR)
except that the EXPR
is only evaluated once.
mayiteratelist
may-iterate-list
true
if its operand is a value type which manages a list and that
list is currently capable of being iterated over, else false
. This
includes arrays, tuples, and hashes (which are a list, of sorts). Note
that the length-0 tuple, a shared constant, is reported as being
iterable but it has no entries, so
iteration on it (with foreach
) is a no-op.
Currently (201912) the only operation which blocks list iteration is Array.sort().
mayiterateprops
may-iterate-props
true
if its operand is a value type which is capable of holding
object-level properties and iteration over those properties is
currently legal, else false
.
The semantics of when it is legal to iterate changed (for the better)
on 20191211, in that it is now possible to iterate concurrently
multiple times over the same value's properties, so long as a specific
operation is not blocking that. As of 20191212, there are no such
blocking operations, but any operations which would modify a
container's property list during iteration are (still) illegal (the
data model does not support modification during iteration). Part of
that change was the requirement for different typeinfo
tags to check
for property-vs-list iteration capabilities/status. Previously there
was an internal ambiguity which caused the underlying flag for this
query to apply to both object properties and list operations.
refcount
Has (as of 20191230) been deprecated in favor of
pragma(refcount)
.
This test script demonstrates most of the above, and this one demonstrates cannew and issnewing.
Potential TODO tags include:
isconst IDENT | prop access
: true if var/property is const. The problem with properties is that this would currently require two lookups for property access, due to how the EXPR is processed.
Reminder to self: i think that can be optimized by addings2_engine::dotOpKvp
, pointing to the KVP the lookup was for, BUT that would have to be done ins2_get_v()
, which is not specific to the dot operator. Hmm. Maybe adds2_engine::getKVP
, holding only the most recent KVP (from objects, hashes, or scopes). Fixing that could potentially also allow thename
query tag to distinguish between undeclared props and those the undefined value. Holding that pointer is inherently dangerous, though, as any change to its container may invalidate it. (Oh, we just need ans2_get()
variant which returns the KVP, and the underlying cwal-level APIs already support that for all our use cases. Maybe someday we'll experiment with that.)