WebAssembly JS API: implement more sections
[WebKit-https.git] / JSTests / wasm / Builder.js
1 /*
2  * Copyright (C) 2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 import * as assert from 'assert.js';
27 import * as BuildWebAssembly from 'Builder_WebAssemblyBinary.js';
28 import * as WASM from 'WASM.js';
29
30 const _toJavaScriptName = name => {
31     const camelCase = name.replace(/([^a-z0-9].)/g, c => c[1].toUpperCase());
32     const CamelCase = camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
33     return CamelCase;
34 };
35
36 const _isValidValue = (value, type) => {
37     switch (type) {
38     case "i32": return ((value & 0xFFFFFFFF) >>> 0) === value;
39     case "i64": throw new Error(`Unimplemented: value check for ${type}`); // FIXME https://bugs.webkit.org/show_bug.cgi?id=163420 64-bit values
40     case "f32": return typeof(value) === "number" && isFinite(value);
41     case "f64": return typeof(value) === "number" && isFinite(value);
42     default: throw new Error(`Implementation problem: unknown type ${type}`);
43     }
44 };
45 const _unknownSectionId = 0;
46
47 const _normalizeFunctionSignature = (params, ret) => {
48     assert.isArray(params);
49     for (const p of params)
50         assert.truthy(WASM.isValidValueType(p), `Type parameter ${p} needs a valid value type`);
51     if (typeof(ret) === "undefined")
52         ret = "void";
53     assert.isNotArray(ret, `Multiple return values not supported by WebAssembly yet`);
54     assert.falsy(ret !== "void" && !WASM.isValidValueType(ret), `Type return ${ret} must be valid value type`);
55     return [params, ret];
56 };
57
58 const _maybeRegisterType = (builder, type) => {
59     const typeSection = builder._getSection("Type");
60     if (typeof(type) === "number") {
61         // Type numbers already refer to the type section, no need to register them.
62         if (builder._checked) {
63             assert.isNotUndef(typeSection, `Can't use type ${type} if a type section isn't present`);
64             assert.isNotUndef(typeSection.data[type], `Type ${type} doesn't exist in type section`);
65         }
66         return type;
67     }
68     assert.hasObjectProperty(type, "params", `Expected type to be a number or object with 'params' and optionally 'ret' fields`);
69     const [params, ret] = _normalizeFunctionSignature(type.params, type.ret);
70     assert.isNotUndef(typeSection, `Can't add type if a type section isn't present`);
71     // Try reusing an equivalent type from the type section.
72     types:
73     for (let i = 0; i !== typeSection.data.length; ++i) {
74         const t = typeSection.data[i];
75         if (t.ret === ret && params.length === t.params.length) {
76             for (let j = 0; j !== t.params.length; ++j) {
77                 if (params[j] !== t.params[j])
78                     continue types;
79             }
80             type = i;
81             break;
82         }
83     }
84     if (typeof(type) !== "number") {
85         // Couldn't reuse a pre-existing type, register this type in the type section.
86         typeSection.data.push({ params: params, ret: ret });
87         type = typeSection.data.length - 1;
88     }
89     return type;
90 };
91
92 const _importFunctionContinuation = (builder, section, nextBuilder) => {
93     return (module, field, type) => {
94         assert.isString(module, `Import function module should be a string, got "${module}"`);
95         assert.isString(field, `Import function field should be a string, got "${field}"`);
96         const typeSection = builder._getSection("Type");
97         type = _maybeRegisterType(builder, type);
98         section.data.push({ field: field, type: type, kind: "Function", module: module });
99         // Imports also count in the function index space. Map them as objects to avoid clashing with Code functions' names.
100         builder._registerFunctionToIndexSpace({ module: module, field: field });
101         return nextBuilder;
102     };
103 };
104
105 const _exportFunctionContinuation = (builder, section, nextBuilder) => {
106     return (field, index, type) => {
107         assert.isString(field, `Export function field should be a string, got "${field}"`);
108         const typeSection = builder._getSection("Type");
109         if (typeof(type) !== "undefined") {
110             // Exports can leave the type unspecified, letting the Code builder patch them up later.
111             type = _maybeRegisterType(builder, type);
112         }
113         // We can't check much about "index" here because the Code section succeeds the Export section. More work is done at Code().End() time.
114         switch (typeof(index)) {
115         case "string": break; // Assume it's a function name which will be revealed in the Code section.
116         case "number": break; // Assume it's a number in the "function index space".
117         case "object":
118             // Re-exporting an import.
119             assert.hasObjectProperty(index, "module", `Re-exporting "${field}" from an import`);
120             assert.hasObjectProperty(index, "field", `Re-exporting "${field}" from an import`);
121             break;
122         case "undefined":
123             // Assume it's the same as the field (i.e. it's not being renamed).
124             index = field;
125             break;
126         default: throw new Error(`Export section's index must be a string or a number, got ${index}`);
127         }
128         const correspondingImport = builder._getFunctionFromIndexSpace(index);
129         const importSection = builder._getSection("Import");
130         if (typeof(index) === "object") {
131             // Re-exporting an import using its module+field name.
132             assert.isNotUndef(correspondingImport, `Re-exporting "${field}" couldn't find import from module "${index.module}" field "${index.field}"`);
133             index = correspondingImport;
134             if (typeof(type) === "undefined")
135                 type = importSection.data[index].type;
136             if (builder._checked)
137                 assert.eq(type, importSection.data[index].type, `Re-exporting import "${importSection.data[index].field}" as "${field}" has mismatching type`);
138         } else if (typeof(correspondingImport) !== "undefined") {
139             // Re-exporting an import using its index.
140             let exportedImport;
141             for (const i of importSection.data) {
142                 if (i.module === correspondingImport.module && i.field === correspondingImport.field) {
143                     exportedImport = i;
144                     break;
145                 }
146             }
147             if (typeof(type) === "undefined")
148                 type = exportedImport.type;
149             if (builder._checked)
150                 assert.eq(type, exportedImport.type, `Re-exporting import "${exportedImport.field}" as "${field}" has mismatching type`);
151         }
152         section.data.push({ field: field, type: type, kind: "Function", index: index });
153         return nextBuilder;
154     };
155 };
156
157 export default class Builder {
158     constructor() {
159         this.setChecked(true);
160         let preamble = {};
161         for (const p of WASM.description.preamble)
162             preamble[p.name] = p.value;
163         this.setPreamble(preamble);
164         this._sections = [];
165         this._functionIndexSpace = {};
166         this._functionIndexSpaceCount = 0;
167         this._registerSectionBuilders();
168     }
169     setChecked(checked) {
170         this._checked = checked;
171         return this;
172     }
173     setPreamble(p) {
174         this._preamble = Object.assign(this._preamble || {}, p);
175         return this;
176     }
177     _registerFunctionToIndexSpace(name) {
178         if (typeof(name) === "undefined") {
179             // Registering a nameless function still adds it to the function index space. Register it as something that can't normally be registered.
180             name = {};
181         }
182         // Collisions are fine: we'll simply count the function and forget the previous one.
183         this._functionIndexSpace[name] = this._functionIndexSpaceCount++;
184         // Map it both ways, the number space is distinct from the name space.
185         this._functionIndexSpace[this._functionIndexSpace[name]] = name;
186     }
187     _getFunctionFromIndexSpace(name) {
188         return this._functionIndexSpace[name];
189     }
190     _registerSectionBuilders() {
191         for (const section in WASM.description.section) {
192             switch (section) {
193             case "Type":
194                 this[section] = function() {
195                     const s = this._addSection(section);
196                     const builder = this;
197                     const typeBuilder = {
198                         End: () => builder,
199                         Func: (params, ret) => {
200                             [params, ret] = _normalizeFunctionSignature(params, ret);
201                             s.data.push({ params: params, ret: ret });
202                             return typeBuilder;
203                         },
204                     };
205                     return typeBuilder;
206                 };
207                 break;
208             case "Import":
209                 this[section] = function() {
210                     const s = this._addSection(section);
211                     const importBuilder = {
212                         End: () => this,
213                         Table: () => { throw new Error(`Unimplemented: import table`); },
214                         Memory: () => { throw new Error(`Unimplemented: import memory`); },
215                         Global: () => { throw new Error(`Unimplemented: import global`); },
216                     };
217                     importBuilder.Function = _importFunctionContinuation(this, s, importBuilder);
218                     return importBuilder;
219                 };
220                 break;
221             case "Export":
222                 this[section] = function() {
223                     const s = this._addSection(section);
224                     const exportBuilder = {
225                         End: () => this,
226                         Table: () => { throw new Error(`Unimplemented: export table`); },
227                         Memory: () => { throw new Error(`Unimplemented: export memory`); },
228                         Global: () => { throw new Error(`Unimplemented: export global`); },
229                     };
230                     exportBuilder.Function = _exportFunctionContinuation(this, s, exportBuilder);
231                     return exportBuilder;
232                 };
233                 break;
234             case "Code":
235                 this[section] = function() {
236                     const s = this._addSection(section);
237                     const builder = this;
238                     const codeBuilder =  {
239                         End: () => {
240                             // We now have enough information to remap the export section's "type" and "index" according to the Code section we're currently ending.
241                             const typeSection = builder._getSection("Type");
242                             const importSection = builder._getSection("Import");
243                             const exportSection = builder._getSection("Export");
244                             const codeSection = s;
245                             if (exportSection) {
246                                 for (const e of exportSection.data) {
247                                     switch (typeof(e.index)) {
248                                     default: throw new Error(`Unexpected export index "${e.index}"`);
249                                     case "string": {
250                                         const index = builder._getFunctionFromIndexSpace(e.index);
251                                         assert.isNumber(index, `Export section contains undefined function "${e.index}"`);
252                                         e.index = index;
253                                     } // Fallthrough.
254                                     case "number": {
255                                         const index = builder._getFunctionFromIndexSpace(e.index);
256                                         if (builder._checked)
257                                             assert.isNotUndef(index, `Export "${e.field}" doesn't correspond to a defined value in the function index space`);
258                                     } break;
259                                     case "undefined":
260                                         throw new Error(`Unimplemented: Function().End() with undefined export index`); // FIXME
261                                     }
262                                     if (typeof(e.type) === "undefined") {
263                                         // This must be a function export from the Code section (re-exports were handled earlier).
264                                         const functionIndexSpaceOffset = importSection ? importSection.data.length : 0;
265                                         const functionIndex = e.index - functionIndexSpaceOffset;
266                                         e.type = codeSection.data[functionIndex].type;
267                                     }
268                                 }
269                             }
270                             return builder;
271                         },
272                         Function: (a0, a1) => {
273                             let signature = typeof(a0) === "string" ? a1 : a0;
274                             const functionName = typeof(a0) === "string" ? a0 : undefined;
275                             if (typeof(signature) === "undefined")
276                                 signature = { params: [] };
277                             assert.hasObjectProperty(signature, "params", `Expect function signature to be an object with a "params" field, got "${signature}"`);
278                             const [params, ret] = _normalizeFunctionSignature(signature.params, signature.ret);
279                             signature = { params: params, ret: ret };
280                             const func = {
281                                 name: functionName,
282                                 type: _maybeRegisterType(builder, signature),
283                                 signature: signature,
284                                 locals: params, // Parameters are the first locals.
285                                 parameterCount: params.length,
286                                 code: []
287                             };
288                             s.data.push(func);
289                             builder._registerFunctionToIndexSpace(functionName);
290                             let functionBuilder = {};
291                             for (const op in WASM.description.opcode) {
292                                 const name = _toJavaScriptName(op);
293                                 const value = WASM.description.opcode[op].value;
294                                 const ret = WASM.description.opcode[op]["return"];
295                                 const param = WASM.description.opcode[op].parameter;
296                                 const imm = WASM.description.opcode[op].immediate;
297                                 const checkStackArgs = builder._checked ? op => {
298                                     for (let expect of param) {
299                                         if (WASM.isValidValueType(expect)) {
300                                             // FIXME implement stack checks for arguments. https://bugs.webkit.org/show_bug.cgi?id=163421
301                                         } else {
302                                             // Handle our own meta-types.
303                                             switch (expect) {
304                                             case "addr": break; // FIXME implement addr. https://bugs.webkit.org/show_bug.cgi?id=163421
305                                             case "any": break; // FIXME implement any. https://bugs.webkit.org/show_bug.cgi?id=163421
306                                             case "bool": break; // FIXME implement bool. https://bugs.webkit.org/show_bug.cgi?id=163421
307                                             case "call": break; // FIXME implement call stack argument checks based on function signature. https://bugs.webkit.org/show_bug.cgi?id=163421
308                                             case "global": break; // FIXME implement global. https://bugs.webkit.org/show_bug.cgi?id=163421
309                                             case "local": break; // FIXME implement local. https://bugs.webkit.org/show_bug.cgi?id=163421
310                                             case "prev": break; // FIXME implement prev, checking for whetever the previous value was. https://bugs.webkit.org/show_bug.cgi?id=163421
311                                             case "size": break; // FIXME implement size. https://bugs.webkit.org/show_bug.cgi?id=163421
312                                             default: throw new Error(`Implementation problem: unhandled meta-type "${expect}" on "${op}"`);
313                                             }
314                                         }
315                                     }
316                                 } : () => {};
317                                 const checkStackReturn = builder._checked ? op => {
318                                     for (let expect of ret) {
319                                         if (WASM.isValidValueType(expect)) {
320                                             // FIXME implement stack checks for return. https://bugs.webkit.org/show_bug.cgi?id=163421
321                                         } else {
322                                             // Handle our own meta-types.
323                                             switch (expect) {
324                                             case "bool": break; // FIXME implement bool. https://bugs.webkit.org/show_bug.cgi?id=163421
325                                             case "call": break; // FIXME implement call stack return check based on function signature. https://bugs.webkit.org/show_bug.cgi?id=163421
326                                             case "control": break; // FIXME implement control. https://bugs.webkit.org/show_bug.cgi?id=163421
327                                             case "global": break; // FIXME implement global. https://bugs.webkit.org/show_bug.cgi?id=163421
328                                             case "local": break; // FIXME implement local. https://bugs.webkit.org/show_bug.cgi?id=163421
329                                             case "prev": break; // FIXME implement prev, checking for whetever the parameter type was. https://bugs.webkit.org/show_bug.cgi?id=163421
330                                             case "size": break; // FIXME implement size. https://bugs.webkit.org/show_bug.cgi?id=163421
331                                             default: throw new Error(`Implementation problem: unhandled meta-type "${expect}" on "${op}"`);
332                                             }
333                                         }
334                                     }
335                                 } : () => {};
336                                 const checkImms = builder._checked ? (op, imms) => {
337                                     assert.eq(imms.length, imm.length, `"${op}" expects ${imm.length} immediates, got ${imms.length}`);
338                                     for (let idx = 0; idx !== imm.length; ++idx) {
339                                         const got = imms[idx];
340                                         const expect = imm[idx];
341                                         switch (expect.name) {
342                                         case "function_index":
343                                             assert.truthy(_isValidValue(got, "i32"), `Invalid value on ${op}: got "${got}", expected i32`);
344                                             // FIXME check function indices. https://bugs.webkit.org/show_bug.cgi?id=163421
345                                             break;
346                                         case "local_index": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
347                                         case "global_index": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
348                                         case "type_index": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
349                                         case "value":
350                                             assert.truthy(_isValidValue(got, ret[0]), `Invalid value on ${op}: got "${got}", expected ${ret[0]}`);
351                                             break;
352                                         case "flags": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
353                                         case "offset": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
354                                         // Control:
355                                         case "default_target": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
356                                         case "relative_depth": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
357                                         case "sig": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
358                                         case "target_count": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
359                                         case "target_table": throw new Error(`Unimplemented: "${expect.name}" on "${op}"`);
360                                         default: throw new Error(`Implementation problem: unhandled immediate "${expect.name}" on "${op}"`);
361                                         }
362                                     }
363                                 } : () => {};
364                                 const nextBuilder = name === "End" ? codeBuilder : functionBuilder;
365                                 functionBuilder[name] = (...args) => {
366                                     const imms = args; // FIXME: allow passing in stack values, as-if code were a stack machine. Just check for a builder to this function, and drop. https://bugs.webkit.org/show_bug.cgi?id=163422
367                                     checkImms(op, imms);
368                                     checkStackArgs(op);
369                                     checkStackReturn(op);
370                                     const stackArgs = []; // FIXME https://bugs.webkit.org/show_bug.cgi?id=162706
371                                     func.code.push({ name: op, value: value, arguments: stackArgs, immediates: imms });
372                                     return nextBuilder;
373                                 };
374                             }
375                             return functionBuilder;
376                         }
377                     };
378                     return codeBuilder;
379                 };
380                 break;
381             default:
382                 this[section] = () => { throw new Error(`Unimplemented: section type "${section}"`); };
383                 break;
384             }
385         }
386         this.Unknown = function(name) {
387             const s = this._addSection(name);
388             const builder = this;
389             const unknownBuilder =  {
390                 End: () => builder,
391                 Byte: b => {
392                     assert.eq(b & 0xFF, b, `Unknown section expected byte, got: "${b}"`);
393                     s.data.push(b);
394                     return unknownBuilder;
395                 }
396             };
397             return unknownBuilder;
398         };
399     }
400     _addSection(nameOrNumber, extraObject) {
401         const name = typeof(nameOrNumber) === "string" ? nameOrNumber : "";
402         const number = typeof(nameOrNumber) === "number" ? nameOrNumber : (WASM.description.section[name] ? WASM.description.section[name].value : _unknownSectionId);
403         if (this._checked) {
404             // Check uniqueness.
405             for (const s of this._sections)
406                 assert.falsy(s.name === name && s.id === number, `Cannot have two sections with the same name "${name}" and ID ${number}`);
407             // Check ordering.
408             if ((number !== _unknownSectionId) && (this._sections.length !== 0)) {
409                 for (let i = this._sections.length - 1; i >= 0; --i) {
410                     if (this._sections[i].id === _unknownSectionId)
411                         continue;
412                     assert.le(this._sections[i].id, number, `Bad section ordering: "${this._sections[i].name}" cannot precede "${name}"`);
413                     break;
414                 }
415             }
416         }
417         const s = Object.assign({ name: name, id: number, data: [] }, extraObject || {});
418         this._sections.push(s);
419         return s;
420     }
421     _getSection(nameOrNumber) {
422         switch (typeof(nameOrNumber)) {
423         default: throw new Error(`Implementation problem: can't get section "${nameOrNumber}"`);
424         case "string":
425             for (const s of this._sections)
426                 if (s.name === nameOrNumber)
427                     return s;
428             return undefined;
429         case "number":
430             for (const s of this._sections)
431                 if (s.id === nameOrNumber)
432                     return s;
433             return undefined;
434         }
435     }
436     optimize() {
437         // FIXME Add more optimizations. https://bugs.webkit.org/show_bug.cgi?id=163424
438         return this;
439     }
440     json() {
441         const obj = {
442             preamble: this._preamble,
443             section: this._sections
444         };
445         return JSON.stringify(obj);
446     }
447     AsmJS() {
448         "use asm"; // For speed.
449         // FIXME Create an asm.js equivalent string which can be eval'd. https://bugs.webkit.org/show_bug.cgi?id=163425
450         throw new Error("asm.js not implemented yet");
451     }
452     WebAssembly() { return BuildWebAssembly.Binary(this._preamble, this._sections); }
453 };