Skip to content

Tag system

IS4 edited this page Oct 6, 2021 · 9 revisions

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.

Universal tags

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)");

Derived tags

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.

Tag operations

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)

Creating new tags

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.

Example tag operations

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.

Controlling ownership and encapsulation

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.

Clone this wiki locally