-
Notifications
You must be signed in to change notification settings - Fork 18
Tag system
In standard Pawn, tags are used as a mostly compile-time only way of attaching a meaning to a value or a variable. Some functions can accept values with multiple tags and differentiate between them via the tag id passes as a separate parameter (using the tagof
operator). Each AMX program has its own set of exported tags and their unique identifiers.
PawnPlus extends the tag system by assigning universal identifiers to different tags based on their name, which is required by dynamic containers in order to transport a tag identifier from one script to another. This is represented by the tag_uid
weak tag in Pawn.
To convert a local tag id to a universal tag id and vice-versa, the tag_uid
and tag_id
functions can be used, respectively.
new tag_uid:uid = tag_uid(tagof(Float:));
assert uid == tag_uid_float;
assert tag_id(uid) == (tagof(Float:));
There is a set of predefined tag uids for common Pawn and PawnPlus types for easier access to their functions, and each newly found tag is added to the list when requested.
tag_uid
values are portable across different scripts, but when the specific tag is not defined in a script where tag_id
is used, 0 will be returned.
All PawnPlus functions which accept a tag id also accept a tag uid. Since a standard public tag id has always its highest bit set, values less than 0x80000000 will be interpreted as tag uids.
new Variant:v = var_new(0x3F800000, .tag_id=tag_uid_float); //equivalent to tagof(Float:)
assert str_val(v) == @("(1.000000)");
PawnPlus offers mechanisms for specifying if a tag is "more specific" than another tag, which is required for obtaining dynamically tagged values from containers in a safe way. A common example is a conversion from GlobalString
to String
:
new GlobalString:str = @("new str");
new Variant:v = var_new(str);
assert var_tagof(v) == (tagof(GlobalString:));
new String:str2;
assert var_get_safe(v, str2);
Even though the variant contains a GlobalString
value, it can be converted to String
, because GlobalString
is a "subtag" of String
(logically, every GlobalString
is also a String
). The underlying mechanism is simple – GlobalString
is actually defined as String@Global
. The @
character is a valid part of a symbol's name and can be used in a tag. PawnPlus treats any tag with @
inside as a subtag of another tag with a name before the @
. For example, A@B@C@D
is a subtag of A@B@C
which is a subtag of A@B
, a subtag of A
. This can be checked with tag_base
.
In Pawn, tags can be defined with certain operations on them, for example adding Float
numbers results in a call to floatadd
. However, this information must be also available at runtime for the variant system which offers executing operations on variants storing values of any tag. The plugin must know what this line means and what result it should give:
var_new(1.0) + var_new(2.0)
To ensure a correct operation is called, each tag has an associated set of operations (corresponding to the overloadable Pawn operators and some basic operations recognized by PawnPlus) which can be dynamically invoked via a corresponding variant operation or other functions, like str_val
or tag_call_op
. The set of predefined tags cannot have their operation changed, but you can change the operations of any other tag (even derived ones).
You can set up a public function that will receive the operands:
forward MyTag_Add(arg1, arg2);
public MyTag_Add(arg1, arg2)
{
return arg1 * arg2;
}
forward String:MyTag_ToString(arg);
public String:MyTag_ToString(arg)
{
return str_format("[%d]", arg);
}
main()
{
new MyTag:a = MyTag:2, MyTag:b = MyTag:3;
new tag_uid:tag = tag_uid((tagof(a)));
tag_set_op(tag, tag_op_add, "MyTag_Add");
tag_set_op(tag, tag_op_string, "MyTag_ToString");
tag_lock(tag);
new MyTag:c = MyTag:tag_call_op(tag, tag_op_add, _:a, _:b);
print_s(str_val(c)); // [6]
}
The public functions will be called for every operation defined on MyTag
, regardless of the calling script. Arithmetic binary and unary operations return values as expected, logical operations return bool, tag_op_string
must return a dynamic string, tag_op_hash
returns a hash of the value, and other operations (tag_op_delete
, tag_op_copy
) return either bool specifying success or a new value.
Operation | Description | Operands | Returns | Example (Variant:a, Variant:b ) |
---|---|---|---|---|
add |
Addition | 2 | tag | a + b |
sub |
Subtraction | 2 | tag | a - b |
mul |
Multiplication | 2 | tag | a * b |
div |
Division | 2 | tag | a / b |
mod |
Modulo | 2 | tag | a % b |
neg |
Unary minus | 1 | tag | -a |
inc |
Increment | 1 | tag | a++ |
dec |
Decrement | 1 | tag | a-- |
eq |
Equality | 2 | bool |
a == b |
neq |
Inequality | 2 | bool |
a != b |
lt |
Less than | 2 | bool |
a < b |
gt |
Greater than | 2 | bool |
a > b |
lte |
Less than or equal | 2 | bool |
a <= b |
gte |
Greater than or equal | 2 | bool |
a >= b |
not |
Logical negation | 1 | bool |
!a |
string |
String value | 1 | String |
str_val(a) |
format |
Formatted value | 3 | String |
str_val(a, .format="d03")) |
delete |
Deletion | 1 | var_delete(a) |
|
release |
Recursive deletion for containers, reference count decrement for GC-objects | 1 | var_release(a) |
|
collect |
GC collection | 1 | pp_collect() |
|
copy |
Copy | 1 | tag | var_clone(a) |
clone |
Deep copy | 1 | tag | var_clone(a) |
assign |
Copy assignment | 1 | tag | var_get(a) |
init |
Copy construction | 1 | tag | var_new(a) |
hash |
Hash value | 1 | int | map_var_add(m, a, 0) |
acquire |
Reference count increment for GC-objects | 1 | var_acquire(a) |
|
handle |
Obtain a lifetime handle | 1 | Handle |
handle_new(a) |
The PawnPlus tag system can be used as a dynamic replacement of a type system, but so far, the created tags were always coming from the current script. However, it is possible to create a brand new tag, not bound to any script, just by specifying its name, using the tag_new
function.
The function ensures that a new tag is created every time, instead of reusing a previous one, and can even specify the base tag. The actual name is generated
new tag_uid:tag = tag_new("Complex", tag_uid_list);
print_s(tag_name_s(tag)); // List@Complex+95174200
Since variant operations are performed on cells and not on arrays, a list (or another variant) must be used as the base of our new Complex
type. Deriving from List
already ensures that the delete_deep
operation destroys the list stored inside any structure, so we don't need to define it again:
new num_lists = pp_num_lists();
new Variant:v = var_new(list_new_args(1.0, 1.0), tag);
var_delete_deep(v);
assert num_lists == pp_num_lists();
The tag name cannot be used in any script (because it's always unique), so all operations must be performed either via a Variant
, or by using tag_call_op
.
See how a basic complex number type could work as a dynamically created tag:
forward List:Complex_Add(List:arg1, List:arg2);
public List:Complex_Add(List:arg1, List:arg2)
{
new Float:a = Float:list_get(arg1, 0), Float:b = Float:list_get(arg1, 1), Float:c = Float:list_get(arg2, 0), Float:d = Float:list_get(arg2, 1);
return list_new_args(a+c, b+d);
}
forward List:Complex_Sub(List:arg1, List:arg2);
public List:Complex_Sub(List:arg1, List:arg2)
{
new Float:a = Float:list_get(arg1, 0), Float:b = Float:list_get(arg1, 1), Float:c = Float:list_get(arg2, 0), Float:d = Float:list_get(arg2, 1);
return list_new_args(a-c, b-d);
}
forward List:Complex_Mul(List:arg1, List:arg2);
public List:Complex_Mul(List:arg1, List:arg2)
{
new Float:a = Float:list_get(arg1, 0), Float:b = Float:list_get(arg1, 1), Float:c = Float:list_get(arg2, 0), Float:d = Float:list_get(arg2, 1);
return list_new_args(a*c-b*d, a*d+b*c);
}
forward List:Complex_Div(List:arg1, List:arg2);
public List:Complex_Div(List:arg1, List:arg2)
{
new Float:a = Float:list_get(arg1, 0), Float:b = Float:list_get(arg1, 1), Float:c = Float:list_get(arg2, 0), Float:d = Float:list_get(arg2, 1);
new Float:e = c*c+d*d;
return list_new_args((a*c+b*d)/e, (b*c-a*d)/e);
}
forward List:Complex_Neg(List:arg);
public List:Complex_Neg(List:arg)
{
new Float:a = Float:list_get(arg, 0), Float:b = Float:list_get(arg, 1);
return list_new_args(-a, -b);
}
forward List:Complex_Copy(List:arg);
public List:Complex_Copy(List:arg)
{
new Float:a = Float:list_get(arg, 0), Float:b = Float:list_get(arg, 1);
return list_new_args(a, b);
}
forward String:Complex_String(List:arg);
public String:Complex_String(List:arg)
{
new Float:a = Float:list_get(arg, 0), Float:b = Float:list_get(arg, 1);
return str_format("%f+%fi", a, b);
}
forward bool:Complex_Collect(List:arg);
public bool:Complex_Collect(List:arg)
{
return list_delete(arg);
}
new tag_uid:complex_tag;
main()
{
complex_tag = tag_new("Complex", tag_uid_list);
tag_set_op(complex_tag, tag_op_add, #Complex_Add);
tag_set_op(complex_tag, tag_op_sub, #Complex_Sub);
tag_set_op(complex_tag, tag_op_mul, #Complex_Mul);
tag_set_op(complex_tag, tag_op_div, #Complex_Div);
tag_set_op(complex_tag, tag_op_neg, #Complex_Neg);
tag_set_op(complex_tag, tag_op_copy, #Complex_Copy);
tag_set_op(complex_tag, tag_op_string, #Complex_String);
tag_set_op(complex_tag, tag_op_collect, #Complex_Collect);
tag_lock(complex_tag);
}
A variant containing a value tagged with complex_tag
will correctly process the standard arithmetic operators, conversion to string, copying (triggered by var_clone
for example) and garbage collection.
The collect
operation is a bit special when compared to others. It cannot be triggered directly (via tag_op_call
) and is called last before any variant is deleted (even in lists and maps), even automatically as part of the usual garbage collection. Defining it on a tag indicates that any dynamic container that stores it completely owns the object. In the case of our complex type, the list stored inside the variant has its lifetime bound to it and will be deleted:
new List:l = list_new_args(1.0, 1.0);
new Variant:v = var_new(l, tag); // v assumes ownership of l
var_delete(v);
assert !list_valid(l);
In some cases, this might not be the desirable behavior. To achieve both absolute ownership and isolation, one can use the assign
and init
operations to force copying the value whenever it is assigned to another location, or when it is stored in new variant.
tag_set_op(tag, tag_op_assign, "Complex_Copy");
tag_set_op(tag, tag_op_init, "Complex_Copy");
new List:l = list_new_args(1.0, 1.0);
new Variant:v = var_new(l, tag); // l is copied to v (via tag_op_init)
new num_lists = pp_num_lists();
var_delete(v); // the copy of l is deleted
assert pp_num_lists() == num_lists - 1 && list_valid(l);
When the value is used as the input, tag_op_init
is called. tag_op_assign
is called when the value is extracted via corresponding functions, or when the variant itself is copied. As a consequence, only the operations defined on the tag will be able to access the real value stored inside.