Skip to content

Commit d17ee42

Browse files
fuzzie360claude
andcommitted
fix: a kernel takes Array or Float32Array (#857), and a local may shadow a constant (#858)
#857: the GL kernel values rejected a value whose constructor differed from the first one they saw, so a kernel called with [1,2,3,4] refused a Float32Array of the same shape. Both spell their type 'Array', so the switched-to kernel had the same signature and rejected it again -- the call never settled and threw after four rebuilds, while cpu (which has no such check) returned the right answer. The container is not what those values depend on: `flattenTo` reads either into the same upload buffer. They now reject only what is not an array-like at all -- a texture, an Input -- which genuinely needs a kernel value built for it. #858: the cpu backend renamed identifiers by spelling, so a local sharing a constant's name was rewritten in its own declarator and `const n = this.constants.n` emitted `const constants_n = constants_n`, dying in the temporal dead zone. It now prefers the local binding, the way JavaScript does; `this.constants.n` is emitted by astMemberExpression and never reached that code. The GL backends were already correct, so this closes a cpu-vs-gpu difference that let a kernel pass in GPU mode and break in cpu. Both are covered per backend and precision, 6 of 18 runnable assertions failing with the fixes reverted. Two limits found while testing are noted in the tests rather than papered over: a local cannot be a loop bound on WebGL1 whatever it is named (GLSL ES 1.00 wants a constant expression), and the unsigned encoder is bit-exact on Metal but ~1e-6 off on SwiftShader, so those assertions compare rounded. Closes #857 Closes #858 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUTVDFaHav3uAdN3XfZLyx
1 parent 78b1fe0 commit d17ee42

11 files changed

Lines changed: 217 additions & 18 deletions

File tree

dist/gpu-browser-core.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.20.0
8-
* @date Fri Jul 31 2026 01:15:28 GMT+0800 (Singapore Standard Time)
8+
* @date Fri Jul 31 2026 18:14:38 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -3264,7 +3264,7 @@
32643264
break;
32653265

32663266
default:
3267-
if (this.constants && this.constants.hasOwnProperty(idtNode.name)) retArr.push("constants_" + idtNode.name); else retArr.push("user_" + idtNode.name);
3267+
if (!this.getDeclaration(idtNode) && this.constants && this.constants.hasOwnProperty(idtNode.name)) retArr.push("constants_" + idtNode.name); else retArr.push("user_" + idtNode.name);
32683268
}
32693269
return retArr;
32703270
}
@@ -8404,7 +8404,7 @@
84048404
return utils.linesToString([ `uniform sampler2D ${this.id}`, `ivec2 ${this.sizeId} = ivec2(${this.textureSize[0]}, ${this.textureSize[1]})`, `ivec3 ${this.dimensionsId} = ivec3(${this.dimensions[0]}, ${this.dimensions[1]}, ${this.dimensions[2]})` ]);
84058405
}
84068406
updateValue(value) {
8407-
if (value.constructor !== this.initialValueConstructor) {
8407+
if (!utils.isArray(value)) {
84088408
this.onUpdateValueMismatch(value.constructor);
84098409
return;
84108410
}
@@ -8714,7 +8714,7 @@
87148714
return utils.linesToString([ `uniform sampler2D ${this.id}`, `ivec2 ${this.sizeId} = ivec2(${this.textureSize[0]}, ${this.textureSize[1]})`, `ivec3 ${this.dimensionsId} = ivec3(${this.dimensions[0]}, ${this.dimensions[1]}, ${this.dimensions[2]})` ]);
87158715
}
87168716
updateValue(value) {
8717-
if (value.constructor !== this.initialValueConstructor) {
8717+
if (!utils.isArray(value)) {
87188718
this.onUpdateValueMismatch(value.constructor);
87198719
return;
87208720
}
@@ -10291,7 +10291,7 @@
1029110291
return utils.linesToString([ `uniform ${variablePrecision} sampler2D ${this.id}`, `${variablePrecision} ivec2 ${this.sizeId} = ivec2(${this.textureSize[0]}, ${this.textureSize[1]})`, `${variablePrecision} ivec3 ${this.dimensionsId} = ivec3(${this.dimensions[0]}, ${this.dimensions[1]}, ${this.dimensions[2]})` ]);
1029210292
}
1029310293
updateValue(value) {
10294-
if (value.constructor !== this.initialValueConstructor) {
10294+
if (!utils.isArray(value)) {
1029510295
this.onUpdateValueMismatch(value.constructor);
1029610296
return;
1029710297
}

dist/gpu-browser-core.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/gpu-browser.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.20.0
8-
* @date Fri Jul 31 2026 01:15:28 GMT+0800 (Singapore Standard Time)
8+
* @date Fri Jul 31 2026 18:14:38 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -7458,7 +7458,7 @@
74587458
break;
74597459

74607460
default:
7461-
if (this.constants && this.constants.hasOwnProperty(idtNode.name)) retArr.push("constants_" + idtNode.name); else retArr.push("user_" + idtNode.name);
7461+
if (!this.getDeclaration(idtNode) && this.constants && this.constants.hasOwnProperty(idtNode.name)) retArr.push("constants_" + idtNode.name); else retArr.push("user_" + idtNode.name);
74627462
}
74637463
return retArr;
74647464
}
@@ -12601,7 +12601,7 @@
1260112601
return utils.linesToString([ `uniform sampler2D ${this.id}`, `ivec2 ${this.sizeId} = ivec2(${this.textureSize[0]}, ${this.textureSize[1]})`, `ivec3 ${this.dimensionsId} = ivec3(${this.dimensions[0]}, ${this.dimensions[1]}, ${this.dimensions[2]})` ]);
1260212602
}
1260312603
updateValue(value) {
12604-
if (value.constructor !== this.initialValueConstructor) {
12604+
if (!utils.isArray(value)) {
1260512605
this.onUpdateValueMismatch(value.constructor);
1260612606
return;
1260712607
}
@@ -12911,7 +12911,7 @@
1291112911
return utils.linesToString([ `uniform sampler2D ${this.id}`, `ivec2 ${this.sizeId} = ivec2(${this.textureSize[0]}, ${this.textureSize[1]})`, `ivec3 ${this.dimensionsId} = ivec3(${this.dimensions[0]}, ${this.dimensions[1]}, ${this.dimensions[2]})` ]);
1291212912
}
1291312913
updateValue(value) {
12914-
if (value.constructor !== this.initialValueConstructor) {
12914+
if (!utils.isArray(value)) {
1291512915
this.onUpdateValueMismatch(value.constructor);
1291612916
return;
1291712917
}
@@ -14488,7 +14488,7 @@
1448814488
return utils.linesToString([ `uniform ${variablePrecision} sampler2D ${this.id}`, `${variablePrecision} ivec2 ${this.sizeId} = ivec2(${this.textureSize[0]}, ${this.textureSize[1]})`, `${variablePrecision} ivec3 ${this.dimensionsId} = ivec3(${this.dimensions[0]}, ${this.dimensions[1]}, ${this.dimensions[2]})` ]);
1448914489
}
1449014490
updateValue(value) {
14491-
if (value.constructor !== this.initialValueConstructor) {
14491+
if (!utils.isArray(value)) {
1449214492
this.onUpdateValueMismatch(value.constructor);
1449314493
return;
1449414494
}

dist/gpu-browser.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/backend/cpu/function-node.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,16 @@ class CPUFunctionNode extends FunctionNode {
136136
retArr.push('Infinity');
137137
break;
138138
default:
139-
if (this.constants && this.constants.hasOwnProperty(idtNode.name)) {
139+
// A local binding wins over a constant of the same name, the way it
140+
// does in JavaScript. Classifying by spelling alone renamed a kernel
141+
// local `n` to `constants_n` in its own declarator, so
142+
// `const n = this.constants.n` emitted `const constants_n =
143+
// constants_n` and died in the temporal dead zone. `this.constants.n`
144+
// itself never reaches here -- astMemberExpression emits it.
145+
if (
146+
!this.getDeclaration(idtNode) &&
147+
this.constants && this.constants.hasOwnProperty(idtNode.name)
148+
) {
140149
retArr.push('constants_' + idtNode.name);
141150
} else {
142151
retArr.push('user_' + idtNode.name);

src/backend/web-gl/kernel-value/single-array.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ class WebGLKernelValueSingleArray extends WebGLKernelArray {
2828
}
2929

3030
updateValue(value) {
31-
if (value.constructor !== this.initialValueConstructor) {
31+
// The container does not matter here: a plain Array and a Float32Array of
32+
// the same numbers both flatten into the same upload buffer, so rejecting
33+
// on constructor identity made two interchangeable values unusable on one
34+
// kernel (#857). What must still switch is a value of a different kind --
35+
// a texture, an Input -- which needs a kernel value built for it.
36+
if (!utils.isArray(value)) {
3237
this.onUpdateValueMismatch(value.constructor);
3338
return;
3439
}

src/backend/web-gl/kernel-value/unsigned-array.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ class WebGLKernelValueUnsignedArray extends WebGLKernelArray {
3131
}
3232

3333
updateValue(value) {
34-
if (value.constructor !== this.initialValueConstructor) {
34+
// The container does not matter here: a plain Array and a Float32Array of
35+
// the same numbers both flatten into the same upload buffer, so rejecting
36+
// on constructor identity made two interchangeable values unusable on one
37+
// kernel (#857). What must still switch is a value of a different kind --
38+
// a texture, an Input -- which needs a kernel value built for it.
39+
if (!utils.isArray(value)) {
3540
this.onUpdateValueMismatch(value.constructor);
3641
return;
3742
}

src/backend/web-gl2/kernel-value/single-array.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ class WebGL2KernelValueSingleArray extends WebGLKernelValueSingleArray {
1212
}
1313

1414
updateValue(value) {
15-
if (value.constructor !== this.initialValueConstructor) {
15+
// The container does not matter here: a plain Array and a Float32Array of
16+
// the same numbers both flatten into the same upload buffer, so rejecting
17+
// on constructor identity made two interchangeable values unusable on one
18+
// kernel (#857). What must still switch is a value of a different kind --
19+
// a texture, an Input -- which needs a kernel value built for it.
20+
if (!utils.isArray(value)) {
1621
this.onUpdateValueMismatch(value.constructor);
1722
return;
1823
}

test/all.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@
297297
<script type="module" src="issues/844-browser-bundle-global.js"></script>
298298
<script type="module" src="issues/854-assignment-expression-parens.js"></script>
299299
<script type="module" src="issues/855-switch-case-break.js"></script>
300+
<script type="module" src="issues/857-array-and-typed-array.js"></script>
301+
<script type="module" src="issues/858-local-shadowing-a-constant.js"></script>
300302
<script type="module" src="issues/91-create-kernel-map-array.js"></script>
301303
<script type="module" src="issues/96-param-names.js"></script>
302304
<script type="module" src="features/to-string/as-file.js"></script>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const { assert, skip, test, module: describe } = require('qunit');
2+
const { GPU } = require('../../src');
3+
4+
describe('issue #857');
5+
6+
// A plain Array and a Float32Array holding the same numbers flatten into the
7+
// same upload buffer, so one kernel should take either. The GL kernel values
8+
// rejected on constructor identity instead, and because both spell their type
9+
// 'Array' the switched-to kernel had the same signature and rejected it again
10+
// -- so the call never settled and threw after four rebuilds. cpu never had
11+
// the check and always worked.
12+
13+
// What is under test is that the container does not matter, not how exactly a
14+
// precision round-trips: the unsigned encoder is bit-exact on some drivers and
15+
// off by ~1e-6 on others (SwiftShader), which would fail an equality assertion
16+
// for reasons that have nothing to do with this bug.
17+
function rounded(values) {
18+
return Array.from(values).map(value => Math.round(value * 1e4) / 1e4);
19+
}
20+
21+
function testInterchangeable(mode, precision, assert) {
22+
const gpu = new GPU({ mode });
23+
const kernel = gpu.createKernel(function (a) {
24+
return a[this.thread.x] * 2;
25+
}, { output: [4], precision });
26+
assert.deepEqual(rounded(kernel([1, 2, 3, 4])), [2, 4, 6, 8], 'plain array');
27+
assert.deepEqual(
28+
rounded(kernel(Float32Array.from([5, 6, 7, 8]))),
29+
[10, 12, 14, 16],
30+
'Float32Array on the same kernel');
31+
assert.deepEqual(rounded(kernel([9, 10, 11, 12])), [18, 20, 22, 24], 'and back to a plain array');
32+
gpu.destroy();
33+
}
34+
35+
const MODES = [
36+
['cpu', true],
37+
['webgl', GPU.isWebGLSupported],
38+
['webgl2', GPU.isWebGL2Supported],
39+
['headlessgl', GPU.isHeadlessGLSupported],
40+
];
41+
42+
for (const [mode, supported] of MODES) {
43+
(supported ? test : skip)(`Issue #857 - Array then Float32Array single precision ${ mode }`, assert => {
44+
testInterchangeable(mode, 'single', assert);
45+
});
46+
(supported ? test : skip)(`Issue #857 - Array then Float32Array unsigned precision ${ mode }`, assert => {
47+
testInterchangeable(mode, 'unsigned', assert);
48+
});
49+
(supported ? test : skip)(`Issue #857 - Float32Array first, then Array ${ mode }`, assert => {
50+
const gpu = new GPU({ mode });
51+
const kernel = gpu.createKernel(function (a) {
52+
return a[this.thread.x] * 2;
53+
}, { output: [4] });
54+
assert.deepEqual(Array.from(kernel(Float32Array.from([1, 2, 3, 4]))), [2, 4, 6, 8]);
55+
assert.deepEqual(Array.from(kernel([5, 6, 7, 8])), [10, 12, 14, 16]);
56+
gpu.destroy();
57+
});
58+
(supported ? test : skip)(`Issue #857 - a changed shape still switches kernels ${ mode }`, assert => {
59+
// relaxing the container check must not let a differently-shaped value
60+
// upload into the texture built for the old shape
61+
const gpu = new GPU({ mode });
62+
const build = g => g.createKernel(function (a) {
63+
return a[this.thread.x][0];
64+
}, { output: [2] });
65+
const kernel = build(gpu);
66+
assert.deepEqual(Array.from(kernel([[1, 2], [3, 4]])), [1, 3], 'first shape');
67+
const reference = build(new GPU({ mode }));
68+
assert.deepEqual(
69+
Array.from(kernel([[9, 8], [7, 6]])),
70+
Array.from(reference([[9, 8], [7, 6]])),
71+
'same answer as a kernel that only saw this value');
72+
gpu.destroy();
73+
});
74+
}

0 commit comments

Comments
 (0)