2 * Copyright (C) 2016 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
26 import * as assert from 'assert.js';
27 import * as BuildWebAssembly from 'Builder_WebAssemblyBinary.js';
28 import * as WASM from 'WASM.js';
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);
36 const _isValidValue = (value, 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}`);
45 const _unknownSectionId = 0;
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")
53 assert.isNotArray(ret, `Multiple return values not supported by WebAssembly yet`);
54 assert.truthy(WASM.isValidBlockType(ret), `Type return ${ret} must be valid block type`);
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 not use type ${type} if a type section is not present`);
64 assert.isNotUndef(typeSection.data[type], `Type ${type} does not exist in type section`);
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 not add type if a type section is not present`);
71 // Try reusing an equivalent type from the type section.
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])
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;
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 });
105 const _importMemoryContinuation = (builder, section, nextBuilder) => {
106 return (module, field, memoryDescription) => {
107 assert.isString(module, `Import function module should be a string, got "${module}"`);
108 assert.isString(field, `Import function field should be a string, got "${field}"`);
109 section.data.push({module, field, kind: "Memory", memoryDescription});
114 const _exportFunctionContinuation = (builder, section, nextBuilder) => {
115 return (field, index, type) => {
116 assert.isString(field, `Export function field should be a string, got "${field}"`);
117 const typeSection = builder._getSection("Type");
118 if (typeof(type) !== "undefined") {
119 // Exports can leave the type unspecified, letting the Code builder patch them up later.
120 type = _maybeRegisterType(builder, type);
123 // We can't check much about "index" here because the Code section succeeds the Export section. More work is done at Code().End() time.
124 switch (typeof(index)) {
125 case "string": break; // Assume it's a function name which will be revealed in the Code section.
126 case "number": break; // Assume it's a number in the "function index space".
128 // Re-exporting an import.
129 assert.hasObjectProperty(index, "module", `Re-exporting "${field}" from an import`);
130 assert.hasObjectProperty(index, "field", `Re-exporting "${field}" from an import`);
133 // Assume it's the same as the field (i.e. it's not being renamed).
136 default: throw new Error(`Export section's index must be a string or a number, got ${index}`);
139 const correspondingImport = builder._getFunctionFromIndexSpace(index);
140 const importSection = builder._getSection("Import");
141 if (typeof(index) === "object") {
142 // Re-exporting an import using its module+field name.
143 assert.isNotUndef(correspondingImport, `Re-exporting "${field}" couldn't find import from module "${index.module}" field "${index.field}"`);
144 index = correspondingImport;
145 if (typeof(type) === "undefined")
146 type = importSection.data[index].type;
147 if (builder._checked)
148 assert.eq(type, importSection.data[index].type, `Re-exporting import "${importSection.data[index].field}" as "${field}" has mismatching type`);
149 } else if (typeof(correspondingImport) !== "undefined") {
150 // Re-exporting an import using its index.
152 for (const i of importSection.data) {
153 if (i.module === correspondingImport.module && i.field === correspondingImport.field) {
158 if (typeof(type) === "undefined")
159 type = exportedImport.type;
160 if (builder._checked)
161 assert.eq(type, exportedImport.type, `Re-exporting import "${exportedImport.field}" as "${field}" has mismatching type`);
163 section.data.push({ field: field, type: type, kind: "Function", index: index });
168 const _checkStackArgs = (op, param) => {
169 for (let expect of param) {
170 if (WASM.isValidType(expect)) {
171 // FIXME implement stack checks for arguments. https://bugs.webkit.org/show_bug.cgi?id=163421
173 // Handle our own meta-types.
175 case "addr": break; // FIXME implement addr. https://bugs.webkit.org/show_bug.cgi?id=163421
176 case "any": break; // FIXME implement any. https://bugs.webkit.org/show_bug.cgi?id=163421
177 case "bool": break; // FIXME implement bool. https://bugs.webkit.org/show_bug.cgi?id=163421
178 case "call": break; // FIXME implement call stack argument checks based on function signature. https://bugs.webkit.org/show_bug.cgi?id=163421
179 case "global": break; // FIXME implement global. https://bugs.webkit.org/show_bug.cgi?id=163421
180 case "local": break; // FIXME implement local. https://bugs.webkit.org/show_bug.cgi?id=163421
181 case "prev": break; // FIXME implement prev, checking for whetever the previous value was. https://bugs.webkit.org/show_bug.cgi?id=163421
182 case "size": break; // FIXME implement size. https://bugs.webkit.org/show_bug.cgi?id=163421
183 default: throw new Error(`Implementation problem: unhandled meta-type "${expect}" on "${op}"`);
189 const _checkStackReturn = (op, ret) => {
190 for (let expect of ret) {
191 if (WASM.isValidType(expect)) {
192 // FIXME implement stack checks for return. https://bugs.webkit.org/show_bug.cgi?id=163421
194 // Handle our own meta-types.
196 case "bool": break; // FIXME implement bool. https://bugs.webkit.org/show_bug.cgi?id=163421
197 case "call": break; // FIXME implement call stack return check based on function signature. https://bugs.webkit.org/show_bug.cgi?id=163421
198 case "control": break; // FIXME implement control. https://bugs.webkit.org/show_bug.cgi?id=163421
199 case "global": break; // FIXME implement global. https://bugs.webkit.org/show_bug.cgi?id=163421
200 case "local": break; // FIXME implement local. https://bugs.webkit.org/show_bug.cgi?id=163421
201 case "prev": break; // FIXME implement prev, checking for whetever the parameter type was. https://bugs.webkit.org/show_bug.cgi?id=163421
202 case "size": break; // FIXME implement size. https://bugs.webkit.org/show_bug.cgi?id=163421
203 default: throw new Error(`Implementation problem: unhandled meta-type "${expect}" on "${op}"`);
209 const _checkImms = (op, imms, expectedImms, ret) => {
210 const minExpectedImms = expectedImms.filter(i => i.type.slice(-1) !== '*').length;
211 if (minExpectedImms !== expectedImms.length)
212 assert.ge(imms.length, minExpectedImms, `"${op}" expects at least ${minExpectedImms} immediate${minExpectedImms !== 1 ? 's' : ''}, got ${imms.length}`);
214 assert.eq(imms.length, minExpectedImms, `"${op}" expects exactly ${minExpectedImms} immediate${minExpectedImms !== 1 ? 's' : ''}, got ${imms.length}`);
215 for (let idx = 0; idx !== expectedImms.length; ++idx) {
216 const got = imms[idx];
217 const expect = expectedImms[idx];
218 switch (expect.name) {
219 case "function_index":
220 assert.truthy(_isValidValue(got, "i32"), `Invalid value on ${op}: got "${got}", expected i32`);
221 // FIXME check function indices. https://bugs.webkit.org/show_bug.cgi?id=163421
223 case "local_index": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
224 case "global_index": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
225 case "type_index": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
227 assert.truthy(_isValidValue(got, ret[0]), `Invalid value on ${op}: got "${got}", expected ${ret[0]}`);
229 case "flags": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
230 case "offset": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
232 case "default_target": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
233 case "relative_depth": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
235 assert.truthy(WASM.isValidBlockType(imms[idx]), `Invalid block type on ${op}: "${imms[idx]}"`);
237 case "target_count": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
238 case "target_table": break; // improve checking https://bugs.webkit.org/show_bug.cgi?id=163421
239 default: throw new Error(`Implementation problem: unhandled immediate "${expect.name}" on "${op}"`);
244 const _createFunctionBuilder = (func, builder, previousBuilder) => {
245 let functionBuilder = {};
246 for (const op in WASM.description.opcode) {
247 const name = _toJavaScriptName(op);
248 const value = WASM.description.opcode[op].value;
249 const ret = WASM.description.opcode[op]["return"];
250 const param = WASM.description.opcode[op].parameter;
251 const imm = WASM.description.opcode[op].immediate;
253 const checkStackArgs = builder._checked ? _checkStackArgs : () => {};
254 const checkStackReturn = builder._checked ? _checkStackReturn : () => {};
255 const checkImms = builder._checked ? _checkImms : () => {};
257 functionBuilder[name] = (...args) => {
261 nextBuilder = functionBuilder;
264 nextBuilder = previousBuilder;
269 nextBuilder = _createFunctionBuilder(func, builder, functionBuilder);
273 // Passing a function as the argument is a way to nest blocks lexically.
274 const continuation = args[args.length - 1];
275 const hasContinuation = typeof(continuation) === "function";
276 const imms = hasContinuation ? args.slice(0, -1) : 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
277 checkImms(op, imms, imm, ret);
278 checkStackArgs(op, param);
279 checkStackReturn(op, ret);
280 const stackArgs = []; // FIXME https://bugs.webkit.org/show_bug.cgi?id=162706
281 func.code.push({ name: op, value: value, arguments: stackArgs, immediates: imms });
283 return continuation(nextBuilder).End();
287 return functionBuilder;
290 const _createFunction = (section, builder, previousBuilder) => {
291 return (...args) => {
292 const nameOffset = (typeof(args[0]) === "string") >>> 0;
293 const functionName = nameOffset ? args[0] : undefined;
294 let signature = args[0 + nameOffset];
295 const locals = args[1 + nameOffset] === undefined ? [] : args[1 + nameOffset];
296 for (const local of locals)
297 assert.truthy(WASM.isValidValueType(local), `Type of local: ${local} needs to be a valid value type`);
299 if (typeof(signature) === "undefined")
300 signature = { params: [] };
301 assert.hasObjectProperty(signature, "params", `Expect function signature to be an object with a "params" field, got "${signature}"`);
302 const [params, ret] = _normalizeFunctionSignature(signature.params, signature.ret);
303 signature = { params: params, ret: ret };
306 type: _maybeRegisterType(builder, signature),
307 signature: signature,
308 locals: params.concat(locals), // Parameters are the first locals.
309 parameterCount: params.length,
313 const functionSection = builder._getSection("Function");
315 functionSection.data.push(func.type);
317 section.data.push(func);
318 builder._registerFunctionToIndexSpace(functionName);
320 return _createFunctionBuilder(func, builder, previousBuilder);
324 export default class Builder {
326 this.setChecked(true);
328 for (const p of WASM.description.preamble)
329 preamble[p.name] = p.value;
330 this.setPreamble(preamble);
332 this._functionIndexSpace = {};
333 this._functionIndexSpaceCount = 0;
334 this._registerSectionBuilders();
336 setChecked(checked) {
337 this._checked = checked;
341 this._preamble = Object.assign(this._preamble || {}, p);
344 _registerFunctionToIndexSpace(name) {
345 if (typeof(name) === "undefined") {
346 // Registering a nameless function still adds it to the function index space. Register it as something that can't normally be registered.
349 // Collisions are fine: we'll simply count the function and forget the previous one.
350 this._functionIndexSpace[name] = this._functionIndexSpaceCount++;
351 // Map it both ways, the number space is distinct from the name space.
352 this._functionIndexSpace[this._functionIndexSpace[name]] = name;
354 _getFunctionFromIndexSpace(name) {
355 return this._functionIndexSpace[name];
357 _registerSectionBuilders() {
358 for (const section in WASM.description.section) {
361 this[section] = function() {
362 const s = this._addSection(section);
363 const builder = this;
364 const typeBuilder = {
366 Func: (params, ret) => {
367 [params, ret] = _normalizeFunctionSignature(params, ret);
368 s.data.push({ params: params, ret: ret });
377 this[section] = function() {
378 const s = this._addSection(section);
379 const importBuilder = {
381 Table: () => { throw new Error(`Unimplemented: import table`); },
382 Global: () => { throw new Error(`Unimplemented: import global`); },
384 importBuilder.Function = _importFunctionContinuation(this, s, importBuilder);
385 importBuilder.Memory = _importMemoryContinuation(this, s, importBuilder);
386 return importBuilder;
391 this[section] = function() {
392 const s = this._addSection(section);
393 const exportBuilder = {
395 // FIXME: add ability to add this with whatever.
397 return exportBuilder;
402 // FIXME Implement table https://bugs.webkit.org/show_bug.cgi?id=164135
403 this[section] = () => { throw new Error(`Unimplemented: section type "${section}"`); };
407 this[section] = function() {
408 const s = this._addSection(section);
409 const exportBuilder = {
411 InitialMaxPages: (initial, max) => {
412 s.data.push({ initial, max });
413 return exportBuilder;
416 return exportBuilder;
421 // FIXME implement global https://bugs.webkit.org/show_bug.cgi?id=164133
422 this[section] = () => { throw new Error(`Unimplemented: section type "${section}"`); };
426 this[section] = function() {
427 const s = this._addSection(section);
428 const exportBuilder = {
430 Table: () => { throw new Error(`Unimplemented: export table`); },
431 Memory: () => { throw new Error(`Unimplemented: export memory`); },
432 Global: () => { throw new Error(`Unimplemented: export global`); },
434 exportBuilder.Function = _exportFunctionContinuation(this, s, exportBuilder);
435 return exportBuilder;
440 this[section] = function(functionIndexOrName) {
441 const s = this._addSection(section);
442 const startBuilder = {
445 if (typeof(functionIndexOrName) !== "number" && typeof(functionIndexOrName) !== "string")
446 throw new Error(`Start section's function index must either be a number or a string`);
447 s.data.push(functionIndexOrName);
453 // FIXME implement element https://bugs.webkit.org/show_bug.cgi?id=161709
454 this[section] = () => { throw new Error(`Unimplemented: section type "${section}"`); };
458 this[section] = function() {
459 const s = this._addSection(section);
460 const builder = this;
461 const codeBuilder = {
463 // We now have enough information to remap the export section's "type" and "index" according to the Code section we're currently ending.
464 const typeSection = builder._getSection("Type");
465 const importSection = builder._getSection("Import");
466 const exportSection = builder._getSection("Export");
467 const startSection = builder._getSection("Start");
468 const codeSection = s;
470 for (const e of exportSection.data) {
471 switch (typeof(e.index)) {
472 default: throw new Error(`Unexpected export index "${e.index}"`);
474 const index = builder._getFunctionFromIndexSpace(e.index);
475 assert.isNumber(index, `Export section contains undefined function "${e.index}"`);
479 const index = builder._getFunctionFromIndexSpace(e.index);
480 if (builder._checked)
481 assert.isNotUndef(index, `Export "${e.field}" does not correspond to a defined value in the function index space`);
484 throw new Error(`Unimplemented: Function().End() with undefined export index`); // FIXME
486 if (typeof(e.type) === "undefined") {
487 // This must be a function export from the Code section (re-exports were handled earlier).
488 let functionIndexSpaceOffset = 0;
490 for (const {kind} of importSection.data) {
491 if (kind === "Function")
492 ++functionIndexSpaceOffset;
495 const functionIndex = e.index - functionIndexSpaceOffset;
496 e.type = codeSection.data[functionIndex].type;
501 const start = startSection.data[0];
502 let mapped = builder._getFunctionFromIndexSpace(start);
503 if (!builder._checked) {
504 if (typeof(mapped) === "undefined")
505 mapped = start; // In unchecked mode, simply use what was provided if it's nonsensical.
506 assert.isA(start, "number"); // It can't be too nonsensical, otherwise we can't create a binary.
507 startSection.data[0] = start;
509 if (typeof(mapped) === "undefined")
510 throw new Error(`Start section refers to non-existant function '${start}'`);
511 if (typeof(start) === "string" || typeof(start) === "object")
512 startSection.data[0] = mapped;
513 // FIXME in checked mode, test that the type is acceptable for start function. We probably want _registerFunctionToIndexSpace to also register types per index. https://bugs.webkit.org/show_bug.cgi?id=165658
520 codeBuilder.Function = _createFunction(s, builder, codeBuilder);
526 // FIXME implement data https://bugs.webkit.org/show_bug.cgi?id=161709
527 this[section] = () => { throw new Error(`Unimplemented: section type "${section}"`); };
531 this[section] = () => { throw new Error(`Unknown section type "${section}"`); };
536 this.Unknown = function(name) {
537 const s = this._addSection(name);
538 const builder = this;
539 const unknownBuilder = {
542 assert.eq(b & 0xFF, b, `Unknown section expected byte, got: "${b}"`);
544 return unknownBuilder;
547 return unknownBuilder;
550 _addSection(nameOrNumber, extraObject) {
551 const name = typeof(nameOrNumber) === "string" ? nameOrNumber : "";
552 const number = typeof(nameOrNumber) === "number" ? nameOrNumber : (WASM.description.section[name] ? WASM.description.section[name].value : _unknownSectionId);
555 for (const s of this._sections)
556 assert.falsy(s.name === name && s.id === number, `Cannot have two sections with the same name "${name}" and ID ${number}`);
558 if ((number !== _unknownSectionId) && (this._sections.length !== 0)) {
559 for (let i = this._sections.length - 1; i >= 0; --i) {
560 if (this._sections[i].id === _unknownSectionId)
562 assert.le(this._sections[i].id, number, `Bad section ordering: "${this._sections[i].name}" cannot precede "${name}"`);
567 const s = Object.assign({ name: name, id: number, data: [] }, extraObject || {});
568 this._sections.push(s);
571 _getSection(nameOrNumber) {
572 switch (typeof(nameOrNumber)) {
573 default: throw new Error(`Implementation problem: can not get section "${nameOrNumber}"`);
575 for (const s of this._sections)
576 if (s.name === nameOrNumber)
580 for (const s of this._sections)
581 if (s.id === nameOrNumber)
587 // FIXME Add more optimizations. https://bugs.webkit.org/show_bug.cgi?id=163424
592 preamble: this._preamble,
593 section: this._sections
595 return JSON.stringify(obj);
598 "use asm"; // For speed.
599 // FIXME Create an asm.js equivalent string which can be eval'd. https://bugs.webkit.org/show_bug.cgi?id=163425
600 throw new Error("asm.js not implemented yet");
602 WebAssembly() { return BuildWebAssembly.Binary(this._preamble, this._sections); }