-
-
Notifications
You must be signed in to change notification settings - Fork 76
impr: Strict type checking on logical and relational binary expressions operands #2647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fed3aa2
89e4f5d
59ebd23
faee545
5bc6920
6538f87
0d6d656
3b6467e
57a384a
870342c
f5b4984
41fe48d
7b9def7
c797858
2dca023
96cd096
8660054
e9c7882
5c29027
a878f01
bbb2dfc
6d5ee16
731cb19
1bc02b3
c6ee8ec
ad575e8
81a30b1
b58883a
f62da06
df3997a
f5a7400
23d4955
36dd367
f91c49e
14ca77f
4e94946
ffdb6f6
53db4a4
67fd71e
a6688d2
29819e6
40c861c
145fbda
fcb0717
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,8 @@ import { $gpuCallable, $internal, $providing, isMarkedInternal } from '../shared | |||||||||
| import { safeStringify } from '../shared/stringify.ts'; | ||||||||||
| import { pow } from '../std/numeric.ts'; | ||||||||||
| import { add, div, mul, neg, sub } from '../std/operators.ts'; | ||||||||||
| import { eq, ne, lt, le, gt, ge } from '../std/boolean.ts'; | ||||||||||
|
|
||||||||||
| import { | ||||||||||
| isGPUCallable, | ||||||||||
| isKnownAtComptime, | ||||||||||
|
|
@@ -80,11 +82,17 @@ const parenthesizedOps = [ | |||||||||
| '|', | ||||||||||
| '^', | ||||||||||
| '&', | ||||||||||
| '&&', | ||||||||||
| '||', | ||||||||||
| ]; | ||||||||||
|
|
||||||||||
| const binaryLogicalOps = ['&&', '||', '==', '!=', '===', '!==', '<', '<=', '>', '>=']; | ||||||||||
| const binaryRelationalOpToStdMap: Record<string, string> = { | ||||||||||
| '===': eq.toString(), | ||||||||||
| '!==': ne.toString(), | ||||||||||
| '<': lt.toString(), | ||||||||||
| '<=': le.toString(), | ||||||||||
| '>': gt.toString(), | ||||||||||
| '>=': ge.toString(), | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const bitShiftOps: string[] = ['<<', '>>', '<<=', '>>=']; | ||||||||||
|
|
||||||||||
|
|
@@ -332,41 +340,72 @@ ${this.ctx.pre}}`; | |||||||||
| return snip(expression, bool, /* origin */ 'constant', false); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if ( | ||||||||||
| expression[0] === NODE.logicalExpr || | ||||||||||
| expression[0] === NODE.binaryExpr || | ||||||||||
| expression[0] === NODE.assignmentExpr | ||||||||||
| ) { | ||||||||||
| // Logical/Binary/Assignment Expression | ||||||||||
| const [exprType, lhs, op, rhs] = expression; | ||||||||||
| if (expression[0] === NODE.logicalExpr) { | ||||||||||
| const [_, lhs, op, rhs] = expression; | ||||||||||
| const lhsExpr = this._expression(lhs); | ||||||||||
|
|
||||||||||
| // Short Circuit Evaluation | ||||||||||
| if ((op === '||' || op === '&&') && isKnownAtComptime(lhsExpr)) { | ||||||||||
| if (isKnownAtComptime(lhsExpr)) { | ||||||||||
| const castToBool = wgsl.isBool(this.ctx.expectedType); | ||||||||||
| const evalRhs = op === '&&' ? lhsExpr.value : !lhsExpr.value; | ||||||||||
|
|
||||||||||
| if (!evalRhs) { | ||||||||||
| return snip(op === '||', bool, 'constant', false); | ||||||||||
| return castToBool | ||||||||||
| ? snip(op === '||', bool, 'constant', false) | ||||||||||
| : coerceToSnippet(lhsExpr.value); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const rhsExpr = this._expression(rhs); | ||||||||||
|
|
||||||||||
| if (rhsExpr.dataType === UnknownData) { | ||||||||||
| throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`); | ||||||||||
| if (isKnownAtComptime(rhsExpr)) { | ||||||||||
| return castToBool | ||||||||||
| ? snip(!!rhsExpr.value, bool, 'constant', false) | ||||||||||
| : coerceToSnippet(rhsExpr.value); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (isKnownAtComptime(rhsExpr)) { | ||||||||||
| return snip(!!rhsExpr.value, bool, 'constant', false); | ||||||||||
| if (rhsExpr.dataType === UnknownData) { | ||||||||||
| throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // we can skip lhs | ||||||||||
| const convRhs = tryConvertSnippet(this.ctx, rhsExpr, bool, false); | ||||||||||
| const rhsStr = this.ctx.resolveSnippet(convRhs).value; | ||||||||||
| return snip(rhsStr, bool, 'runtime', convRhs.possibleSideEffects); | ||||||||||
| return rhsExpr; | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No cast to bool check & call here? |
||||||||||
| } | ||||||||||
|
|
||||||||||
| const rhsExpr = this._expression(rhs); | ||||||||||
|
|
||||||||||
| // they are not known at comptime | ||||||||||
| if (lhsExpr.dataType === UnknownData) { | ||||||||||
| throw new WgslTypeError(`Left-hand side of '${op}' is of unknown type`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (!isKnownAtComptime(rhsExpr) && rhsExpr.dataType === UnknownData) { | ||||||||||
| throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (!wgsl.isBool(lhsExpr.dataType) || !wgsl.isBool(rhsExpr.dataType)) { | ||||||||||
| throw new WgslTypeError( | ||||||||||
| `Logical expression '${op}' requires boolean operands. Got '${String(lhsExpr.dataType)}' and '${String(rhsExpr.dataType)}'.`, | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const lhsStr = this.ctx.resolve(lhsExpr.value, lhsExpr.dataType).value; | ||||||||||
| const rhsStr = this.ctx.resolve(rhsExpr.value, rhsExpr.dataType).value; | ||||||||||
|
Comment on lines
+391
to
+392
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
|
||||||||||
| // hardcoded parentheses - operators not present in `parenthesizedOps` | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why were they removed from there? |
||||||||||
| return snip( | ||||||||||
| `(${lhsStr} ${op} ${rhsStr})`, | ||||||||||
| bool, | ||||||||||
| 'runtime', | ||||||||||
| lhsExpr.possibleSideEffects || rhsExpr.possibleSideEffects, | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (expression[0] === NODE.binaryExpr || expression[0] === NODE.assignmentExpr) { | ||||||||||
| // Binary/Assignment Expression | ||||||||||
| const [exprType, lhs, op, rhs] = expression; | ||||||||||
| const lhsExpr = this._expression(lhs); | ||||||||||
| const rhsExpr = this._expression(rhs); | ||||||||||
|
|
||||||||||
| if (rhsExpr.value instanceof RefOperator) { | ||||||||||
| throw new WgslTypeError( | ||||||||||
| stitch`Cannot assign a ref to an existing variable '${stringifyNode(lhs)}', define a new variable instead.`, | ||||||||||
|
|
@@ -381,24 +420,26 @@ ${this.ctx.pre}}`; | |||||||||
| throw new Error('Please use the !== operator instead of !='); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (op === '===' && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) { | ||||||||||
| return snip(lhsExpr.value === rhsExpr.value, bool, 'constant', false); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (op === '!==' && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) { | ||||||||||
| return snip(lhsExpr.value !== rhsExpr.value, bool, 'constant', false); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if ( | ||||||||||
| (op === '<' || op === '<=' || op === '>' || op === '>=') && | ||||||||||
| isKnownAtComptime(lhsExpr) && | ||||||||||
| isKnownAtComptime(rhsExpr) | ||||||||||
| ) { | ||||||||||
| const stdBinaryRelationalOp = binaryRelationalOpToStdMap[op]; | ||||||||||
| if (stdBinaryRelationalOp && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) { | ||||||||||
| const left = lhsExpr.value; | ||||||||||
| const right = rhsExpr.value; | ||||||||||
|
|
||||||||||
| switch (op) { | ||||||||||
| case '===': | ||||||||||
| return snip(left === right, bool, 'constant', false); | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get it that we want string and other comparisons to work, but it's still funny that comparing |
||||||||||
| case '!==': | ||||||||||
| return snip(left !== right, bool, 'constant', false); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (typeof left !== 'number' || typeof right !== 'number') { | ||||||||||
| const bothVectors = wgsl.isVec(lhsExpr.dataType) && wgsl.isVec(rhsExpr.dataType); | ||||||||||
| throw new WgslTypeError( | ||||||||||
| `Inequality comparison '${op}' requires numeric operands, got '${typeof left}' and '${typeof right}'`, | ||||||||||
| `Comparison '${op}' requires numeric operands.${ | ||||||||||
| bothVectors | ||||||||||
| ? ` For component-wise comparison, use 'std.${stdBinaryRelationalOp}'.` | ||||||||||
| : '' | ||||||||||
| }`, | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -467,6 +508,24 @@ ${this.ctx.pre}}`; | |||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (stdBinaryRelationalOp) { | ||||||||||
| const equalityCheck = ['===', '!=='].includes(op); | ||||||||||
| const correctOperandTypes = | ||||||||||
| (wgsl.isNumericSchema(convLhs.dataType) && wgsl.isNumericSchema(convRhs.dataType)) || | ||||||||||
| (equalityCheck && wgsl.isBool(convLhs.dataType) && wgsl.isBool(convRhs.dataType)); | ||||||||||
|
|
||||||||||
| if (!correctOperandTypes) { | ||||||||||
| const bothVectors = wgsl.isVec(convLhs.dataType) && wgsl.isVec(convRhs.dataType); | ||||||||||
| throw new WgslTypeError( | ||||||||||
| `Comparison '${op}' requires numeric${equalityCheck ? ' or boolean' : ''} operands. Got '${String(convLhs.dataType)}' and '${String(convRhs.dataType)}'.${ | ||||||||||
| bothVectors | ||||||||||
| ? ` For component-wise comparison, use 'std.${stdBinaryRelationalOp}'.` | ||||||||||
| : '' | ||||||||||
| }`, | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return snip( | ||||||||||
| parenthesizedOps.includes(op) | ||||||||||
| ? `(${lhsStr} ${OP_MAP[op] ?? op} ${rhsStr})` | ||||||||||
|
|
@@ -876,6 +935,7 @@ ${this.ctx.pre}}`; | |||||||||
| if (isKnownAtComptime(test)) { | ||||||||||
| return test.value ? this._expression(consequentNode) : this._expression(alternativeNode); | ||||||||||
| } else { | ||||||||||
| const convertedTest = tryConvertSnippet(this.ctx, test, bool, false); | ||||||||||
| const consequent = this._expression(consequentNode); | ||||||||||
| const alternative = this._expression(alternativeNode); | ||||||||||
| const [con, alt] = | ||||||||||
|
|
@@ -888,7 +948,7 @@ ${this.ctx.pre}}`; | |||||||||
| } | ||||||||||
|
|
||||||||||
| return snip( | ||||||||||
| stitch`select(${alt}, ${con}, ${test})`, | ||||||||||
| stitch`select(${alt}, ${con}, ${convertedTest})`, | ||||||||||
| con.dataType, | ||||||||||
| 'runtime', | ||||||||||
| // this select has side-effects only if the condition has side-effects | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a slight inconsistency between
if (...)andif (true && ...)