@pulse-compute/wasm-build-support 1.0.0-beta.5 → 1.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
  > **Supported entry points:** `None for application authors.`<br>
8
8
  > **Stability:** Internal release-set interface; exported modules may change with compiler implementation needs.<br>
9
9
  > **npm:** [`@pulse-compute/wasm-build-support`](https://www.npmjs.com/package/@pulse-compute/wasm-build-support)<br>
10
- > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/)
10
+ > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/)
11
11
  >
12
- > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.5` package policy.
12
+ > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.6` package policy.
13
13
  <!-- pulse-package-status:end -->
14
14
 
15
15
  Shared build-time helpers for PulseWasm package-owned builders.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-compute/wasm-build-support",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.6",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": "^22.14.0 || ^24.0.0"
@@ -16,7 +16,7 @@
16
16
  "./files": "./src/files.js"
17
17
  },
18
18
  "dependencies": {
19
- "@pulse-compute/wasm-contracts": "1.0.0-beta.5"
19
+ "@pulse-compute/wasm-contracts": "1.0.0-beta.6"
20
20
  },
21
21
  "publishConfig": {
22
22
  "access": "public"
@@ -32,7 +32,7 @@
32
32
  "url": "git+https://github.com/pulse-compute/pulse.git",
33
33
  "directory": "wasm/packages/build-support"
34
34
  },
35
- "homepage": "https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/",
35
+ "homepage": "https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/",
36
36
  "bugs": {
37
37
  "url": "https://github.com/pulse-compute/pulse/issues"
38
38
  }
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('node:path');
4
+
3
5
  const {
4
6
  NATIVE_OPTIMIZATION_VERSION,
5
- EXPERIMENTAL_NATIVE_SIZE_MODE
7
+ EXPERIMENTAL_NATIVE_SIZE_MODE,
8
+ EXPERIMENTAL_NATIVE_BOUNDED_SIZE_MODE
6
9
  } = require('@pulse-compute/wasm-contracts/project/native-optimization');
7
10
  const EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION = Object.freeze({
8
11
  version: NATIVE_OPTIMIZATION_VERSION,
@@ -17,6 +20,19 @@ const EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION = Object.freeze({
17
20
  })
18
21
  });
19
22
 
23
+ const EXPERIMENTAL_NATIVE_BOUNDED_SIZE_OPTIMIZATION = Object.freeze({
24
+ version: NATIVE_OPTIMIZATION_VERSION,
25
+ mode: EXPERIMENTAL_NATIVE_BOUNDED_SIZE_MODE,
26
+ experimental: true,
27
+ goal: 'size',
28
+ assemblyScript: Object.freeze({
29
+ optimize: true,
30
+ optimizeLevel: 3,
31
+ shrinkLevel: 2,
32
+ converge: false
33
+ })
34
+ });
35
+
20
36
  function resolveNativeOptimization(value) {
21
37
  if (value === undefined || value === null || value === false) return undefined;
22
38
  if (
@@ -26,11 +42,21 @@ function resolveNativeOptimization(value) {
26
42
  ) {
27
43
  return EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION;
28
44
  }
45
+ if (value === EXPERIMENTAL_NATIVE_BOUNDED_SIZE_MODE
46
+ || (typeof value === 'object' && value.mode === EXPERIMENTAL_NATIVE_BOUNDED_SIZE_MODE)) {
47
+ return EXPERIMENTAL_NATIVE_BOUNDED_SIZE_OPTIMIZATION;
48
+ }
29
49
  throw new TypeError(`Unsupported Pulse Native optimization mode ${String(value && value.mode || value)}.`);
30
50
  }
31
51
 
32
- function appendAssemblyScriptOptimizationArgs(args, value) {
52
+ function appendAssemblyScriptOptimizationArgs(args, value, options = {}) {
33
53
  if (!Array.isArray(args)) throw new TypeError('AssemblyScript optimization arguments require a mutable argument array.');
54
+ // AssemblyScript ignores custom @noinline annotations. Apply Pulse's generated
55
+ // annotations to Binaryen IR before its default passes in every profile.
56
+ args.push('--transform', path.join(__dirname, 'native-retention-transform.cjs'));
57
+ // Run merging after asc's optimizer has honored retained helper boundaries.
58
+ // Guest inputs are merged once, with the other units, by the guest-link owner.
59
+ if (!options.guestLinked) args.push('--runPasses', 'merge-similar-functions');
34
60
  const optimization = resolveNativeOptimization(value);
35
61
  if (!optimization) return undefined;
36
62
  args.push(
@@ -45,6 +71,8 @@ module.exports = Object.freeze({
45
71
  NATIVE_OPTIMIZATION_VERSION,
46
72
  EXPERIMENTAL_NATIVE_SIZE_MODE,
47
73
  EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION,
74
+ EXPERIMENTAL_NATIVE_BOUNDED_SIZE_MODE,
75
+ EXPERIMENTAL_NATIVE_BOUNDED_SIZE_OPTIMIZATION,
48
76
  resolveNativeOptimization,
49
77
  appendAssemblyScriptOptimizationArgs
50
78
  });
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ // asc injects its own pinned Binaryen instance into transform constructors.
4
+ // No additional optimizer instance, public decorator or dependency is needed.
5
+ const generatedEntries = new Set(['canonical-native.as', 'fastly-native-platform-capabilities.as']);
6
+ // Legacy numeric names remain supported for captured sources and fixtures.
7
+ // The private retained suffix is only a batching aid: it grants no retention
8
+ // without the existing compiler-owned source and @noinline annotation.
9
+ const generatedFunction = /^__pulse_(?:expr_\d+|ex_\d+\$[ik]|chunk_\d+|shared_stage_\d+|stage_(?:prepare|clear|ready|result)|step|route_error)$/;
10
+
11
+ function commonPrefixLength(left, right) {
12
+ let length = 0;
13
+ while (length < left.length && length < right.length && left[length] === right[length]) length++;
14
+ return length;
15
+ }
16
+
17
+ function retentionPatterns(module, binaryen, retainedNames) {
18
+ const selected = new Set(retainedNames.filter(name => module.getFunction(name)));
19
+ if (selected.size < 2) return [...selected];
20
+ const names = [];
21
+ for (let index = 0; index < module.getNumFunctions(); index++) {
22
+ names.push(binaryen.Function.getName(module.getFunctionByIndex(index)));
23
+ }
24
+ const patterns = new Set();
25
+ for (const entry of generatedEntries) {
26
+ const prefix = `${entry}/__pulse_ex_`, suffix = '$k';
27
+ const matches = names.filter(name => name.startsWith(prefix) && name.endsWith(suffix));
28
+ // Check every actual wildcard match, including unannotated functions and
29
+ // imports. A private-looking name never grants retention on its own.
30
+ if (matches.length > 1 && matches.every(name => selected.has(name))) {
31
+ patterns.add(`${prefix}*${suffix}`);
32
+ for (const name of matches) selected.delete(name);
33
+ }
34
+ }
35
+ names.sort();
36
+ for (let start = 0; start < names.length;) {
37
+ if (!selected.has(names[start])) { start++; continue; }
38
+ let end = start + 1;
39
+ while (end < names.length && selected.has(names[end])) end++;
40
+ // In lexical order, the nearest unselected neighbors bound every safe
41
+ // prefix in this run. Include all module functions, including imports and
42
+ // unannotated leaves: a wildcard must never broaden retention policy.
43
+ const before = names[start - 1] || '';
44
+ const after = names[end] || '';
45
+ for (let index = start; index < end; index++) {
46
+ const name = names[index];
47
+ const length = Math.max(
48
+ name.indexOf('/__pulse_') + '/__pulse_'.length,
49
+ commonPrefixLength(name, before) + 1,
50
+ commonPrefixLength(name, after) + 1
51
+ );
52
+ // An unselected name may extend the entire selected name (e.g. 1/10).
53
+ // In that case only an exact pattern is safe.
54
+ patterns.add(length > name.length ? name : `${name.slice(0, length)}*`);
55
+ }
56
+ start = end;
57
+ }
58
+ return [...patterns];
59
+ }
60
+
61
+ module.exports = class NativeRetentionTransform {
62
+ afterParse(parser) {
63
+ this.retainedNames = [];
64
+ for (const source of parser.sources) {
65
+ if (!generatedEntries.has(source.internalPath)) continue;
66
+ for (const declaration of source.statements) {
67
+ if (generatedFunction.test(declaration.name?.text || '')
68
+ && declaration.decorators?.some(decorator => decorator.name?.text === 'noinline')) {
69
+ this.retainedNames.push(`${source.internalPath}/${declaration.name.text}`);
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ afterCompile(module) {
76
+ // afterCompile precedes optimization; asc --runPasses runs too late.
77
+ const previous = this.binaryen.getPassArgument('no-inline');
78
+ try {
79
+ // Binaryen's no-inline pass scans the whole module for one wildcard.
80
+ // Batch only patterns whose existing matches are all already selected.
81
+ for (const pattern of retentionPatterns(module, this.binaryen, this.retainedNames)) {
82
+ this.binaryen.setPassArgument('no-inline', pattern);
83
+ module.runPasses(['no-inline']);
84
+ }
85
+ } finally {
86
+ this.binaryen.setPassArgument('no-inline', previous);
87
+ }
88
+ }
89
+ };