Description
This is a KSC 0.11-SNAPSHOT regression - this worked in 0.10, but is broken in 91aeb53e
(the latest commit at master
at the time of writing).
Related to #69
I noticed this first in common/vlq_base128_le.ksy
, more specifically on the value_signed
value instance. It is defined like this:
common/vlq_base128_le.ksy:61-62
value_signed:
value: '((value ^ sign_bit).as<s8> - sign_bit.as<s8>).as<s8>'
However, KSC at commit 91aeb53e
(i.e. the latest KSC 0.11-SNAPSHOT at the time of writing) translates it as follows:
Object.defineProperty(VlqBase128Le.prototype, 'valueSigned', {
get: function() {
if (this._m_valueSigned !== undefined)
return this._m_valueSigned;
this._m_valueSigned = this.value ^ this.signBit - this.signBit;
return this._m_valueSigned;
}
});
This is wrong and leads to incorrect results. In JavaScript, the bitwise XOR operator (^
) has a lower precedence than the subtraction operator (-
), so it will be evaluated like this:
value_signed:
value: 'value ^ (sign_bit - sign_bit)'
Which is obviously very different: since (sign_bit - sign_bit)
is always 0
, it's effectively value ^ 0
, so value_signed
will always be equal to the value
attribute (which represents the unsigned value) without performing the intended sign extension.
This incorrect omission of essential parentheses is apparently caused by adding the type cast. Since kaitai-io/kaitai_struct_compiler#315, I knew that KSC knows operator precedence in individual target languages very well (because I have tested it extensively), so I was very surprised to find this bug.
The following simple .ksy snippet that I tested in the devel Web IDE shows that everything works fine without type cast, but once the type cast is applied to the parentheses, they disappear:
meta:
id: cast_removes_parens
instances:
no_cast:
value: '(1 + 2) * 3'
with_cast:
value: '(1 + 2).as<s4> * 3'
Generated JavaScript code:
this._m_noCast = (1 + 2) * 3;
// ...
this._m_withCast = 1 + 2 * 3;
It doesn't just apply to JavaScript - it applies to all dynamically typed languages (where type casting is a no-op by design):
-
Lua
self._m_no_cast = (1 + 2) * 3 -- ... self._m_with_cast = 1 + 2 * 3
-
Perl
$self->{no_cast} = (1 + 2) * 3; # ... $self->{with_cast} = 1 + 2 * 3;
-
PHP
$this->_m_noCast = (1 + 2) * 3; // ... $this->_m_withCast = 1 + 2 * 3;
-
Python
self._m_no_cast = (1 + 2) * 3 # ... self._m_with_cast = 1 + 2 * 3
-
Ruby
@no_cast = (1 + 2) * 3 # ... @with_cast = 1 + 2 * 3