Skip to content

Commit

Permalink
Fix compile-time bitwise ops (#215)
Browse files Browse the repository at this point in the history
* remove support for the ">>>" operator

This operator is the unsigned right shift operator,
which is not supported in mlog

* use bigints to perform constant bitwise operations

this is done to prevent data losses caused by javascript using 32-bit
integers to perform bitwise operations while
mlog uses 64-bit integers for that

* use number values for all operators except "+"

This makes the constant expression evaluation more consistent with the
mlog environment, since the input values there are always
converted to numbers first.
However, constant string concatenation via the + operator is still
allowed in order to not break existing mlogjs scripts

* add a test for constant bitwise operations

* update the changelog
  • Loading branch information
JeanJPNM committed Nov 25, 2023
1 parent e10534d commit d8cfbe7
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 14 deletions.
1 change: 1 addition & 0 deletions compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Fixed compile time operation evaluation not working properly on values bigger than 2^31-1.
- Fixed the online editor parsing declaration files multiple times.
- Fixed missing field declarations on units and buildings.
- Fixed calling `toString` in a literal value not having the correct error message.
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,14 @@ export const operatorMap = {
"&": "and",
"^": "xor",
">>": "shr",
">>>": "shr",
"<<": "shl",
"&&": "land",
"||": "or",
} as const satisfies Record<
Exclude<BinaryOperator | LogicalOperator, "instanceof" | "in" | "!==" | "??">,
Exclude<
BinaryOperator | LogicalOperator,
"instanceof" | "in" | "!==" | "??" | ">>>"
>,
string
>;

Expand Down
49 changes: 37 additions & 12 deletions compiler/src/values/LiteralValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class LiteralValue<T extends TLiteral | null = TLiteral>
return false;
}

get num() {
get num(): number {
if (this.data === null) return 0;
if (typeof this.data === "string") return 1;
return this.data;
Expand All @@ -95,7 +95,7 @@ export class LiteralValue<T extends TLiteral | null = TLiteral>
}
}

type TOperationFn = (a: number, b?: number) => number;
type TOperationFn = (a: number) => number;
type TBinOperationFn = (a: number, b: number) => number;

const operatorMap = {
Expand All @@ -113,16 +113,15 @@ const operatorMap = {
"/": (a, b) => a / b,
"%": (a, b) => a % b,
"**": (a, b) => a ** b,
"|": (a, b) => a | b,
"&": (a, b) => a & b,
"^": (a, b) => a ^ b,
">>": (a, b) => a >> b,
">>>": (a, b) => a >> b,
"<<": (a, b) => a << b,
"|": bitwiseOp((a, b) => a | b),
"&": bitwiseOp((a, b) => a & b),
"^": bitwiseOp((a, b) => a ^ b),
">>": bitwiseOp((a, b) => a >> b),
"<<": bitwiseOp((a, b) => a << b),
"&&": (a, b) => +(a && b),
"||": (a, b) => +(a || b),
} as const satisfies Record<
Exclude<BinaryOperator | LogicalOperator, "instanceof" | "in" | "??">,
Exclude<BinaryOperator | LogicalOperator, "instanceof" | "in" | "??" | ">>>">,
TBinOperationFn
>;

Expand All @@ -149,15 +148,21 @@ for (const k in operatorMap) {
return BaseValue.prototype[key].apply(this, [scope, value, out]);
}

return [new LiteralValue(fn(this.data as never, value.data as never)), []];
// patch constant string concatenation
// TODO: remove this in favor of the `concat` function
if (key === "+" && this.isString()) {
return [new LiteralValue(this.data + value.data), []];
}

return [new LiteralValue(fn(this.num, value.num)), []];
};
}

const unaryOperatorMap: {
[k in Exclude<UnaryOperator, "delete" | "typeof" | "void">]: TOperationFn;
} = {
"!": v => +!v,
"~": v => ~v,
"~": bitwiseOp(v => ~v),
"u-": v => -v,
"u+": v => +v,
} as const;
Expand All @@ -168,6 +173,26 @@ for (const key in unaryOperatorMap) {
this: LiteralValue,
): TValueInstructions {
const fn = unaryOperatorMap[key as K];
return [new LiteralValue(fn(this.data as never)), []];
return [new LiteralValue(fn(this.num)), []];
};
}

/**
* Performs bitwise operations on 64-bit integers to ensure that the operations
* evaluated at compile time produce the same results as the mlog runtime.
*
* This is necessary because javascript converts its 64-bit floats into 32-bit
* integers to perform bitwise operations, however mlog casts 64-bit floats into
* 64-bit integers to achieve the same goal. This means that using javascript
* numbers to evaluate these operations at compile time can cause disparity
* between the compiler and the runtime for values bigger than `2^31-1`.
*/
function bitwiseOp(fn: (...args: bigint[]) => bigint) {
return (...args: number[]) => {
const bigResult = fn(...args.map(BigInt));

// limit the result to 64 bits of precision (signed long)
// and convert it back into a number
return Number(BigInt.asIntN(64, bigResult));
};
}
12 changes: 12 additions & 0 deletions compiler/test/in/constant_bitwise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// makes sure that the compiler evaluates constant bitwise
// operations using 64-bit integers to have parity with
// the mlog runtime

// integer outside the limits of 32-bit integers
const value = 2 ** 33;

print(value >> 30, "\n"); // expected: 8
print(value | 0, "\n"); // should not lose precision
print(value ^ (2 ** 32), "\n");
print(value << 1, "\n");
printFlush();
9 changes: 9 additions & 0 deletions compiler/test/out/constant_bitwise.mlog
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
print 8
print "\n"
print 8589934592
print "\n"
print 12884901888
print "\n"
print 17179869184
print "\n"
printflush message1

0 comments on commit d8cfbe7

Please sign in to comment.