- Joined
- Nov 7, 2014
- Messages
- 571
Poor vJass' value types
As you might know, vJass implements its user defined data types (called structs) as integer indices into parallel arrays, i.e the storage for each struct field is its own global array.
So if we have this example struct:
JassHelper (vJass's compiler/preprocessor) would generate the following code for it (more or less):
If we then make another Point and we assign it to p1, and change some of its fields, we are going to modify p1's fields:
In a sense, vJass's struct instances act as references to objects, i.e the assignment of 1 Point to another doesn't do a deep copy, only a shallow copy (an integer copy, really).
We could of course write our own copy Point function/method that creates a new Point from an existing one but now we face the problem that Jass' arrays only grow up to 8192, so we can only have 8192 Points at time (8191 really), which is plenty, but unlike some other programming languages, there is no automatic memory management in [v]Jass, i.e when we are done using our Point(s) we have to free them manually (vJass provides both a create and a destroy method), otherwise we could easily reach that 8191 array size limit (creating points in a loop, for example).
Wouldn't it be nice if vJass had struct-like thingies (user defined data types) that didn't need to be manually destroyed and the assignment of one such thingy to another did a deep copy (aka value types)? Well... yes, but no, there ain't such thingies in vJass unfortunately.
The only way, that I know, of emulating the semantics of value types in vJass is to pack their data into a single integer (which itself has value semantics). The problem with that is that there are only 32 bits in an integer (integers in Jass are signed 32 bit integers) , which is very limiting of course.
We can't repesent the above Point struct with only 32 bits (a float is 32 bits, and the Point struct has 2 of them).
The funny thing is that we can store 2 vJass Point references/instances in a single value. Because references are integers that go up to 8192, we need
We are going to place the bits of the 2 references like so:
The way bit packing is done (as far as I know) in non-scripting languages is with some bitwise operators/functions:
We can emulate (<<) with multiplication (*), (>>) with division (/) and (|) with addition (+).
We want the division (/) to act as logical/unsigned integer right shift, but because the integers are signed, it acts as arithmetic right shift, we could work around that but it would make things slower, so we always keep the sign bit unset instead.
Note that (+) emulates (|) only if the bits of its arguments are non-overlapping.
In vJass this might look like:
With this we can create pp(s), store them in variables/arrays, pass them to functions, etc. But we would also like to access the actual Point fields that we've packed. We could do it like so:
The custom operator/property feature of vJass makes it look like an actual field access, we didn't have to call a method, like p.get_a(), for example.
The way these work is:
We also need to be able to modify these fields:
Can you see the slight problem? The 'this' variable in the methods is not an alias for to the 'p' variable, these are two separate variables holding the same value, and we can't pass a reference/pointer to the p variable to be modified by a function in Jass. We can however return a new value and reassign p to it:
We have to use an actual method for the setter of the fields though, we prefix them with 'set_', not to confuse vJass.
Because of the limited storage of these value types (32 bits) we can't pack in them arbitrary data, we can pack a set of data that's known beforehand though.
Lets implement the value type 'sru' that can store the following sets of values:
We simply use lookup tables to map the set values to the integers from 0 to set.size - 1 and back.
Note that because the 'r' field's bits are in the middle, we have to preserve the bits on their left and right, in the 'set_r' method, which is slightly more involved, but not really.
The 'pp' and 'sru' value types are pretty useless but they hopefully got the point across.
Because of the storage limitations we can't really do much we these thingies, I suppose one could have an (error-code, reference) value returned from a function, or (reference-tag, reference) values floating around but not a vector3 unfortunately... maybe a vector2 (real x : 15, real y : 15) although 15 bit reals would probably be way too imprecise, but that's "Poor vJass' value types" for you.
As you might know, vJass implements its user defined data types (called structs) as integer indices into parallel arrays, i.e the storage for each struct field is its own global array.
So if we have this example struct:
JASS:
struct Point
real x
real y
endstruct
...
local Point p1 = Point.create()
set p1.x = 1.0
set p1.y = 2.0
JassHelper (vJass's compiler/preprocessor) would generate the following code for it (more or less):
JASS:
globals
real array s__Point_x // struct Point's fields,
real array s__Point_y // with a separate array for each field
endglobals
...
local integer p1= s__Point__allocate() // p1 is an integer
set s__Point_x[p1]=1.0 // used as an index into the
set s__Point_y[p1]=2.0 // parallel arrays
If we then make another Point and we assign it to p1, and change some of its fields, we are going to modify p1's fields:
JASS:
local Point p2
set p2 = p1
set p2.x = 3.0
call BJDebugMsg(R2S(p2.x)) // 3.0
call BJDebugMsg(R2S(p1.x)) // 3.0
// =>
local integer p2
set p2=p1
set s__Point_x[p2]=3.0
call BJDebugMsg(R2S(s__Point_x[p2])) // 3.0
call BJDebugMsg(R2S(s__Point_x[p1])) // 3.0
In a sense, vJass's struct instances act as references to objects, i.e the assignment of 1 Point to another doesn't do a deep copy, only a shallow copy (an integer copy, really).
We could of course write our own copy Point function/method that creates a new Point from an existing one but now we face the problem that Jass' arrays only grow up to 8192, so we can only have 8192 Points at time (8191 really), which is plenty, but unlike some other programming languages, there is no automatic memory management in [v]Jass, i.e when we are done using our Point(s) we have to free them manually (vJass provides both a create and a destroy method), otherwise we could easily reach that 8191 array size limit (creating points in a loop, for example).
Wouldn't it be nice if vJass had struct-like thingies (user defined data types) that didn't need to be manually destroyed and the assignment of one such thingy to another did a deep copy (aka value types)? Well... yes, but no, there ain't such thingies in vJass unfortunately.
The only way, that I know, of emulating the semantics of value types in vJass is to pack their data into a single integer (which itself has value semantics). The problem with that is that there are only 32 bits in an integer (integers in Jass are signed 32 bit integers) , which is very limiting of course.
We can't repesent the above Point struct with only 32 bits (a float is 32 bits, and the Point struct has 2 of them).
The funny thing is that we can store 2 vJass Point references/instances in a single value. Because references are integers that go up to 8192, we need
ceil(log(8192, 2)) = 13
bits to store them, and 13 can fit 2 times in 32.We are going to place the bits of the 2 references like so:
JASS:
S 000 ZAAAAAAAAAAAAA ZBBBBBBBBBBBBB
S - the sign bit of the underlying integer which we always want to keep unset (equal to 0)
000 - unused bits
Z - an always 0 bit that helps with keeping the S bit unset
AAAAAAAAAAAAA - 1st reference's 13 bits
BBBBBBBBBBBBB - 2nd reference's 13 bits
The way bit packing is done (as far as I know) in non-scripting languages is with some bitwise operators/functions:
JASS:
// in C-like languages usually there are dedicated operators for doing bit manipulations:
<< -- left shift
>> -- right shift (arithmetic, >>> for logical right shift in languages without unsigned integers?)
& -- bitwise and
| -- bitwise or
We can emulate (<<) with multiplication (*), (>>) with division (/) and (|) with addition (+).
JASS:
x << 0 | x * 0x1 | x >> 0 | x / 0x1
x << 1 | x * 0x2 | x >> 1 | x / 0x2
x << 2 | x * 0x4 | x >> 2 | x / 0x4
x << 3 | x * 0x8 | x >> 3 | x / 0x8
x << 4 | x * 0x10 | x >> 4 | x / 0x10
x << 5 | x * 0x20 | x >> 5 | x / 0x20
x << 6 | x * 0x40 | x >> 6 | x / 0x40
x << 7 | x * 0x80 | x >> 7 | x / 0x80
x << 8 | x * 0x100 | x >> 8 | x / 0x100
x << 9 | x * 0x200 | x >> 9 | x / 0x200
x << 10 | x * 0x400 | x >> 10 | x / 0x400
x << 11 | x * 0x800 | x >> 11 | x / 0x800
x << 12 | x * 0x1000 | x >> 12 | x / 0x1000
x << 13 | x * 0x2000 | x >> 13 | x / 0x2000
x << 14 | x * 0x4000 | x >> 14 | x / 0x4000
x << 15 | x * 0x8000 | x >> 15 | x / 0x8000
x << 16 | x * 0x10000 | x >> 16 | x / 0x10000
x << 17 | x * 0x20000 | x >> 17 | x / 0x20000
x << 18 | x * 0x40000 | x >> 18 | x / 0x40000
x << 19 | x * 0x80000 | x >> 19 | x / 0x80000
x << 20 | x * 0x100000 | x >> 20 | x / 0x100000
x << 21 | x * 0x200000 | x >> 21 | x / 0x200000
x << 22 | x * 0x400000 | x >> 22 | x / 0x400000
x << 23 | x * 0x800000 | x >> 23 | x / 0x800000
x << 24 | x * 0x1000000 | x >> 24 | x / 0x1000000
x << 25 | x * 0x2000000 | x >> 25 | x / 0x2000000
x << 26 | x * 0x4000000 | x >> 26 | x / 0x4000000
x << 27 | x * 0x8000000 | x >> 27 | x / 0x8000000
x << 28 | x * 0x10000000 | x >> 28 | x / 0x10000000
x << 29 | x * 0x20000000 | x >> 29 | x / 0x20000000
x << 30 | x * 0x40000000 | x >> 30 | x / 0x40000000
x << 31 | x * 0x80000000 | x >> 31 | x / 0x80000000
0b1111_0000 0b1111_0000 | 0b0110_1010 0b0110_1010
or 0b0000_1111 + 0b0000_1111 | or 0b0101_0111 + 0b0101_0111
0b1111_1111 0b1111_1111 | 0b0111_1111 0b1100_0001
We want the division (/) to act as logical/unsigned integer right shift, but because the integers are signed, it acts as arithmetic right shift, we could work around that but it would make things slower, so we always keep the sign bit unset instead.
Note that (+) emulates (|) only if the bits of its arguments are non-overlapping.
In vJass this might look like:
JASS:
// we declare the pp type that we are going to use in order to implement our value struct
// for our purposes the 'extends array' syntax in vJass means declaring a value type (its kind of goofy, I know)
struct pp extends array
static method make takes Point a, Point b returns pp
// local pp result = integer(a) << 14 | integer(b) << 0
// local pp result = integer(a)*0x4000 + integer(b)*0x1
// local pp result = a*0x4000 + b*0x1 // integer casting is optional, the << 0 (* 0x1) does nothing
// return result
return a*0x4000 + b
endmethod
endstruct
// shorthand alias for pp.make
function Pp takes Point a, Point b returns pp
return pp.make(a, b)
endfunction
With this we can create pp(s), store them in variables/arrays, pass them to functions, etc. But we would also like to access the actual Point fields that we've packed. We could do it like so:
JASS:
method operator a takes nothing returns Point
// 'this' in methods is the instance the method was called on
// return Point(this >> 14)
return Point(this / 0x4000)
endmethod
method operator b takes nothing returns Point
// return Point((this << 18) >> 18)
return Point((this * 0x40000) / 0x40000)
endmethod
...
local Point p1 = Point.create()
local Point p2 = Point.create()
local pp p = Pp(p1, p2)
set p1.x = 1.0
call BJDebugMsg(R2S( p.a.x )) // 1.0
The custom operator/property feature of vJass makes it look like an actual field access, we didn't have to call a method, like p.get_a(), for example.
The way these work is:
JASS:
remember that our value struct looked like (S = 0, Z = 0):
S 000 ZAAAAAAAAAAAAA ZBBBBBBBBBBBBB
// for operator a
after '>> 14'
0 000 00000000000000 0AAAAAAAAAAAAA
// for operator b
after '<< 18'
0 BBB BBBBBBBBBB 000000000000000000
after '>> 18'
0 000 00000000000000 0BBBBBBBBBBBBB
We also need to be able to modify these fields:
JASS:
method operator a= takes integer x returns nothing
// set this = (x << 14) | ((this << 18) >> 18)
set this = (x * 0x4000) + ((this * 0x40000) / 0x40000)
endmethod
method operator b= takes integer x returns nothing
// set this = ((this >> 14) << 14) | (x << 0)
set this = ((this / 0x4000) * 0x4000) + x
endmethod
...
local Point p1 = Point.create()
local pp p = Pp(0, 0)
set p1.x = 3.0
set p.a = p1
call BJDebugMsg(R2S( p.a.x )) // 0.0 huh?
Can you see the slight problem? The 'this' variable in the methods is not an alias for to the 'p' variable, these are two separate variables holding the same value, and we can't pass a reference/pointer to the p variable to be modified by a function in Jass. We can however return a new value and reassign p to it:
JASS:
method set_a takes integer x returns pp
return (x * 0x4000) + ((this * 0x40000) / 0x40000)
endmethod
method set_b takes integer x returns pp
return ((this / 0x4000) * 0x4000) + x
endmethod
...
local Point p1 = Point.create()
local pp p = Pp(0, 0)
set p1.x = 3.0
set p = p.set_a(p1)
call BJDebugMsg(R2S( p.a.x )) // 3.0
We have to use an actual method for the setter of the fields though, we prefix them with 'set_', not to confuse vJass.
Because of the limited storage of these value types (32 bits) we can't pack in them arbitrary data, we can pack a set of data that's known beforehand though.
Lets implement the value type 'sru' that can store the following sets of values:
JASS:
globals
constant string S0 = "A"
constant string S1 = "B"
constant string S2 = "C"
constant string S3 = "D"
string array S_a
constant real R0 = 0.0
constant real R1 = 6.28
constant real R2 = 2.718281
constant real R3 = 3.0
real array R_a
constant integer U0 = 'hfoo'
constant integer U1 = 'hpea'
constant integer U2 = 'ogru'
constant integer U3 = 'ugho'
integer array U_a
endglobals
function lookup_tables_init takes nothing returns nothing
set S_a[0] = S0
set S_a[1] = S1
set S_a[2] = S2
set S_a[3] = S3
set R_a[0] = R0
set R_a[1] = R1
set R_a[2] = R2
set R_a[3] = R3
set U_a[0] = U0
set U_a[1] = U1
set U_a[2] = U2
set U_a[3] = U3
endfunction
struct sru extends array
// string s : 4
// real r : 4
// integer u : 4
// S0000000000000000 Zssss Zrrrr Zuuuu
static method id_for_s takes string s returns integer
local integer i = 0
loop
if s == S_a[i] then
return i
endif
set i = i + 1
endloop
return 0
endmethod
static method id_for_r takes real r returns integer
local integer i = 0
loop
if r == R_a[i] then
return i
endif
set i = i + 1
endloop
return 0
endmethod
static method id_for_u takes integer u returns integer
local integer i = 0
loop
if u == U_a[i] then
return i
endif
set i = i + 1
endloop
return 0
endmethod
static method make takes string s, real r, integer u returns sru
// return id_for_s(s) << 10 | id_for_r(r) << 5 | id_for_u(u) << 0
return id_for_s(s) * 0x400 + id_for_r(r) * 0x20 + id_for_u(u)
endmethod
method operator s takes nothing returns string
// return S_a[this >> 10]
return S_a[this / 0x400]
endmethod
method operator r takes nothing returns real
// return R_a[(this << 22) >> 27]
return R_a[(this * 0x400000) / 0x8000000]
endmethod
method operator u takes nothing returns integer
// return U_a[(this << 27) >> 27]
return U_a[(this * 0x8000000) / 0x8000000]
endmethod
method set_s takes string s returns sru
// return (id_for_s(s) << 10) | ((this << 22) >> 22)
return (id_for_s(s) * 0x400) + ((this * 0x400000) / 0x400000)
endmethod
method set_r takes real r returns sru
// return ((this >> 10) << 10) | (id_for_r(r) << 5) | ((this << 27) >> 27)
return ((this * 0x400) / 0x400) + (id_for_r(r) * 0x20) + ((this * 0x8000000) / 0x8000000)
endmethod
method set_u takes integer u returns sru
// return ((this >> 5) << 5) | (id_for_u(u) << 0)
return ((this / 0x20) * 0x20) + id_for_u(u)
endmethod
endstruct
function Sru takes string s, real r, integer u returns sru
return sru.make(s, r, u)
endfunction
...
local sru p
call lookup_tables_init()
set p = Sru(S0, R1, U2)
call BJDebugMsg(p.s) // "A"
call BJDebugMsg(R2S(p.r)) // 6.280
call BJDebugMsg(I2S(p.u)) // 1869050485
call BJDebugMsg(I2S('ogru')) // 1869050485
set p = p.set_s(S1).set_r(R2)
call BJDebugMsg(p.s) // "B"
call BJDebugMsg(R2S(p.r)) // 3.000
We simply use lookup tables to map the set values to the integers from 0 to set.size - 1 and back.
Note that because the 'r' field's bits are in the middle, we have to preserve the bits on their left and right, in the 'set_r' method, which is slightly more involved, but not really.
The 'pp' and 'sru' value types are pretty useless but they hopefully got the point across.
Because of the storage limitations we can't really do much we these thingies, I suppose one could have an (error-code, reference) value returned from a function, or (reference-tag, reference) values floating around but not a vector3 unfortunately... maybe a vector2 (real x : 15, real y : 15) although 15 bit reals would probably be way too imprecise, but that's "Poor vJass' value types" for you.