diff --git a/full/Makefile b/full/Makefile index cdbf4db..49de5b4 100644 --- a/full/Makefile +++ b/full/Makefile @@ -66,9 +66,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/full/package.json b/full/package.json index 6848207..a5d3226 100644 --- a/full/package.json +++ b/full/package.json @@ -21,7 +21,7 @@ "wasm:rebuild": "pnpm wasm:make rebuild", "wasm:clean": "pnpm wasm:make clean", "wasm:clean-cache": "pnpm wasm:make clean-cache", - "test": "node --test test/parsing.test.js test/fingerprint.test.js test/normalize.test.js test/plpgsql.test.js test/scan.test.js test/errors.test.js", + "test": "node --test test/parsing.test.js test/fingerprint.test.js test/normalize.test.js test/plpgsql.test.js test/scan.test.js test/errors.test.js test/memory.test.js test/deep-nesting.test.js", "yamlize": "node ./scripts/yamlize.js" }, "author": "Constructive ", diff --git a/full/test/deep-nesting.test.js b/full/test/deep-nesting.test.js new file mode 100644 index 0000000..a67f257 --- /dev/null +++ b/full/test/deep-nesting.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const query = require('../'); + +// Pairs with the reduced STACK_SIZE in the Makefile. Parsing and serializing a +// deeply nested tree recurses on the WASM C stack; if that stack is too small, +// deep input crashes the module with a hard WebAssembly trap instead of failing +// gracefully. This test asserts the parser always either succeeds or throws a +// clean, catchable error (e.g. SqlError "memory exhausted") — never a WASM trap. +// CI rebuilds the WASM from source, so a stack that's too small fails here. +describe('Deep nesting fails gracefully', () => { + // Nested function calls build a genuinely deep parse tree (FuncCall nodes) — + // the shape that recurses hardest during tree->JSON serialization. Depths span + // the band that parses today through where it fails cleanly. + const nestedFuncs = (depth) => 'SELECT ' + 'f('.repeat(depth) + '1' + ')'.repeat(depth); + + for (const depth of [1000, 4000, 7000, 10000]) { + it(`depth ${depth}: parses or fails cleanly, never a WASM trap`, async () => { + try { + await query.parse(nestedFuncs(depth)); + } catch (err) { + assert.ok( + !(err instanceof WebAssembly.RuntimeError), + `deep input crashed with a WASM trap (stack too small?): ${err && err.message}` + ); + } + }); + } +}); diff --git a/full/test/memory.test.js b/full/test/memory.test.js new file mode 100644 index 0000000..6fe91cb --- /dev/null +++ b/full/test/memory.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +// Emscripten MODULARIZE factory; calling it instantiates a fresh, isolated +// WASM instance with its own linear memory. +const createModule = require('../wasm/libpg-query.js'); + +// Guards the WASM load-time footprint from regressing. The module reserves its +// initial linear memory on instantiation, before any parse — that reservation +// is what inflates load-time memory. Growth is enabled (-sALLOW_MEMORY_GROWTH), +// so the initial size is a footprint knob, not a hard limit. We read the live +// size from the instance (HEAPU8 views the linear memory) instead of parsing +// the binary: no dependency, and it reflects the real allocation. +describe('WASM memory footprint', () => { + // Ceiling for the initial reservation; the single source of truth. Keep it a + // bit above -sINITIAL_MEMORY in the Makefile so normal tuning doesn't trip it. + const MAX_INITIAL_BYTES = 64 * 1024 * 1024; + + it('reserves an initial linear memory within budget', async () => { + const m = await createModule(); + const initialBytes = m.HEAPU8.buffer.byteLength; + const mib = (n) => (n / (1024 * 1024)).toFixed(0); + assert.ok( + initialBytes <= MAX_INITIAL_BYTES, + `WASM initial memory ${mib(initialBytes)} MiB exceeds the ${mib(MAX_INITIAL_BYTES)} MiB budget. ` + + `Lower -sINITIAL_MEMORY in the Makefile (memory grows on demand via -sALLOW_MEMORY_GROWTH).` + ); + }); +}); diff --git a/templates/Makefile.template b/templates/Makefile.template index 422f286..7d25287 100644 --- a/templates/Makefile.template +++ b/templates/Makefile.template @@ -71,9 +71,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/versions/13/Makefile b/versions/13/Makefile index 4ace118..229195d 100644 --- a/versions/13/Makefile +++ b/versions/13/Makefile @@ -74,9 +74,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/versions/14/Makefile b/versions/14/Makefile index 1d9b073..8a80879 100644 --- a/versions/14/Makefile +++ b/versions/14/Makefile @@ -71,9 +71,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/versions/14/package.json b/versions/14/package.json index c1f7fd6..51af32c 100644 --- a/versions/14/package.json +++ b/versions/14/package.json @@ -28,7 +28,7 @@ "wasm:rebuild": "pnpm wasm:make rebuild", "wasm:clean": "pnpm wasm:make clean", "wasm:clean-cache": "pnpm wasm:make clean-cache", - "test": "node --test test/parsing.test.js test/errors.test.js" + "test": "node --test test/parsing.test.js test/errors.test.js test/memory.test.js test/deep-nesting.test.js" }, "author": "Constructive ", "license": "MIT", diff --git a/versions/14/test/deep-nesting.test.js b/versions/14/test/deep-nesting.test.js new file mode 100644 index 0000000..a67f257 --- /dev/null +++ b/versions/14/test/deep-nesting.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const query = require('../'); + +// Pairs with the reduced STACK_SIZE in the Makefile. Parsing and serializing a +// deeply nested tree recurses on the WASM C stack; if that stack is too small, +// deep input crashes the module with a hard WebAssembly trap instead of failing +// gracefully. This test asserts the parser always either succeeds or throws a +// clean, catchable error (e.g. SqlError "memory exhausted") — never a WASM trap. +// CI rebuilds the WASM from source, so a stack that's too small fails here. +describe('Deep nesting fails gracefully', () => { + // Nested function calls build a genuinely deep parse tree (FuncCall nodes) — + // the shape that recurses hardest during tree->JSON serialization. Depths span + // the band that parses today through where it fails cleanly. + const nestedFuncs = (depth) => 'SELECT ' + 'f('.repeat(depth) + '1' + ')'.repeat(depth); + + for (const depth of [1000, 4000, 7000, 10000]) { + it(`depth ${depth}: parses or fails cleanly, never a WASM trap`, async () => { + try { + await query.parse(nestedFuncs(depth)); + } catch (err) { + assert.ok( + !(err instanceof WebAssembly.RuntimeError), + `deep input crashed with a WASM trap (stack too small?): ${err && err.message}` + ); + } + }); + } +}); diff --git a/versions/14/test/memory.test.js b/versions/14/test/memory.test.js new file mode 100644 index 0000000..6fe91cb --- /dev/null +++ b/versions/14/test/memory.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +// Emscripten MODULARIZE factory; calling it instantiates a fresh, isolated +// WASM instance with its own linear memory. +const createModule = require('../wasm/libpg-query.js'); + +// Guards the WASM load-time footprint from regressing. The module reserves its +// initial linear memory on instantiation, before any parse — that reservation +// is what inflates load-time memory. Growth is enabled (-sALLOW_MEMORY_GROWTH), +// so the initial size is a footprint knob, not a hard limit. We read the live +// size from the instance (HEAPU8 views the linear memory) instead of parsing +// the binary: no dependency, and it reflects the real allocation. +describe('WASM memory footprint', () => { + // Ceiling for the initial reservation; the single source of truth. Keep it a + // bit above -sINITIAL_MEMORY in the Makefile so normal tuning doesn't trip it. + const MAX_INITIAL_BYTES = 64 * 1024 * 1024; + + it('reserves an initial linear memory within budget', async () => { + const m = await createModule(); + const initialBytes = m.HEAPU8.buffer.byteLength; + const mib = (n) => (n / (1024 * 1024)).toFixed(0); + assert.ok( + initialBytes <= MAX_INITIAL_BYTES, + `WASM initial memory ${mib(initialBytes)} MiB exceeds the ${mib(MAX_INITIAL_BYTES)} MiB budget. ` + + `Lower -sINITIAL_MEMORY in the Makefile (memory grows on demand via -sALLOW_MEMORY_GROWTH).` + ); + }); +}); diff --git a/versions/15/Makefile b/versions/15/Makefile index 68777d3..f775ffe 100644 --- a/versions/15/Makefile +++ b/versions/15/Makefile @@ -73,9 +73,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/versions/15/package.json b/versions/15/package.json index adf7cd3..d563ad6 100644 --- a/versions/15/package.json +++ b/versions/15/package.json @@ -28,7 +28,7 @@ "wasm:rebuild": "pnpm wasm:make rebuild", "wasm:clean": "pnpm wasm:make clean", "wasm:clean-cache": "pnpm wasm:make clean-cache", - "test": "node --test test/parsing.test.js test/errors.test.js" + "test": "node --test test/parsing.test.js test/errors.test.js test/memory.test.js test/deep-nesting.test.js" }, "author": "Constructive ", "license": "MIT", diff --git a/versions/15/test/deep-nesting.test.js b/versions/15/test/deep-nesting.test.js new file mode 100644 index 0000000..a67f257 --- /dev/null +++ b/versions/15/test/deep-nesting.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const query = require('../'); + +// Pairs with the reduced STACK_SIZE in the Makefile. Parsing and serializing a +// deeply nested tree recurses on the WASM C stack; if that stack is too small, +// deep input crashes the module with a hard WebAssembly trap instead of failing +// gracefully. This test asserts the parser always either succeeds or throws a +// clean, catchable error (e.g. SqlError "memory exhausted") — never a WASM trap. +// CI rebuilds the WASM from source, so a stack that's too small fails here. +describe('Deep nesting fails gracefully', () => { + // Nested function calls build a genuinely deep parse tree (FuncCall nodes) — + // the shape that recurses hardest during tree->JSON serialization. Depths span + // the band that parses today through where it fails cleanly. + const nestedFuncs = (depth) => 'SELECT ' + 'f('.repeat(depth) + '1' + ')'.repeat(depth); + + for (const depth of [1000, 4000, 7000, 10000]) { + it(`depth ${depth}: parses or fails cleanly, never a WASM trap`, async () => { + try { + await query.parse(nestedFuncs(depth)); + } catch (err) { + assert.ok( + !(err instanceof WebAssembly.RuntimeError), + `deep input crashed with a WASM trap (stack too small?): ${err && err.message}` + ); + } + }); + } +}); diff --git a/versions/15/test/memory.test.js b/versions/15/test/memory.test.js new file mode 100644 index 0000000..6fe91cb --- /dev/null +++ b/versions/15/test/memory.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +// Emscripten MODULARIZE factory; calling it instantiates a fresh, isolated +// WASM instance with its own linear memory. +const createModule = require('../wasm/libpg-query.js'); + +// Guards the WASM load-time footprint from regressing. The module reserves its +// initial linear memory on instantiation, before any parse — that reservation +// is what inflates load-time memory. Growth is enabled (-sALLOW_MEMORY_GROWTH), +// so the initial size is a footprint knob, not a hard limit. We read the live +// size from the instance (HEAPU8 views the linear memory) instead of parsing +// the binary: no dependency, and it reflects the real allocation. +describe('WASM memory footprint', () => { + // Ceiling for the initial reservation; the single source of truth. Keep it a + // bit above -sINITIAL_MEMORY in the Makefile so normal tuning doesn't trip it. + const MAX_INITIAL_BYTES = 64 * 1024 * 1024; + + it('reserves an initial linear memory within budget', async () => { + const m = await createModule(); + const initialBytes = m.HEAPU8.buffer.byteLength; + const mib = (n) => (n / (1024 * 1024)).toFixed(0); + assert.ok( + initialBytes <= MAX_INITIAL_BYTES, + `WASM initial memory ${mib(initialBytes)} MiB exceeds the ${mib(MAX_INITIAL_BYTES)} MiB budget. ` + + `Lower -sINITIAL_MEMORY in the Makefile (memory grows on demand via -sALLOW_MEMORY_GROWTH).` + ); + }); +}); diff --git a/versions/16/Makefile b/versions/16/Makefile index 6bd9e62..e2b546d 100644 --- a/versions/16/Makefile +++ b/versions/16/Makefile @@ -71,9 +71,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/versions/16/package.json b/versions/16/package.json index 8ec811d..1126c11 100644 --- a/versions/16/package.json +++ b/versions/16/package.json @@ -28,7 +28,7 @@ "wasm:rebuild": "pnpm wasm:make rebuild", "wasm:clean": "pnpm wasm:make clean", "wasm:clean-cache": "pnpm wasm:make clean-cache", - "test": "node --test test/parsing.test.js test/errors.test.js" + "test": "node --test test/parsing.test.js test/errors.test.js test/memory.test.js test/deep-nesting.test.js" }, "author": "Constructive ", "license": "MIT", diff --git a/versions/16/test/deep-nesting.test.js b/versions/16/test/deep-nesting.test.js new file mode 100644 index 0000000..a67f257 --- /dev/null +++ b/versions/16/test/deep-nesting.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const query = require('../'); + +// Pairs with the reduced STACK_SIZE in the Makefile. Parsing and serializing a +// deeply nested tree recurses on the WASM C stack; if that stack is too small, +// deep input crashes the module with a hard WebAssembly trap instead of failing +// gracefully. This test asserts the parser always either succeeds or throws a +// clean, catchable error (e.g. SqlError "memory exhausted") — never a WASM trap. +// CI rebuilds the WASM from source, so a stack that's too small fails here. +describe('Deep nesting fails gracefully', () => { + // Nested function calls build a genuinely deep parse tree (FuncCall nodes) — + // the shape that recurses hardest during tree->JSON serialization. Depths span + // the band that parses today through where it fails cleanly. + const nestedFuncs = (depth) => 'SELECT ' + 'f('.repeat(depth) + '1' + ')'.repeat(depth); + + for (const depth of [1000, 4000, 7000, 10000]) { + it(`depth ${depth}: parses or fails cleanly, never a WASM trap`, async () => { + try { + await query.parse(nestedFuncs(depth)); + } catch (err) { + assert.ok( + !(err instanceof WebAssembly.RuntimeError), + `deep input crashed with a WASM trap (stack too small?): ${err && err.message}` + ); + } + }); + } +}); diff --git a/versions/16/test/memory.test.js b/versions/16/test/memory.test.js new file mode 100644 index 0000000..6fe91cb --- /dev/null +++ b/versions/16/test/memory.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +// Emscripten MODULARIZE factory; calling it instantiates a fresh, isolated +// WASM instance with its own linear memory. +const createModule = require('../wasm/libpg-query.js'); + +// Guards the WASM load-time footprint from regressing. The module reserves its +// initial linear memory on instantiation, before any parse — that reservation +// is what inflates load-time memory. Growth is enabled (-sALLOW_MEMORY_GROWTH), +// so the initial size is a footprint knob, not a hard limit. We read the live +// size from the instance (HEAPU8 views the linear memory) instead of parsing +// the binary: no dependency, and it reflects the real allocation. +describe('WASM memory footprint', () => { + // Ceiling for the initial reservation; the single source of truth. Keep it a + // bit above -sINITIAL_MEMORY in the Makefile so normal tuning doesn't trip it. + const MAX_INITIAL_BYTES = 64 * 1024 * 1024; + + it('reserves an initial linear memory within budget', async () => { + const m = await createModule(); + const initialBytes = m.HEAPU8.buffer.byteLength; + const mib = (n) => (n / (1024 * 1024)).toFixed(0); + assert.ok( + initialBytes <= MAX_INITIAL_BYTES, + `WASM initial memory ${mib(initialBytes)} MiB exceeds the ${mib(MAX_INITIAL_BYTES)} MiB budget. ` + + `Lower -sINITIAL_MEMORY in the Makefile (memory grows on demand via -sALLOW_MEMORY_GROWTH).` + ); + }); +}); diff --git a/versions/17/Makefile b/versions/17/Makefile index 6674f7a..f4c9ad6 100644 --- a/versions/17/Makefile +++ b/versions/17/Makefile @@ -71,9 +71,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/versions/17/package.json b/versions/17/package.json index 0014343..d6d866a 100644 --- a/versions/17/package.json +++ b/versions/17/package.json @@ -28,7 +28,7 @@ "wasm:rebuild": "pnpm wasm:make rebuild", "wasm:clean": "pnpm wasm:make clean", "wasm:clean-cache": "pnpm wasm:make clean-cache", - "test": "node --test test/parsing.test.js test/errors.test.js" + "test": "node --test test/parsing.test.js test/errors.test.js test/memory.test.js test/deep-nesting.test.js" }, "author": "Constructive ", "license": "MIT", diff --git a/versions/17/test/deep-nesting.test.js b/versions/17/test/deep-nesting.test.js new file mode 100644 index 0000000..a67f257 --- /dev/null +++ b/versions/17/test/deep-nesting.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const query = require('../'); + +// Pairs with the reduced STACK_SIZE in the Makefile. Parsing and serializing a +// deeply nested tree recurses on the WASM C stack; if that stack is too small, +// deep input crashes the module with a hard WebAssembly trap instead of failing +// gracefully. This test asserts the parser always either succeeds or throws a +// clean, catchable error (e.g. SqlError "memory exhausted") — never a WASM trap. +// CI rebuilds the WASM from source, so a stack that's too small fails here. +describe('Deep nesting fails gracefully', () => { + // Nested function calls build a genuinely deep parse tree (FuncCall nodes) — + // the shape that recurses hardest during tree->JSON serialization. Depths span + // the band that parses today through where it fails cleanly. + const nestedFuncs = (depth) => 'SELECT ' + 'f('.repeat(depth) + '1' + ')'.repeat(depth); + + for (const depth of [1000, 4000, 7000, 10000]) { + it(`depth ${depth}: parses or fails cleanly, never a WASM trap`, async () => { + try { + await query.parse(nestedFuncs(depth)); + } catch (err) { + assert.ok( + !(err instanceof WebAssembly.RuntimeError), + `deep input crashed with a WASM trap (stack too small?): ${err && err.message}` + ); + } + }); + } +}); diff --git a/versions/17/test/memory.test.js b/versions/17/test/memory.test.js new file mode 100644 index 0000000..6fe91cb --- /dev/null +++ b/versions/17/test/memory.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +// Emscripten MODULARIZE factory; calling it instantiates a fresh, isolated +// WASM instance with its own linear memory. +const createModule = require('../wasm/libpg-query.js'); + +// Guards the WASM load-time footprint from regressing. The module reserves its +// initial linear memory on instantiation, before any parse — that reservation +// is what inflates load-time memory. Growth is enabled (-sALLOW_MEMORY_GROWTH), +// so the initial size is a footprint knob, not a hard limit. We read the live +// size from the instance (HEAPU8 views the linear memory) instead of parsing +// the binary: no dependency, and it reflects the real allocation. +describe('WASM memory footprint', () => { + // Ceiling for the initial reservation; the single source of truth. Keep it a + // bit above -sINITIAL_MEMORY in the Makefile so normal tuning doesn't trip it. + const MAX_INITIAL_BYTES = 64 * 1024 * 1024; + + it('reserves an initial linear memory within budget', async () => { + const m = await createModule(); + const initialBytes = m.HEAPU8.buffer.byteLength; + const mib = (n) => (n / (1024 * 1024)).toFixed(0); + assert.ok( + initialBytes <= MAX_INITIAL_BYTES, + `WASM initial memory ${mib(initialBytes)} MiB exceeds the ${mib(MAX_INITIAL_BYTES)} MiB budget. ` + + `Lower -sINITIAL_MEMORY in the Makefile (memory grows on demand via -sALLOW_MEMORY_GROWTH).` + ); + }); +}); diff --git a/versions/18/Makefile b/versions/18/Makefile index b66d7d4..af77345 100644 --- a/versions/18/Makefile +++ b/versions/18/Makefile @@ -71,9 +71,9 @@ ifdef EMSCRIPTEN -sMODULARIZE=1 \ -sEXPORT_ES6=0 \ -sALLOW_MEMORY_GROWTH=1 \ - -sINITIAL_MEMORY=134217728 \ + -sINITIAL_MEMORY=25165824 \ -sMAXIMUM_MEMORY=1073741824 \ - -sSTACK_SIZE=33554432 \ + -sSTACK_SIZE=16777216 \ -lpg_query \ -o $@ \ $(SRC_FILES) diff --git a/versions/18/package.json b/versions/18/package.json index a76c966..ffdc1ed 100644 --- a/versions/18/package.json +++ b/versions/18/package.json @@ -28,7 +28,7 @@ "wasm:rebuild": "pnpm wasm:make rebuild", "wasm:clean": "pnpm wasm:make clean", "wasm:clean-cache": "pnpm wasm:make clean-cache", - "test": "node --test test/parsing.test.js test/errors.test.js" + "test": "node --test test/parsing.test.js test/errors.test.js test/memory.test.js test/deep-nesting.test.js" }, "author": "Constructive ", "license": "MIT", diff --git a/versions/18/test/deep-nesting.test.js b/versions/18/test/deep-nesting.test.js new file mode 100644 index 0000000..a67f257 --- /dev/null +++ b/versions/18/test/deep-nesting.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const query = require('../'); + +// Pairs with the reduced STACK_SIZE in the Makefile. Parsing and serializing a +// deeply nested tree recurses on the WASM C stack; if that stack is too small, +// deep input crashes the module with a hard WebAssembly trap instead of failing +// gracefully. This test asserts the parser always either succeeds or throws a +// clean, catchable error (e.g. SqlError "memory exhausted") — never a WASM trap. +// CI rebuilds the WASM from source, so a stack that's too small fails here. +describe('Deep nesting fails gracefully', () => { + // Nested function calls build a genuinely deep parse tree (FuncCall nodes) — + // the shape that recurses hardest during tree->JSON serialization. Depths span + // the band that parses today through where it fails cleanly. + const nestedFuncs = (depth) => 'SELECT ' + 'f('.repeat(depth) + '1' + ')'.repeat(depth); + + for (const depth of [1000, 4000, 7000, 10000]) { + it(`depth ${depth}: parses or fails cleanly, never a WASM trap`, async () => { + try { + await query.parse(nestedFuncs(depth)); + } catch (err) { + assert.ok( + !(err instanceof WebAssembly.RuntimeError), + `deep input crashed with a WASM trap (stack too small?): ${err && err.message}` + ); + } + }); + } +}); diff --git a/versions/18/test/memory.test.js b/versions/18/test/memory.test.js new file mode 100644 index 0000000..6fe91cb --- /dev/null +++ b/versions/18/test/memory.test.js @@ -0,0 +1,29 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +// Emscripten MODULARIZE factory; calling it instantiates a fresh, isolated +// WASM instance with its own linear memory. +const createModule = require('../wasm/libpg-query.js'); + +// Guards the WASM load-time footprint from regressing. The module reserves its +// initial linear memory on instantiation, before any parse — that reservation +// is what inflates load-time memory. Growth is enabled (-sALLOW_MEMORY_GROWTH), +// so the initial size is a footprint knob, not a hard limit. We read the live +// size from the instance (HEAPU8 views the linear memory) instead of parsing +// the binary: no dependency, and it reflects the real allocation. +describe('WASM memory footprint', () => { + // Ceiling for the initial reservation; the single source of truth. Keep it a + // bit above -sINITIAL_MEMORY in the Makefile so normal tuning doesn't trip it. + const MAX_INITIAL_BYTES = 64 * 1024 * 1024; + + it('reserves an initial linear memory within budget', async () => { + const m = await createModule(); + const initialBytes = m.HEAPU8.buffer.byteLength; + const mib = (n) => (n / (1024 * 1024)).toFixed(0); + assert.ok( + initialBytes <= MAX_INITIAL_BYTES, + `WASM initial memory ${mib(initialBytes)} MiB exceeds the ${mib(MAX_INITIAL_BYTES)} MiB budget. ` + + `Lower -sINITIAL_MEMORY in the Makefile (memory grows on demand via -sALLOW_MEMORY_GROWTH).` + ); + }); +});