@theqrl/hypc 0.0.2 → 0.3.1

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.
Files changed (60) hide show
  1. package/README.md +62 -15
  2. package/abi.js +17 -7
  3. package/bindings/compile.js +158 -31
  4. package/bindings/core.js +22 -6
  5. package/bindings/helpers.js +3 -4
  6. package/bindings/index.js +13 -6
  7. package/common/helpers.js +17 -3
  8. package/downloadCurrentVersion.js +20 -14
  9. package/formatters.js +2 -3
  10. package/hypc.js +91 -35
  11. package/hypjson.js +110 -1732
  12. package/linker.js +63 -32
  13. package/package.json +28 -28
  14. package/smtchecker.js +3 -1
  15. package/smtsolver.js +44 -17
  16. package/translate.js +4 -4
  17. package/verifyVersion.js +17 -7
  18. package/wrapper.js +305 -30
  19. package/build/clean.js +0 -6
  20. package/build/pack-publish-block.js +0 -10
  21. package/build/postbuild.js +0 -3
  22. package/contractA_hyp_A.bin +0 -1
  23. package/contractB_hyp_B.bin +0 -1
  24. package/importA_hyp_C.bin +0 -1
  25. package/importB_hyp_D.bin +0 -1
  26. package/libX_hyp_X.bin +0 -1
  27. package/libY_hyp_Y.bin +0 -1
  28. package/libZ_hyp_Z.bin +0 -1
  29. package/test/abi.js +0 -77
  30. package/test/cli.js +0 -245
  31. package/test/compiler.js +0 -791
  32. package/test/index.js +0 -36
  33. package/test/linker.js +0 -132
  34. package/test/resources/fixtureAsmJson.hyp +0 -8
  35. package/test/resources/fixtureAsmJson.json +0 -528
  36. package/test/resources/fixtureAsmJson.output +0 -101
  37. package/test/resources/fixtureIncorrectSource.hyp +0 -1
  38. package/test/resources/fixtureSmoke.hyp +0 -5
  39. package/test/resources/importA.hyp +0 -7
  40. package/test/resources/importB.hyp +0 -5
  41. package/test/resources/importCallback/base/contractA.hyp +0 -8
  42. package/test/resources/importCallback/base/contractB.hyp +0 -6
  43. package/test/resources/importCallback/contractC.hyp +0 -4
  44. package/test/resources/importCallback/includeA/libX.hyp +0 -4
  45. package/test/resources/importCallback/includeA/libY.hyp +0 -6
  46. package/test/resources/importCallback/includeA/utils.hyp +0 -4
  47. package/test/resources/importCallback/includeB/libZ.hyp +0 -4
  48. package/test/resources/smtChecker/loop.hyp +0 -8
  49. package/test/resources/smtChecker/smoke.hyp +0 -5
  50. package/test/resources/smtChecker/smoke_with_engine.hyp +0 -6
  51. package/test/resources/smtChecker/smoke_with_multi_engine.hyp +0 -8
  52. package/test/smtcallback.js +0 -221
  53. package/test/smtchecker.js +0 -83
  54. package/test/translate.js +0 -83
  55. package/test_resources_fixtureSmoke_hyp_C.abi +0 -1
  56. package/test_resources_fixtureSmoke_hyp_C.bin +0 -1
  57. package/test_resources_importA_hyp_C.bin +0 -1
  58. package/test_resources_importB_hyp_D.bin +0 -1
  59. package/tsconfig.json +0 -25
  60. package/utils_hyp_Utils.bin +0 -1
package/README.md CHANGED
@@ -12,6 +12,8 @@ To use the latest stable version of the Hyperion compiler via Node.js you can in
12
12
  npm install @theqrl/hypc
13
13
  ```
14
14
 
15
+ It needs Node.js 22.12 or later.
16
+
15
17
  ### Usage on the Command-Line
16
18
 
17
19
  If this package is installed globally (`npm install -g @theqrl/hypc`), a command-line tool called `hypcjs` will be available.
@@ -30,6 +32,14 @@ Use the ``--base-path`` and ``--include-path`` options to describe the layout of
30
32
  ``--base-path`` represents the root of your own source tree while ``--include-path`` allows you to
31
33
  specify extra locations containing external code (e.g. libraries installed with a package manager).
32
34
 
35
+ As in `hypc`, the import callback only reads files inside the base path (the working directory
36
+ when ``--base-path`` is not given), the include paths and the directories of the files given on
37
+ the command line. Symbolic links are resolved first. Any other import fails with
38
+ `File outside of allowed directories.`
39
+ If the working directory is the root of the filesystem (or of a drive on Windows) and
40
+ ``--base-path`` is not given, the working directory is not allowed, since every readable file
41
+ would then be importable. Pass ``--base-path`` explicitly if you really need it.
42
+
33
43
  Note: ensure that all the files you specify on the command line are located inside the base path or
34
44
  one of the include paths.
35
45
  The compiler refers to files from outside of these directories using absolute paths.
@@ -37,7 +47,7 @@ Having absolute paths in contract metadata will result in your bytecode being re
37
47
  when it's placed in these exact absolute locations.
38
48
 
39
49
  Note: this commandline interface is not compatible with `hypc` provided by the Hyperion compiler package and thus cannot be
40
- used in combination with an Zond client via the `zond.compile.hyperion()` RPC method. Please refer to the
50
+ used in combination with a QRL client via the `qrl.compile.hyperion()` RPC method. Please refer to the
41
51
  [Hyperion compiler documentation](https://theqrl.org/) for instructions to install `hypc`.
42
52
  Furthermore, the commandline interface to hypc-js provides fewer features than the binary release.
43
53
 
@@ -61,6 +71,11 @@ as a string. It cannot be used together with callback-based, asynchronous,
61
71
  filesystem access. A workaround is to collect the names of dependencies, return
62
72
  an error, and keep re-running the compiler until all of them are resolved.
63
73
 
74
+ A callback that throws, or that returns anything but an object with a string ``contents`` or a
75
+ string ``error``, does not abort the compilation: the compiler reports it as the import error, for
76
+ example ``Source "a.hyp" not found: Callback threw Error: ENOENT: no such file or directory``. An
77
+ ``error`` that is not a string, such as an ``Error`` object, is reported as the error it describes.
78
+
64
79
  #### Example usage without the import callback
65
80
 
66
81
  Example:
@@ -91,7 +106,7 @@ for (var contractName in output.contracts['test.hyp']) {
91
106
  console.log(
92
107
  contractName +
93
108
  ': ' +
94
- output.contracts['test.hyp'][contractName].evm.bytecode.object
109
+ output.contracts['test.hyp'][contractName].qrvm.bytecode.object
95
110
  );
96
111
  }
97
112
  ```
@@ -136,7 +151,7 @@ for (var contractName in output.contracts['test.hyp']) {
136
151
  console.log(
137
152
  contractName +
138
153
  ': ' +
139
- output.contracts['test.hyp'][contractName].evm.bytecode.object
154
+ output.contracts['test.hyp'][contractName].qrvm.bytecode.object
140
155
  );
141
156
  }
142
157
  ```
@@ -149,7 +164,7 @@ return either an error or the result from the solver. A default
149
164
  ``smtchecker.js`` which exports the ``smtCallback`` function that takes 1) a
150
165
  function that takes queries and returns the solving result, and 2) a solver
151
166
  configuration object. The module ``smtsolver.js`` has a few predefined solver
152
- configurations, and relies on Z3, Eldarica or cvc5 being installed locally. It
167
+ configurations, and relies on Z3, Eldarica, cvc5 or cvc4 being installed locally. It
153
168
  exports the list of locally found solvers and a function that invokes a given
154
169
  solver.
155
170
 
@@ -190,6 +205,18 @@ var output = JSON.parse(
190
205
  The assertion is clearly false, and an ``assertion failure`` warning
191
206
  should be returned, together with a counterexample.
192
207
 
208
+ #### When an exception unwinds through the compiler
209
+
210
+ `compile` throws when a JavaScript exception unwinds through the compiler itself, for example the
211
+ `RangeError` of a stack overflow on deeply nested source. The compiler's cleanup does not run then,
212
+ so the module instance the exception unwound through cannot compile again, and the next call to
213
+ `compile` loads a fresh instance of the module, or, where none can be loaded, goes on with the same
214
+ instance once the compiler has discarded the abandoned compilation. This needs nothing from the
215
+ caller for the bundled compiler and for one from `loadRemoteVersion`; for a module given to
216
+ `setupMethods`, see [Using a Legacy Version](#using-a-legacy-version). Going on with the same
217
+ instance keeps the memory of the abandoned compilation allocated, since the compiler cannot free
218
+ what the exception interrupted; a fresh instance does not.
219
+
193
220
  #### Low-level API
194
221
 
195
222
  The low-level API is as follows:
@@ -228,33 +255,50 @@ In order to compile contracts using a specific version of Hyperion, the `hypc.lo
228
255
  You can also load the "binary" manually and use `setupMethods` to create the familiar wrapper functions described above:
229
256
  `var hypc = hypc.setupMethods(require("/my/local/hypjson.js"))`.
230
257
 
231
- ### Using the Latest Development Snapshot
258
+ When an exception unwinds through such a module (see above), `setupMethods` loads a fresh instance
259
+ from the module's file if `require` loaded it. Otherwise pass a function that returns a fresh
260
+ module as the second argument, `hypc.setupMethods(hypjson, loadHypjson)`. Without one, as in a
261
+ browser, the same instance goes on after `hyperion_reset()` has discarded the abandoned
262
+ compilation, which compilers from 0.3.0 on support; with an older compiler, or after a WebAssembly
263
+ trap, the next `compile` returns an error that says the module cannot be used again. Each
264
+ compilation the same instance goes on from leaves its memory allocated in the instance, so a
265
+ long-lived page that sees `compile` throw more than rarely should load a fresh module itself.
266
+
267
+ ### Using the Latest Release
232
268
 
233
- By default, the npm version is only created for releases. This prevents people from deploying contracts with non-release versions because they are less stable and harder to verify. If you would like to use the latest development snapshot (at your own risk!), you may use the following example code.
269
+ `loadRemoteVersion('latest', …)` loads the latest release in the
270
+ [release list](https://binaries.theqrl.org/bin/list.json), which can be newer than the compiler that
271
+ your installed copy of this package bundles.
234
272
 
235
273
  ```javascript
236
274
  var hypc = require('@theqrl/hypc');
237
275
 
238
- // getting the development snapshot
239
- hypc.loadRemoteVersion('latest', function(err, hypcSnapshot) {
276
+ // getting the latest release
277
+ hypc.loadRemoteVersion('latest', function(err, hypcLatest) {
240
278
  if (err) {
241
279
  // An error was encountered, display and quit
242
280
  } else {
243
- // NOTE: Use `hypcSnapshot` here with the same interface `hypc` has
281
+ // NOTE: Use `hypcLatest` here with the same interface `hypc` has
244
282
  // For example:
245
- const output = hypcSnapshot.compile(/* ... */)
283
+ const output = hypcLatest.compile(/* ... */)
246
284
  }
247
285
  });
248
286
  ```
249
287
 
250
- The version **must** be in the long format.
251
- Thus, if you would like to use version `v0.8.17` you need to include the commit hash of the release.
252
- You can extract the long version string for each version from the [publicly available release list](https://binaries.theqrl.org/bin/list.json).
288
+ The version is `latest`, a release such as `0.2.0`, or a build in the long format, which includes
289
+ the commit hash, such as `v0.2.0+commit.e36a0aea`. The [publicly available release list](https://binaries.theqrl.org/bin/list.json)
290
+ has every version.
253
291
 
254
292
  ```javascript
255
- hypc.loadRemoteVersion('v0.8.17+commit.8df45f5f', function(err, hypcSnapshot) { /* ... */ });
293
+ hypc.loadRemoteVersion('v0.2.0+commit.e36a0aea', function(err, hypcRemote) { /* ... */ });
256
294
  ```
257
295
 
296
+ `loadRemoteVersion` looks the version up in that list and downloads the build from
297
+ `binaries.theqrl.org`. It runs the build only if its keccak256 and sha256 match the list, and
298
+ otherwise passes an error to the callback. It follows redirects only to `https` URLs on the same
299
+ host, and stops reading a build larger than 64 MiB. A build that fails to load also reaches the
300
+ callback as an error.
301
+
258
302
  ### Linking Bytecode
259
303
 
260
304
  When using libraries, the resulting bytecode will contain placeholders for the real addresses of the referenced libraries. These have to be updated, via a process called linking, before deploying the contract.
@@ -266,9 +310,12 @@ The `linkBytecode` method provides a simple helper for linking:
266
310
  ```javascript
267
311
  var linker = require('@theqrl/hypc/linker');
268
312
 
269
- bytecode = linker.linkBytecode(bytecode, { MyLibrary: '0x123456...' });
313
+ bytecode = linker.linkBytecode(bytecode, { MyLibrary: 'Q123456...' });
270
314
  ```
271
315
 
316
+ An address is `Q` followed by 128 hex digits. As in the compiler's `settings.libraries`, an address
317
+ in mixed case must carry the address checksum; one in all lower case or all upper case does not.
318
+
272
319
  As of Hyperion 0.4.11 the compiler supports [standard JSON input and output](https://theqrl.org) which outputs a _link references_ map. This gives a map of library names to offsets in the bytecode to replace the addresses at. It also doesn't have the limitation on library file and contract name lengths.
273
320
 
274
321
  There is a method available in the `linker` module called `findLinkReferences` which can find such link references in bytecode produced by an older compiler:
package/abi.js CHANGED
@@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
25
35
  const semver = __importStar(require("semver"));
26
36
  function update(compilerVersion, abi) {
27
37
  let hasConstructor = false;
@@ -3,19 +3,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.setupCompile = void 0;
6
+ exports.setupCompile = setupCompile;
7
7
  const assert_1 = __importDefault(require("assert"));
8
8
  const helpers_1 = require("../common/helpers");
9
9
  const helpers_2 = require("./helpers");
10
- function setupCompile(hypJson, core) {
10
+ /**
11
+ * @param hypJson The Emscripten compiled Hyperion object.
12
+ * @param core The core bound Hyperion methods.
13
+ * @param onEscapedException Called with the exception when a JavaScript exception has unwound
14
+ * through the compiler's wasm frames; see `guarded` below.
15
+ */
16
+ function setupCompile(hypJson, core, onEscapedException = () => { }) {
17
+ const guard = (method) => guarded(method, hypJson, onEscapedException);
11
18
  return {
12
- compileJson: bindCompileJson(hypJson),
13
- compileJsonCallback: bindCompileJsonCallback(hypJson, core),
14
- compileJsonMulti: bindCompileJsonMulti(hypJson),
15
- compileStandard: bindCompileStandard(hypJson, core)
19
+ compileJson: bindCompileJson(hypJson, guard),
20
+ compileJsonCallback: bindCompileJsonCallback(hypJson, core, guard),
21
+ compileJsonMulti: bindCompileJsonMulti(hypJson, guard),
22
+ compileStandard: bindCompileStandard(hypJson, core, guard)
23
+ };
24
+ }
25
+ /**
26
+ * Wraps a bound compiler method so that an exception which unwinds through the compiler's wasm
27
+ * frames is reported before it propagates.
28
+ *
29
+ * The compiler is built with Emscripten's JavaScript exception handling: a C++ throw is a
30
+ * JavaScript throw of a number, which the `invoke_*` trampolines catch at every C++ try block so
31
+ * that landing pads (destructors, catch clauses) run. Any other JavaScript exception, such as the
32
+ * RangeError V8 raises when deeply recursive compiler code runs out of stack, or an exception
33
+ * thrown by an import callback, is rethrown by those trampolines without running the landing
34
+ * pads. The compiler's globals then stay as they were mid-compilation (the CompilerStack
35
+ * singleton guard is still taken, TypeProvider is not reset), and every later compilation on this
36
+ * module instance fails with "You shall not have another CompilerStack aside me." The wrapper
37
+ * (wrapper.ts) loads a fresh instance of the module in that case, or asks the compiler to discard
38
+ * the abandoned compilation where it cannot.
39
+ *
40
+ * Emscripten also restores the wasm stack pointer at every C++ try block such an exception passes,
41
+ * but not at the entry point, so the pointer is left below where the call started with nothing but
42
+ * dead frames above it. It is put back where it was, when the build exposes stackSave/stackRestore.
43
+ */
44
+ function guarded(method, hypJson, onEscapedException) {
45
+ if ((0, helpers_1.isNil)(method))
46
+ return method;
47
+ return function (...args) {
48
+ const stackPointer = typeof hypJson.stackSave === 'function' ? hypJson.stackSave() : undefined;
49
+ try {
50
+ return method(...args);
51
+ }
52
+ catch (e) {
53
+ if (stackPointer !== undefined && typeof hypJson.stackRestore === 'function') {
54
+ try {
55
+ hypJson.stackRestore(stackPointer);
56
+ }
57
+ catch (restoreError) {
58
+ // Then the instance is beyond help; the wrapper will not go on with it.
59
+ }
60
+ }
61
+ onEscapedException(e);
62
+ throw e;
63
+ }
16
64
  };
17
65
  }
18
- exports.setupCompile = setupCompile;
19
66
  /**********************
20
67
  * COMPILE
21
68
  **********************/
@@ -24,18 +71,20 @@ exports.setupCompile = setupCompile;
24
71
  * input (text), optimize (bool) -> output (jsontext)
25
72
  *
26
73
  * @param hypJson The Emscripten compiled Hyperion object.
74
+ * @param guard See `guarded`.
27
75
  */
28
- function bindCompileJson(hypJson) {
29
- return (0, helpers_2.bindHypcMethod)(hypJson, 'compileJSON', 'string', ['string', 'number'], null);
76
+ function bindCompileJson(hypJson, guard) {
77
+ return guard((0, helpers_2.bindHypcMethod)(hypJson, 'compileJSON', 'string', ['string', 'number'], null));
30
78
  }
31
79
  /**
32
80
  * Returns a binding to the hyperion compileJSONMulti method.
33
81
  * input (jsontext), optimize (bool) -> output (jsontext)
34
82
  *
35
83
  * @param hypJson The Emscripten compiled Hyperion object.
84
+ * @param guard See `guarded`.
36
85
  */
37
- function bindCompileJsonMulti(hypJson) {
38
- return (0, helpers_2.bindHypcMethod)(hypJson, 'compileJSONMulti', 'string', ['string', 'number'], null);
86
+ function bindCompileJsonMulti(hypJson, guard) {
87
+ return guard((0, helpers_2.bindHypcMethod)(hypJson, 'compileJSONMulti', 'string', ['string', 'number'], null));
39
88
  }
40
89
  /**
41
90
  * Returns a binding to the hyperion compileJSONCallback method.
@@ -43,13 +92,14 @@ function bindCompileJsonMulti(hypJson) {
43
92
  *
44
93
  * @param hypJson The Emscripten compiled Hyperion object.
45
94
  * @param coreBindings The core bound Hyperion methods.
95
+ * @param guard See `guarded`.
46
96
  */
47
- function bindCompileJsonCallback(hypJson, coreBindings) {
48
- const compileInternal = (0, helpers_2.bindHypcMethod)(hypJson, 'compileJSONCallback', 'string', ['string', 'number', 'number'], null);
97
+ function bindCompileJsonCallback(hypJson, coreBindings, guard) {
98
+ const compileInternal = guard((0, helpers_2.bindHypcMethod)(hypJson, 'compileJSONCallback', 'string', ['string', 'number', 'number'], null));
49
99
  if ((0, helpers_1.isNil)(compileInternal))
50
100
  return null;
51
101
  return function (input, optimize, readCallback) {
52
- return runWithCallbacks(hypJson, coreBindings, readCallback, compileInternal, [input, optimize]);
102
+ return runWithCallbacks(hypJson, coreBindings, readCallback, compileInternal, () => [input, optimize]);
53
103
  };
54
104
  }
55
105
  /**
@@ -59,22 +109,35 @@ function bindCompileJsonCallback(hypJson, coreBindings) {
59
109
  *
60
110
  * @param hypJson The Emscripten compiled Hyperion object.
61
111
  * @param coreBindings The core bound Hyperion methods.
112
+ * @param guard See `guarded`.
62
113
  */
63
- function bindCompileStandard(hypJson, coreBindings) {
114
+ function bindCompileStandard(hypJson, coreBindings, guard) {
64
115
  let boundFunctionStandard = null;
65
116
  let boundFunctionHyperion = null;
66
117
  // input (jsontext), callback (ptr) -> output (jsontext)
67
- const compileInternal = (0, helpers_2.bindHypcMethod)(hypJson, 'compileStandard', 'string', ['string', 'number'], null);
68
- // input (jsontext), callback (ptr), callback_context (ptr) -> output (jsontext)
69
- boundFunctionHyperion = (0, helpers_2.bindHypcMethod)(hypJson, 'hyperion_compile', 'string', ['string', 'number', 'number'], null);
118
+ const compileInternal = guard((0, helpers_2.bindHypcMethod)(hypJson, 'compileStandard', 'string', ['string', 'number'], null));
119
+ // input (ptr to text), callback (ptr), callback_context (ptr) -> output (jsontext)
120
+ //
121
+ // The input is passed as a pointer to a copy made with hyperion_alloc(), not as a 'string'
122
+ // argument: Emscripten's ccall copies a 'string' argument onto the wasm stack, whose size is
123
+ // fixed when the compiler is built (5 MB), so an input of more than about 1.3 million
124
+ // characters overflows it and the compilation crashes instead of running. The copy is freed by
125
+ // the reset() that runWithCallbacks makes after each compilation, like every other allocation
126
+ // made for the compiler. The output stays a 'string': it is read from the compiler's memory and
127
+ // freed by the same reset().
128
+ boundFunctionHyperion = guard((0, helpers_2.bindHypcMethod)(hypJson, 'hyperion_compile', 'string', ['number', 'number', 'number'], null));
70
129
  if (!(0, helpers_1.isNil)(compileInternal)) {
71
130
  boundFunctionStandard = function (input, readCallback) {
72
- return runWithCallbacks(hypJson, coreBindings, readCallback, compileInternal, [input]);
131
+ return runWithCallbacks(hypJson, coreBindings, readCallback, compileInternal, () => [input]);
73
132
  };
74
133
  }
75
134
  if (!(0, helpers_1.isNil)(boundFunctionHyperion)) {
76
135
  boundFunctionStandard = function (input, callbacks) {
77
- return runWithCallbacks(hypJson, coreBindings, callbacks, boundFunctionHyperion, [input]);
136
+ // The copy is made right before the guarded call, once the callback is set up, so that
137
+ // nothing that can fail runs between the copy and the compilation whose reset() frees it.
138
+ // It is still outside the guarded call: a failed allocation leaves nothing behind in the
139
+ // compiler, so it must not mark the instance poisoned.
140
+ return runWithCallbacks(hypJson, coreBindings, callbacks, boundFunctionHyperion, () => [coreBindings.allocCString(input)]);
78
141
  };
79
142
  }
80
143
  return boundFunctionStandard;
@@ -87,17 +150,83 @@ function wrapCallbackWithKind(coreBindings, callback) {
87
150
  return function (context, kind, data, contents, error) {
88
151
  // Must be a null pointer.
89
152
  (0, assert_1.default)(context === 0, 'Callback context must be null.');
90
- const result = callback(coreBindings.copyFromCString(kind), coreBindings.copyFromCString(data));
91
- if (typeof result.contents === 'string') {
92
- coreBindings.copyToCString(result.contents, contents);
153
+ // The compiler calls this from inside its wasm frames. An exception thrown here would unwind
154
+ // through them without running the compiler's cleanup and leave the module instance unusable
155
+ // (see `guarded`), so a callback that throws, or returns no usable object, is answered with an
156
+ // error instead, which the compiler reports like any other failed import or query.
157
+ let contentsString;
158
+ let errorString;
159
+ try {
160
+ const result = callback(coreBindings.copyFromCString(kind), coreBindings.copyFromCString(data));
161
+ ({ contents: contentsString, error: errorString } = callbackResultStrings(result));
93
162
  }
94
- if (typeof result.error === 'string') {
95
- coreBindings.copyToCString(result.error, error);
163
+ catch (e) {
164
+ contentsString = undefined;
165
+ errorString = `Callback threw ${(0, helpers_1.describeException)(e)}`;
166
+ }
167
+ if (contentsString !== undefined) {
168
+ coreBindings.copyToCString(contentsString, contents);
169
+ }
170
+ if (errorString !== undefined) {
171
+ coreBindings.copyToCString(errorString, error);
96
172
  }
97
173
  };
98
174
  }
99
- // calls compile() with args || cb
100
- function runWithCallbacks(hypJson, coreBindings, callbacks, compile, args) {
175
+ /**
176
+ * The strings to hand the compiler for what a callback returned: its `contents` and `error` where
177
+ * they are strings, otherwise an error that says what was returned. The compiler must get one of
178
+ * the two for every result: with neither, it reports "Callback not supported.", which hides that
179
+ * a callback was there and answered badly. An `error` that is not a string, such as an Error
180
+ * object, is reported as the error it describes.
181
+ *
182
+ * @param result What the callback returned.
183
+ */
184
+ function callbackResultStrings(result) {
185
+ if ((0, helpers_1.isNil)(result) || !(0, helpers_1.isObject)(result)) {
186
+ return { error: `Callback returned ${describeValue(result)} instead of an object with contents or error.` };
187
+ }
188
+ const strings = {};
189
+ if (typeof result.contents === 'string') {
190
+ strings.contents = result.contents;
191
+ }
192
+ if (typeof result.error === 'string') {
193
+ strings.error = result.error;
194
+ }
195
+ else if (!(0, helpers_1.isNil)(result.error)) {
196
+ strings.error = (0, helpers_1.describeException)(result.error);
197
+ }
198
+ if (strings.contents === undefined && strings.error === undefined) {
199
+ strings.error = (0, helpers_1.isNil)(result.contents)
200
+ ? 'Callback returned an object with neither contents nor error.'
201
+ : `Callback returned ${describeValue(result.contents)} as contents instead of a string.`;
202
+ }
203
+ return strings;
204
+ }
205
+ /**
206
+ * Names the kind of a value for an error message: `null`, `undefined`, `an array`, `a string`...
207
+ *
208
+ * @param value
209
+ */
210
+ function describeValue(value) {
211
+ if ((0, helpers_1.isNil)(value))
212
+ return String(value);
213
+ if (Array.isArray(value))
214
+ return 'an array';
215
+ return `${typeof value === 'object' ? 'an' : 'a'} ${typeof value}`;
216
+ }
217
+ /**
218
+ * Runs a compile method with the given callbacks installed, and frees what the compilation
219
+ * allocated afterwards.
220
+ *
221
+ * @param hypJson The Emscripten compiled Hyperion object.
222
+ * @param coreBindings The core bound Hyperion methods.
223
+ * @param callbacks The callbacks object (or, before 0.6.0, the import callback) given by the caller.
224
+ * @param compile The guarded compile method.
225
+ * @param prepareArgs Returns the arguments that precede the callback and its context. It runs once
226
+ * the callback is installed, right before the call, so that an argument allocated in the
227
+ * compiler for the call cannot be left behind by a failure to install the callback.
228
+ */
229
+ function runWithCallbacks(hypJson, coreBindings, callbacks, compile, prepareArgs) {
101
230
  if (callbacks) {
102
231
  (0, assert_1.default)(typeof callbacks === 'object', 'Invalid callback object specified.');
103
232
  }
@@ -137,10 +266,8 @@ function runWithCallbacks(hypJson, coreBindings, callbacks, compile, args) {
137
266
  const cb = coreBindings.addFunction(singleCallback, 'viiiii');
138
267
  let output;
139
268
  try {
140
- args.push(cb);
141
- // Callback context.
142
- args.push(null);
143
- output = compile(...args);
269
+ // The callback, then its context.
270
+ output = compile(...prepareArgs(), cb, null);
144
271
  }
145
272
  finally {
146
273
  coreBindings.removeFunction(cb);
package/bindings/core.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.setupCore = void 0;
6
+ exports.setupCore = setupCore;
7
7
  const helpers_1 = require("./helpers");
8
8
  const translate_1 = __importDefault(require("../translate"));
9
9
  const helpers_2 = require("../common/helpers");
@@ -19,6 +19,7 @@ function setupCore(hypJson) {
19
19
  removeFunction: unboundRemoveFunction.bind(this, hypJson),
20
20
  copyFromCString: unboundCopyFromCString.bind(this, hypJson),
21
21
  copyToCString: unboundCopyToCString.bind(this, hypJson, core.alloc),
22
+ allocCString: unboundAllocCString.bind(this, hypJson, core.alloc),
22
23
  // @ts-ignore
23
24
  versionToSemver: versionToSemver(core.version())
24
25
  };
@@ -27,7 +28,6 @@ function setupCore(hypJson) {
27
28
  ...helpers
28
29
  };
29
30
  }
30
- exports.setupCore = setupCore;
31
31
  /**********************
32
32
  * Core Functions
33
33
  **********************/
@@ -81,7 +81,7 @@ function bindReset(hypJson) {
81
81
  /**
82
82
  * Copy to a C string.
83
83
  *
84
- * Allocates memory using hypc's allocator.
84
+ * Allocates memory using hypc's allocator and returns the address of the copy.
85
85
  *
86
86
  * After 0.6.0:
87
87
  * The duty is on hypc-js to free these pointers. We accomplish that by calling `reset` at the end.
@@ -89,13 +89,29 @@ function bindReset(hypJson) {
89
89
  * @param hypJson The Emscripten compiled Hyperion object.
90
90
  * @param alloc The memory allocation function.
91
91
  * @param str The source string being copied to a C string.
92
- * @param ptr The pointer location where the C string will be set.
93
92
  */
94
- function unboundCopyToCString(hypJson, alloc, str, ptr) {
93
+ function unboundAllocCString(hypJson, alloc, str) {
95
94
  const length = hypJson.lengthBytesUTF8(str);
96
95
  const buffer = alloc(length + 1);
96
+ if (!buffer) {
97
+ // Writing through a NULL pointer would overwrite wasm memory at address 0.
98
+ throw new Error(`hyperion_alloc failed to allocate ${length + 1} bytes.`);
99
+ }
97
100
  hypJson.stringToUTF8(str, buffer, length + 1);
98
- hypJson.setValue(ptr, buffer, '*');
101
+ return buffer;
102
+ }
103
+ /**
104
+ * Copy to a C string, and store its address through a pointer.
105
+ *
106
+ * Allocates memory as `unboundAllocCString` does.
107
+ *
108
+ * @param hypJson The Emscripten compiled Hyperion object.
109
+ * @param alloc The memory allocation function.
110
+ * @param str The source string being copied to a C string.
111
+ * @param ptr The pointer location where the C string will be set.
112
+ */
113
+ function unboundCopyToCString(hypJson, alloc, str, ptr) {
114
+ hypJson.setValue(ptr, unboundAllocCString(hypJson, alloc, str), '*');
99
115
  }
100
116
  /**
101
117
  * Wrapper over Emscripten's C String copying function (which can be different
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSupportedMethods = exports.bindHypcMethodWithFallbackFunc = exports.bindHypcMethod = void 0;
3
+ exports.bindHypcMethod = bindHypcMethod;
4
+ exports.bindHypcMethodWithFallbackFunc = bindHypcMethodWithFallbackFunc;
5
+ exports.getSupportedMethods = getSupportedMethods;
4
6
  const helpers_1 = require("../common/helpers");
5
7
  function bindHypcMethod(hypJson, method, returnType, args, defaultValue) {
6
8
  if ((0, helpers_1.isNil)(hypJson[`_${method}`]) && defaultValue !== undefined) {
@@ -8,7 +10,6 @@ function bindHypcMethod(hypJson, method, returnType, args, defaultValue) {
8
10
  }
9
11
  return hypJson.cwrap(method, returnType, args);
10
12
  }
11
- exports.bindHypcMethod = bindHypcMethod;
12
13
  function bindHypcMethodWithFallbackFunc(hypJson, method, returnType, args, fallbackMethod, finalFallback = undefined) {
13
14
  const methodFunc = bindHypcMethod(hypJson, method, returnType, args, null);
14
15
  if (!(0, helpers_1.isNil)(methodFunc)) {
@@ -16,7 +17,6 @@ function bindHypcMethodWithFallbackFunc(hypJson, method, returnType, args, fallb
16
17
  }
17
18
  return bindHypcMethod(hypJson, fallbackMethod, returnType, args, finalFallback);
18
19
  }
19
- exports.bindHypcMethodWithFallbackFunc = bindHypcMethodWithFallbackFunc;
20
20
  function getSupportedMethods(hypJson) {
21
21
  return {
22
22
  licenseSupported: anyMethodExists(hypJson, 'hyperion_license'),
@@ -29,7 +29,6 @@ function getSupportedMethods(hypJson) {
29
29
  compileJsonStandardSupported: anyMethodExists(hypJson, 'compileStandard', 'hyperion_compile')
30
30
  };
31
31
  }
32
- exports.getSupportedMethods = getSupportedMethods;
33
32
  function anyMethodExists(hypJson, ...names) {
34
33
  return names.some(name => !(0, helpers_1.isNil)(hypJson[`_${name}`]));
35
34
  }
package/bindings/index.js CHANGED
@@ -1,16 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = setupBindings;
3
4
  const core_1 = require("./core");
4
5
  const helpers_1 = require("./helpers");
5
6
  const compile_1 = require("./compile");
6
7
  function setupBindings(hypJson) {
7
8
  const coreBindings = (0, core_1.setupCore)(hypJson);
8
- const compileBindings = (0, compile_1.setupCompile)(hypJson, coreBindings);
9
- const methodFlags = (0, helpers_1.getSupportedMethods)(hypJson);
10
- return {
11
- methodFlags,
9
+ const bindings = {
10
+ methodFlags: (0, helpers_1.getSupportedMethods)(hypJson),
12
11
  coreBindings,
13
- compileBindings
12
+ compileBindings: null,
13
+ // Set, with the exception, once a JavaScript exception has unwound through the compiler's wasm
14
+ // frames. This module instance cannot compile after that; see `guarded` in ./compile.ts.
15
+ poisoned: false,
16
+ poisonedBy: undefined
14
17
  };
18
+ bindings.compileBindings = (0, compile_1.setupCompile)(hypJson, coreBindings, (e) => {
19
+ bindings.poisoned = true;
20
+ bindings.poisonedBy = e;
21
+ });
22
+ return bindings;
15
23
  }
16
- exports.default = setupBindings;
package/common/helpers.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isObject = exports.isNil = void 0;
3
+ exports.isNil = isNil;
4
+ exports.isObject = isObject;
5
+ exports.describeException = describeException;
4
6
  /**
5
7
  * Returns true if and only if the value is null or undefined.
6
8
  *
@@ -10,7 +12,6 @@ function isNil(value) {
10
12
  // Uses == over === which compares both null and undefined.
11
13
  return value == null;
12
14
  }
13
- exports.isNil = isNil;
14
15
  /**
15
16
  * Returns true if and only if the value is an object and not an array.
16
17
  *
@@ -21,4 +22,17 @@ function isObject(value) {
21
22
  // to confirm it's just an object.
22
23
  return typeof value === 'object' && !Array.isArray(value);
23
24
  }
24
- exports.isObject = isObject;
25
+ /**
26
+ * Returns the exception as a string for an error message, such as `Error: boom` or
27
+ * `RangeError: Maximum call stack size exceeded`.
28
+ *
29
+ * @param e The thrown value.
30
+ */
31
+ function describeException(e) {
32
+ try {
33
+ return String(e);
34
+ }
35
+ catch (conversionError) {
36
+ return 'an exception that cannot be converted to a string';
37
+ }
38
+ }