├── .gitignore ├── .travis.yml ├── README.md ├── compile ├── c-header.ts ├── context.ts ├── control.ts ├── conventions.ts ├── drop-select.ts ├── helpers.ts ├── instructions.ts ├── memory.ts ├── module.ts ├── numeric.ts ├── variable.ts └── x86_64-asm.ts ├── link-modules.ts ├── main.ts ├── package-lock.json ├── package.json ├── parse ├── index.ts ├── instruction.ts ├── module.ts └── value-type.ts ├── test ├── .gitignore ├── fib-expected.txt ├── fib-test.c ├── fib.wast ├── main.ts ├── params-test.c ├── params.wast ├── parse-s.ts ├── sha256-test.c ├── sha256.wast ├── trig-test.c ├── trig.wast └── tsconfig.json ├── tsconfig.json └── util.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.js 3 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 12 5 | - 10 # needed for BigInt 6 | os: 7 | - linux 8 | - osx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-to-asm 2 | 3 | [![Build Status](https://travis-ci.org/calebsander/wasm-to-asm.svg?branch=master)](https://travis-ci.org/calebsander/wasm-to-asm) 4 | 5 | This project compiles [WebAssembly](https://webassembly.org) modules to x86_64 assembly code. 6 | This allows WebAssembly code to be linked with C, C++, assembly, Rust or any other language that supports the [SysV ABI calling convention](https://wiki.osdev.org/System_V_ABI#x86-64). 7 | 8 | ## Example 9 | 10 | Condier the following WebAssembly module to compute Fibonacci numbers: 11 | ```lisp 12 | (module 13 | (func (export "fib") (param $n i32) (result i64) 14 | (local $prevPrev i64) 15 | (local $prev i64) 16 | (set_local $prevPrev (i64.const 1)) 17 | (loop $computeNext 18 | (if (get_local $n) (then 19 | (i64.add (get_local $prevPrev) (get_local $prev)) 20 | (set_local $prevPrev (get_local $prev)) 21 | (set_local $prev) 22 | (set_local $n (i32.sub (get_local $n) (i32.const 1))) 23 | (br $computeNext) 24 | )) 25 | ) 26 | (get_local $prev) 27 | ) 28 | ) 29 | ``` 30 | It exports a function `fib` which takes an 32-bit integer `n` and returns the 64-bit integer storing the `n`th Fibonacci number. 31 | We can convert this `.wast` (WebAssembly text) file to a `.wasm` (WebAssembly binary) file using [`wat2wasm`](https://github.com/WebAssembly/wabt). 32 | We can then use this project to compile the `.wasm` file to x86_64 assembly: 33 | ```bash 34 | npm i 35 | npm run build 36 | node main fib.wasm 37 | ``` 38 | This creates a `fib.s` assembly file and a `fib.h` C header file: 39 | ```asm 40 | MODULE0_FUNC0: 41 | push %rsi 42 | push %r8 43 | push %r9 44 | push %r10 45 | mov $0, %rsi 46 | mov $0, %r8 47 | # {"type":"i64.const","value":"1"} 48 | mov $1, %r9 49 | # {"type":"set_local","local":1} 50 | mov %r9, %rsi 51 | # {"type":"loop","returns":"empty","instructions":[...]} 52 | MODULE0_FUNC0_LOOP1: 53 | # {"type":"get_local","local":0} 54 | mov %rdi, %r9 55 | # {"type":"if","returns":"empty","ifInstructions":[...],"elseInstructions":[]} 56 | test %r9d, %r9d 57 | je MODULE0_FUNC0_IF_END2 58 | # {"type":"get_local","local":1} 59 | mov %rsi, %r9 60 | # {"type":"get_local","local":2} 61 | mov %r8, %r10 62 | # {"type":"i64.add"} 63 | add %r10, %r9 64 | # {"type":"get_local","local":2} 65 | mov %r8, %r10 66 | # {"type":"set_local","local":1} 67 | mov %r10, %rsi 68 | # {"type":"set_local","local":2} 69 | mov %r9, %r8 70 | # {"type":"get_local","local":0} 71 | mov %rdi, %r9 72 | # {"type":"i32.const","value":1} 73 | mov $1, %r10d 74 | # {"type":"i32.sub"} 75 | sub %r10d, %r9d 76 | # {"type":"set_local","local":0} 77 | mov %r9, %rdi 78 | # {"type":"br","label":1} 79 | jmp MODULE0_FUNC0_LOOP1 80 | MODULE0_FUNC0_IF_END2: 81 | # {"type":"get_local","local":2} 82 | mov %r8, %r9 83 | MODULE0_RETURN0: 84 | mov %r9, %rax 85 | pop %r10 86 | pop %r9 87 | pop %r8 88 | pop %rsi 89 | ret 90 | .globl wasm_fib_fib 91 | wasm_fib_fib: 92 | jmp MODULE0_FUNC0 93 | ``` 94 | ```c 95 | long wasm_fib_fib(int); 96 | ``` 97 | We can now call this `wasm_fib_fib()` function from C code: 98 | ```c 99 | #include 100 | #include "fib.h" 101 | 102 | int main() { 103 | for (int i = 0; i <= 93; i++) { // fib(94) doesn't fit in 64 bits 104 | printf("%d: %lu\n", i, wasm_fib_fib(i)); 105 | } 106 | } 107 | ``` 108 | We can simply compile against the assembly code and run the executable: 109 | ```bash 110 | $ gcc fib.s fib.c -o fib 111 | $ ./fib 112 | 0: 0 113 | 1: 1 114 | 2: 1 115 | 3: 2 116 | 4: 3 117 | 5: 5 118 | 6: 8 119 | 7: 13 120 | 8: 21 121 | 9: 34 122 | 10: 55 123 | ... 124 | 91: 4660046610375530309 125 | 92: 7540113804746346429 126 | 93: 12200160415121876738 127 | ``` 128 | Pretty nifty! 129 | 130 | ## Compatibility 131 | 132 | The assembly code produced should be compatible with any AT&T-syntax assembler for x86_64, such as [gas](https://en.wikipedia.org/wiki/GNU_Assembler). 133 | Currently, `unreachable` and the linear growable memory are implemented with Linux system calls, so WebAssembly code using these features will only work on Linux. 134 | To call WebAssembly functions from C or similar languages, the compiler must use the SysV ABI calling convention. 135 | 136 | ## TODO 137 | 138 | There are several features that aren't supported yet: 139 | - Linking multiple WebAssembly modules to each other 140 | - Runtime traps (out-of-bounds memory access, division by 0, etc.) 141 | - Optimizations for multiple-instruction sequences (e.g. incrementing a local) 142 | 143 | ## Calling convention 144 | 145 | The calling convention is similar to SysV ABI but doesn't have any caller-save registers. 146 | 147 | ### Use of registers 148 | 149 | #### `i32`/`i64` registers 150 | `%rax`, `%rcx`, and `%rdx` are used to store integer intermediates, e.g. when moving values between locations in memory. 151 | (These registers were chosen because bitshift instructions (`shl`, `ror`, etc.) and division instructions (`div` and `idiv`) require an operand to be stored in them. Also, `%rax` is needed to store integer return values.) 152 | The rest of the general-purpose registers (`%rdi`, `%rsi`, `%r8` to `%r15`, `%rbx`, and `%rbp`) are used to store local variables and the bottom of the integer computation stack. 153 | 154 | #### `f32`/`f64` registers 155 | Floats are stored individually in the lower 32 or 64 bits of the SIMD registers. 156 | `%xmm0`, `%xmm1`, and `%xmm15` are used to store intermediate values for float computations. 157 | (`%xmm0` was chosen because float return values are placed there. `%xmm15` was chosen because it is not used for function arguments in SysV, so params can be temporarily placed there while being moved between registers.) 158 | Registers `%xmm2` to `%xmm14` are used to store local variables and the bottom of the float computation stack. 159 | 160 | #### Example 161 | Consider a function with the following local variables: 162 | - `(param $f f64) (param $i i32)` 163 | - `(local $i1 i64) ($local $i2 i32) (local $f1 f32)` 164 | 165 | The register usage will be: 166 | - Integer: `$i` in `%edi`, `$i1` in `%rsi`, and `$i2` in `%r8d`. The stack will be stored in `%r9` through `%r15`, `%rbx`, `%rbp`, and then `(%rsp)`. 167 | - Float: `$f` in `%xmm2` and `$f1` in `%xmm3`. The stack will be stored in `%xmm4` through `%xmm14`, and then `(%rsp)`. 168 | 169 | ### Caller-callee handoff 170 | 171 | The caller first sets up the param registers for the callee, saving any that were in use onto the stack. 172 | The caller then invokes the `call` instruction to push `%rip` to the stack. 173 | The callee pushes all registers it will modify, does its work, and then restores those registers. 174 | If the callee returns a value, it is placed in `%rax` if it is an integer and `%xmm0` if it is a float. 175 | The callee invokes the `ret` instruction to return control to the caller. 176 | Finally, the caller restores any registers it saved to the stack and moves the return value onto the computation stack. -------------------------------------------------------------------------------- /compile/c-header.ts: -------------------------------------------------------------------------------- 1 | import {ResultType} from '../parse/instruction' 2 | 3 | type CType = 'void' | 'int' | 'long' | 'float' | 'double' 4 | 5 | export function getCType(type: ResultType): CType { 6 | switch (type) { 7 | case 'empty': return 'void' 8 | case 'i32': return 'int' 9 | case 'i64': return 'long' 10 | case 'f32': return 'float' 11 | case 'f64': return 'double' 12 | } 13 | } 14 | 15 | export interface HeaderDeclaration { 16 | readonly str: string 17 | } 18 | export class GlobalDeclaration { 19 | constructor( 20 | readonly type: CType | 'pointer', 21 | readonly constant: boolean, 22 | readonly name: string 23 | ) {} 24 | 25 | get str() { 26 | const pointer = this.type === 'pointer' 27 | const constAttribute = this.constant ? 'const ' : '' 28 | return pointer 29 | ? `void * ${constAttribute}${this.name};` 30 | : `${constAttribute}${this.type} ${this.name};` 31 | } 32 | } 33 | export class FunctionDeclaration { 34 | constructor( 35 | readonly returnType: CType, 36 | readonly name: string, 37 | readonly argTypes: CType[] 38 | ) {} 39 | 40 | get str() { 41 | return `${this.returnType} ${this.name}(${this.argTypes.join(', ')});` 42 | } 43 | } -------------------------------------------------------------------------------- /compile/context.ts: -------------------------------------------------------------------------------- 1 | import {ResultType} from '../parse/instruction' 2 | import {Export, FunctionType, Global, GlobalType, Import} from '../parse/module' 3 | import {ValueType} from '../parse/value-type' 4 | import {INVALID_EXPORT_CHAR, STACK_TOP, getGeneralRegisters, isFloat} from './conventions' 5 | import {ParamTarget} from './helpers' 6 | import {Datum, Register} from './x86_64-asm' 7 | 8 | export class SPRelative { 9 | constructor(public stackOffset: number) {} 10 | 11 | static forLocal(index: number, stackValues: number): SPRelative { 12 | return new SPRelative(stackValues - index - 1) 13 | } 14 | 15 | get datum(): Datum { 16 | return {...STACK_TOP, immediate: this.stackOffset << 3} 17 | } 18 | } 19 | 20 | export const getFunctionStats = 21 | ({params, results}: FunctionType, locals: ValueType[] = []): FunctionStats => ({ 22 | params, 23 | locals, 24 | result: (results as [ValueType?])[0] 25 | }) 26 | 27 | const makeModulePrefix = (moduleIndex: number) => `MODULE${moduleIndex}` 28 | const makeExportLabel = (modulePrefix: string, type: string, name: string) => 29 | `${modulePrefix}_EXPORT_${type}_${name}` 30 | 31 | interface BlockReference { 32 | loop: boolean // whether inside a loop 33 | label: string 34 | intStackHeight: number 35 | floatStackHeight: number 36 | result?: ValueType 37 | } 38 | interface FunctionStats { 39 | params: ValueType[] 40 | locals: ValueType[] 41 | result?: ValueType 42 | } 43 | interface LocalLocation { 44 | float: boolean 45 | index: number 46 | stackIndex?: number 47 | } 48 | export interface StackState { 49 | intStackHeight: number 50 | floatStackHeight: number 51 | stackFloats: boolean[] 52 | } 53 | interface ImportReference { 54 | modulePrefix: string 55 | name: string 56 | } 57 | export interface ModuleIndices { 58 | [module: string]: number 59 | } 60 | export class ModuleContext { 61 | readonly exportMemory: string[] = [] 62 | readonly exportFunctions = new Map() 63 | readonly exportGlobals = new Map() 64 | readonly globalTypes: GlobalType[] = [] 65 | private readonly importFunctions: ImportReference[] = [] 66 | private readonly importGlobals: ImportReference[] = [] 67 | private readonly importGlobalTypes: ValueType[] = [] 68 | private readonly types: FunctionType[] = [] 69 | private readonly functionStats = new Map() 70 | private memoryMax: number | undefined 71 | 72 | constructor( 73 | readonly index: number, 74 | readonly moduleIndices: ModuleIndices 75 | ) {} 76 | 77 | addGlobals(globals: Global[]): void { 78 | for (const {type} of globals) this.globalTypes.push(type) 79 | } 80 | addExports(exportSection: Export[]): void { 81 | for (let {name, description: {type, index}} of exportSection) { 82 | name = name.replace(INVALID_EXPORT_CHAR, '_') 83 | let exportMap: Map 84 | switch (type) { 85 | case 'memory': 86 | if (index) throw new Error('Invalid memory index') 87 | this.exportMemory.push(name) 88 | continue 89 | case 'function': 90 | exportMap = this.exportFunctions 91 | break 92 | case 'global': 93 | exportMap = this.exportGlobals 94 | break 95 | default: 96 | throw new Error('Unexpected export type: ' + type) 97 | } 98 | const names = exportMap.get(index) 99 | if (names) names.push(name) 100 | else exportMap.set(index, [name]) 101 | } 102 | } 103 | addImports(imports: Import[]): void { 104 | for (const {module, name, description} of imports) { 105 | const moduleIndex = this.moduleIndices[module] as number | undefined 106 | if (moduleIndex === undefined) throw new Error('No such module: ' + module) 107 | const modulePrefix = makeModulePrefix(moduleIndex) 108 | let importMap: ImportReference[] | undefined 109 | switch (description.type) { 110 | case 'function': 111 | importMap = this.importFunctions 112 | break 113 | case 'global': 114 | importMap = this.importGlobals 115 | this.importGlobalTypes.push(description.valueType.type) 116 | } 117 | if (importMap) importMap.push({modulePrefix, name}) 118 | } 119 | } 120 | addTypes(types: FunctionType[]): void { 121 | this.types.push(...types) 122 | } 123 | getType(index: number): FunctionType { 124 | const type = this.types[index] as FunctionType | undefined 125 | if (!type) throw new Error(`No function type ${index}`) 126 | return type 127 | } 128 | getFunctionIndex(segmentIndex: number): number { 129 | return this.baseFunctionIndex + segmentIndex 130 | } 131 | setFunctionStats(segmentIndex: number, stats: FunctionStats): void { 132 | this.functionStats.set(this.getFunctionIndex(segmentIndex), stats) 133 | } 134 | makeFunctionContext(functionIndex: number): CompilationContext { 135 | // TODO: can this context be cached to avoid recomputing the context for the same function? 136 | const thisStats = this.functionStats.get(functionIndex) 137 | if (!thisStats) throw new Error(`No function stats for function ${functionIndex}`) 138 | return new CompilationContext(this, thisStats, functionIndex) 139 | } 140 | private get baseFunctionIndex(): number { 141 | return this.importFunctions.length 142 | } 143 | private get baseGlobalIndex(): number { 144 | return this.importGlobals.length 145 | } 146 | resolveGlobalType(index: number): ValueType { 147 | const moduleGlobalIndex = index - this.baseGlobalIndex 148 | return moduleGlobalIndex < 0 149 | ? this.importGlobalTypes[index] 150 | : this.globalTypes[moduleGlobalIndex].type 151 | } 152 | resolveGlobalLabel(index: number): string { 153 | const moduleGlobalIndex = index - this.baseGlobalIndex 154 | if (moduleGlobalIndex < 0) { 155 | const importRef = this.importGlobals[index] 156 | return makeExportLabel(importRef.modulePrefix, 'GLOBAL', importRef.name) 157 | } 158 | return this.globalLabel(moduleGlobalIndex) 159 | } 160 | getFunctionLabel(index: number): string { 161 | const moduleFunctionIndex = index - this.baseFunctionIndex 162 | if (moduleFunctionIndex < 0) { 163 | const importRef = this.importFunctions[index] 164 | return makeExportLabel(importRef.modulePrefix, 'FUNC', importRef.name) 165 | } 166 | return this.functionLabel(moduleFunctionIndex) 167 | } 168 | private get modulePrefix(): string { 169 | return makeModulePrefix(this.index) 170 | } 171 | globalLabel(index: number): string { 172 | return `${this.modulePrefix}_GLOBAL${index}` 173 | } 174 | functionLabel(index: number): string { 175 | return `${this.modulePrefix}_FUNC${index}` 176 | } 177 | returnLabel(index: number): string { 178 | return `${this.modulePrefix}_RETURN${index}` 179 | } 180 | exportLabel(type: string, name: string): string { 181 | return makeExportLabel(this.modulePrefix, type, name) 182 | } 183 | tableLabel(index: number): string { 184 | return `${this.modulePrefix}_TABLE${index}` 185 | } 186 | get memorySizeLabel(): string { 187 | return `${this.modulePrefix}_MEMSIZE` 188 | } 189 | get memoryStart(): number { 190 | // mac OS uses 0x1xxxxxxxx for the executable and heap, 191 | // so we start virtual mappings at 0x2xxxxxxxx 192 | return 0x100000000 * (this.index + 2) 193 | } 194 | setMemoryMax(max?: number): void { 195 | this.memoryMax = max 196 | } 197 | get maxPages(): number { 198 | // Can't have more than 2 ** 32 / 2 ** 16 == 2 ** 16 pages 199 | return this.memoryMax === undefined ? 1 << 16 : this.memoryMax 200 | } 201 | } 202 | export class CompilationContext { 203 | readonly params: LocalLocation[] 204 | readonly locals: LocalLocation[] 205 | readonly result?: ValueType 206 | private intLocalCount = 0 207 | private floatLocalCount = 0 208 | readonly stackParams: number 209 | private stackFloats: boolean[] = [] 210 | private intStackHeight = 0 211 | private floatStackHeight = 0 212 | private maxIntStackHeight = 0 213 | private maxFloatStackHeight = 0 214 | private labelCount = 0 215 | private readonly containingLabels: BlockReference[] = [] 216 | 217 | constructor( 218 | readonly moduleContext: ModuleContext, 219 | {params, locals, result}: FunctionStats, 220 | private readonly index?: number 221 | ) { 222 | let stackIndex = 0 223 | const getLocalLocation = (param: ValueType): LocalLocation => { 224 | const float = isFloat(param) 225 | const index = float ? this.floatLocalCount++ : this.intLocalCount++ 226 | const location: LocalLocation = {float, index} 227 | if (index >= getGeneralRegisters(float).length) { 228 | location.stackIndex = stackIndex++ 229 | } 230 | return location 231 | } 232 | this.params = params.map(getLocalLocation) 233 | this.stackParams = stackIndex 234 | this.locals = locals.map(getLocalLocation) 235 | this.result = result 236 | } 237 | 238 | push(float: boolean): void { 239 | this.stackFloats.push(float) 240 | if (float) { 241 | this.maxFloatStackHeight = 242 | Math.max(++this.floatStackHeight, this.maxFloatStackHeight) 243 | } 244 | else { 245 | this.maxIntStackHeight = 246 | Math.max(++this.intStackHeight, this.maxIntStackHeight) 247 | } 248 | } 249 | pop(): boolean { 250 | const float = this.peek() 251 | this.stackFloats.pop() 252 | if (float) this.floatStackHeight-- 253 | else this.intStackHeight-- 254 | return float 255 | } 256 | peek(): boolean { 257 | const stackHeight = this.stackFloats.length 258 | if (!stackHeight) throw new Error('Empty stack') 259 | return this.stackFloats[stackHeight - 1] 260 | } 261 | private localsAndStackHeight(float: boolean): number { 262 | return float 263 | ? this.floatLocalCount + this.floatStackHeight 264 | : this.intLocalCount + this.intStackHeight 265 | } 266 | private getValuesAfterRegisters(totalValues: number, float: boolean): number { 267 | return Math.max(totalValues - getGeneralRegisters(float).length, 0) 268 | } 269 | getValuesOnStack(float: boolean): number { 270 | return this.getValuesAfterRegisters(this.localsAndStackHeight(float), float) 271 | } 272 | getParam(paramIndex: number): LocalLocation { 273 | const localIndex = paramIndex - this.params.length 274 | return localIndex < 0 ? this.params[paramIndex] : this.locals[localIndex] 275 | } 276 | resolveParam(paramIndex: number): ParamTarget { 277 | const {float, index, stackIndex} = this.getParam(paramIndex) 278 | const generalRegisters = getGeneralRegisters(float) 279 | return stackIndex === undefined 280 | ? generalRegisters[index] 281 | : SPRelative.forLocal( 282 | stackIndex, 283 | this.getValuesOnStack(false) + this.getValuesOnStack(true) 284 | ) 285 | } 286 | resolveParamDatum(index: number): Datum { 287 | const resolved = this.resolveParam(index) 288 | return resolved instanceof SPRelative 289 | ? resolved.datum 290 | : {type: 'register', register: resolved} 291 | } 292 | resolveLocal(index: number): ParamTarget { 293 | return this.resolveParam(this.params.length + index) 294 | } 295 | resolveLocalDatum(index: number): Datum { 296 | return this.resolveParamDatum(this.params.length + index) 297 | } 298 | private resolveStackTop(float: boolean): Register | undefined { 299 | return (getGeneralRegisters(float) as (Register | undefined)[]) 300 | [this.localsAndStackHeight(float)] 301 | } 302 | resolvePush(float: boolean): Register | undefined { 303 | const resolved = this.resolveStackTop(float) 304 | this.push(float) 305 | return resolved 306 | } 307 | resolvePop(): Register | undefined { 308 | return this.resolveStackTop(this.pop()) 309 | } 310 | // If wholeFunction is true, maxStackHeight is used and params are excluded 311 | registersUsed(wholeFunction?: true): Register[] { 312 | const toSave: Register[] = [] 313 | if (!wholeFunction) { 314 | const {params} = this 315 | for (let i = 0; i < params.length; i++) { 316 | const resolved = this.resolveParam(i) 317 | if (!(resolved instanceof SPRelative)) toSave.push(resolved) 318 | } 319 | } 320 | const { 321 | locals, 322 | intStackHeight: originalIntHeight, 323 | floatStackHeight: originalFloatHeight 324 | } = this 325 | for (let i = 0; i < locals.length; i++) { 326 | const resolved = this.resolveLocal(i) 327 | if (!(resolved instanceof SPRelative)) toSave.push(resolved) 328 | } 329 | let intStackHeight: number, floatStackHeight: number 330 | if (wholeFunction) { 331 | intStackHeight = this.maxIntStackHeight 332 | floatStackHeight = this.maxFloatStackHeight 333 | } 334 | else { 335 | ({intStackHeight, floatStackHeight} = this) 336 | } 337 | for (let stackHeight = 0; stackHeight < intStackHeight; stackHeight++) { 338 | this.intStackHeight = stackHeight 339 | const resolved = this.resolveStackTop(false) 340 | if (resolved) toSave.push(resolved) 341 | else break 342 | } 343 | for (let stackHeight = 0; stackHeight < floatStackHeight; stackHeight++) { 344 | this.floatStackHeight = stackHeight 345 | const resolved = this.resolveStackTop(true) 346 | if (resolved) toSave.push(resolved) 347 | else break 348 | } 349 | this.intStackHeight = originalIntHeight 350 | this.floatStackHeight = originalFloatHeight 351 | return toSave 352 | } 353 | get functionIndex(): number { 354 | if (this.index === undefined) throw new Error('Outside function context') 355 | return this.index 356 | } 357 | makeLabel(prefix: string): string { 358 | const functionLabel = this.index === undefined 359 | ? 'GLOBAL' 360 | : this.moduleContext.functionLabel(this.index) 361 | return `${functionLabel}_${prefix}${++this.labelCount}` 362 | } 363 | getStackHeight(float: boolean): number { 364 | return float ? this.floatStackHeight : this.intStackHeight 365 | } 366 | get stackState(): StackState { 367 | return { 368 | intStackHeight: this.intStackHeight, 369 | floatStackHeight: this.floatStackHeight, 370 | stackFloats: this.stackFloats.slice() 371 | } 372 | } 373 | restoreStackState({intStackHeight, floatStackHeight, stackFloats}: StackState) { 374 | this.intStackHeight = intStackHeight 375 | this.floatStackHeight = floatStackHeight 376 | this.stackFloats = stackFloats.slice() 377 | } 378 | pushLabel(loop: boolean, label: string, returns: ResultType): void { 379 | this.containingLabels.push({ 380 | loop, 381 | label, 382 | intStackHeight: this.intStackHeight, 383 | floatStackHeight: this.floatStackHeight, 384 | result: returns === 'empty' ? undefined : returns 385 | }) 386 | } 387 | popLabel(): void { 388 | this.containingLabels.pop() 389 | } 390 | getNestedLabel(nesting: number): BlockReference | undefined { 391 | return (this.containingLabels as (BlockReference | undefined)[]) 392 | [this.containingLabels.length - nesting - 1] 393 | } 394 | private get stackIntLocals(): number { 395 | return this.getValuesAfterRegisters(this.intLocalCount, false) 396 | } 397 | private get stackFloatLocals(): number { 398 | return this.getValuesAfterRegisters(this.floatLocalCount, true) 399 | } 400 | get stackLocals(): number { 401 | return this.stackIntLocals + this.stackFloatLocals 402 | } 403 | } -------------------------------------------------------------------------------- /compile/control.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BranchInstruction, 3 | BranchTableInstruction, 4 | BlockInstruction, 5 | CallIndirectInstruction, 6 | CallInstruction, 7 | IfInstruction, 8 | ResultType 9 | } from '../parse/instruction' 10 | import {reverse} from '../util' 11 | import {CompilationContext, getFunctionStats, SPRelative, StackState} from './context' 12 | import { 13 | INT_INTERMEDIATE_REGISTERS, 14 | SYSCALL, 15 | SYSCALL_DATUM, 16 | getResultRegister, 17 | isFloat 18 | } from './conventions' 19 | import {compileBranch, growStack, ParamTarget, relocateArguments, shrinkStack} from './helpers' 20 | import {compileInstructions} from './instructions' 21 | import * as asm from './x86_64-asm' 22 | 23 | export interface BranchResult { 24 | /** The possible block/loop/if labels that could be branched to */ 25 | branches: Set 26 | /** Whether a branch will definitely occur */ 27 | definitely: boolean 28 | } 29 | 30 | export const ALWAYS_BRANCHES: BranchResult = {branches: new Set, definitely: true}, 31 | NEVER_BRANCHES: BranchResult = {branches: new Set, definitely: false} 32 | 33 | const JUMP_TABLE_ENTRY_SIZE = 8 34 | 35 | // exit(0xFF) 36 | export const UNREACHABLE_INSTRUCTIONS: asm.AssemblyInstruction[] = [ 37 | new asm.MoveInstruction( 38 | {type: 'immediate', value: SYSCALL.exit}, SYSCALL_DATUM 39 | ), 40 | new asm.MoveInstruction( 41 | {type: 'immediate', value: 0xFF}, {type: 'register', register: 'rdi'} 42 | ), 43 | new asm.SysCallInstruction 44 | ] 45 | 46 | function getBlockEndBranches( 47 | blockLabel: string, 48 | subBranches: Set, 49 | returns: ResultType, 50 | context: CompilationContext, 51 | stackState: StackState 52 | ): BranchResult { 53 | const branches = new Set() 54 | for (const label of subBranches) { 55 | if (label !== blockLabel) branches.add(label) 56 | } 57 | context.restoreStackState(stackState) 58 | if (returns !== 'empty') context.push(isFloat(returns)) 59 | return {branches, definitely: false} 60 | } 61 | 62 | export function compileBlockInstruction( 63 | instruction: BlockInstruction, 64 | context: CompilationContext, 65 | output: asm.AssemblyInstruction[] 66 | ): BranchResult { 67 | const {type, returns, instructions} = instruction 68 | const {stackState} = context 69 | const blockLabel = context.makeLabel(type.toUpperCase()) 70 | const loop = type === 'loop' 71 | if (loop) output.push(new asm.Label(blockLabel)) 72 | context.pushLabel(loop, blockLabel, returns) 73 | const branch = compileInstructions(instructions, context, output) 74 | context.popLabel() 75 | const {branches, definitely} = branch 76 | if (!loop) output.push(new asm.Label(blockLabel)) 77 | if (definitely) { 78 | if (!branches.has(blockLabel)) return branch 79 | if (loop && branches.size === 1) return ALWAYS_BRANCHES // infinite loop 80 | } 81 | 82 | return getBlockEndBranches(blockLabel, branches, returns, context, stackState) 83 | } 84 | export function compileIfInstruction( 85 | instruction: IfInstruction, 86 | context: CompilationContext, 87 | output: asm.AssemblyInstruction[] 88 | ): BranchResult { 89 | const {returns, ifInstructions, elseInstructions} = instruction 90 | const endLabel = context.makeLabel('IF_END') 91 | const elseLabel = 92 | elseInstructions.length ? context.makeLabel('ELSE') : undefined 93 | let cond = context.resolvePop() 94 | if (!cond) { // cond is on the stack 95 | [cond] = INT_INTERMEDIATE_REGISTERS 96 | output.push(new asm.PopInstruction({type: 'register', register: cond})) 97 | } 98 | const {stackState} = context 99 | const datum: asm.Datum = {type: 'register', register: cond, width: 'l'} 100 | output.push( 101 | new asm.TestInstruction(datum, datum), 102 | new asm.JumpInstruction(elseLabel || endLabel, 'e') 103 | ) 104 | context.pushLabel(false, endLabel, returns) 105 | const branch = compileInstructions(ifInstructions, context, output) 106 | if (elseLabel) { 107 | context.restoreStackState(stackState) 108 | if (!branch.definitely) output.push(new asm.JumpInstruction(endLabel)) 109 | output.push(new asm.Label(elseLabel)) 110 | const {branches, definitely} = 111 | compileInstructions(elseInstructions, context, output) 112 | branch.definitely = branch.definitely && definitely 113 | for (const label of branches) branch.branches.add(label) 114 | } 115 | else branch.definitely = false // if statement may be skipped entirely 116 | context.popLabel() 117 | output.push(new asm.Label(endLabel)) 118 | return branch.definitely && !branch.branches.has(endLabel) 119 | ? branch 120 | : getBlockEndBranches(endLabel, branch.branches, returns, context, stackState) 121 | } 122 | export function compileBranchInstruction( 123 | instruction: BranchInstruction, 124 | context: CompilationContext, 125 | output: asm.AssemblyInstruction[] 126 | ): BranchResult { 127 | const {type, label} = instruction 128 | let endLabel: string | undefined 129 | let stackState: StackState 130 | if (type === 'br_if') { 131 | endLabel = context.makeLabel('BR_IF_END') 132 | let cond = context.resolvePop() 133 | if (!cond) { 134 | [cond] = INT_INTERMEDIATE_REGISTERS 135 | output.push(new asm.PopInstruction({type: 'register', register: cond})) 136 | } 137 | ({stackState} = context) 138 | const datum: asm.Datum = {type: 'register', register: cond, width: 'l'} 139 | output.push( 140 | new asm.TestInstruction(datum, datum), 141 | new asm.JumpInstruction(endLabel, 'e') 142 | ) 143 | } 144 | const branch = compileBranch(label, context, output) 145 | const branches = branch ? new Set([branch]) : new Set() 146 | if (!endLabel) return {branches, definitely: true} 147 | 148 | context.restoreStackState(stackState!) 149 | output.push(new asm.Label(endLabel)) 150 | return {branches, definitely: false} 151 | } 152 | export function compileBranchTableInstruction( 153 | instruction: BranchTableInstruction, 154 | context: CompilationContext, 155 | output: asm.AssemblyInstruction[] 156 | ): BranchResult { 157 | const {cases, defaultCase} = instruction 158 | let value = context.resolvePop() 159 | if (!value) { 160 | value = INT_INTERMEDIATE_REGISTERS[0] 161 | output.push(new asm.PopInstruction({type: 'register', register: value})) 162 | } 163 | 164 | const tableLabel = context.makeLabel('BR_TABLE') 165 | const {stackState} = context 166 | const caseOutput: asm.AssemblyInstruction[] = [] 167 | const branches = new Set() 168 | const nestingLabels = new Map() 169 | const compileCase = (nesting: number) => { 170 | let caseLabel = nestingLabels.get(nesting) 171 | if (!caseLabel) { 172 | nestingLabels.set(nesting, caseLabel = `${tableLabel}_${nesting}`) 173 | caseOutput.push(new asm.Label(caseLabel)) 174 | const branch = compileBranch(nesting, context, caseOutput) 175 | if (branch) branches.add(branch) 176 | context.restoreStackState(stackState) 177 | } 178 | return caseLabel 179 | } 180 | const defaultLabel = compileCase(defaultCase) 181 | 182 | const tableAddress = INT_INTERMEDIATE_REGISTERS[1] 183 | const addressDatum: asm.Datum = {type: 'register', register: tableAddress} 184 | output.push( 185 | new asm.CmpInstruction( 186 | {type: 'immediate', value: cases.length}, 187 | {type: 'register', register: value, width: 'l'} 188 | ), 189 | new asm.JumpInstruction(defaultLabel, 'ae'), 190 | new asm.LeaInstruction({type: 'label', label: tableLabel}, addressDatum), 191 | new asm.AddInstruction( 192 | { 193 | type: 'indirect', 194 | register: tableAddress, 195 | offset: {register: value, scale: JUMP_TABLE_ENTRY_SIZE} 196 | }, 197 | addressDatum 198 | ), 199 | new asm.JumpInstruction(addressDatum), 200 | new asm.Directive({type: 'section', args: ['.rodata']}), 201 | new asm.Directive({type: 'balign', args: [JUMP_TABLE_ENTRY_SIZE]}), 202 | new asm.Label(tableLabel) 203 | ) 204 | for (const nesting of cases) { 205 | output.push(new asm.Directive( 206 | {type: 'quad', args: [compileCase(nesting) + ' - ' + tableLabel]} 207 | )) 208 | } 209 | output.push( 210 | new asm.Directive({type: 'section', args: ['.text']}), 211 | ...caseOutput 212 | ) 213 | return {branches, definitely: true} 214 | } 215 | export function compileCallInstruction( 216 | instruction: CallInstruction | CallIndirectInstruction, 217 | context: CompilationContext, 218 | output: asm.AssemblyInstruction[] 219 | ): void { 220 | // TODO: need to know other modules' functionsStats 221 | const {moduleContext} = context 222 | let otherContext: CompilationContext 223 | let indexRegister: asm.Register 224 | if (instruction.type === 'call') { 225 | otherContext = moduleContext.makeFunctionContext(instruction.func) 226 | } 227 | else { // call_indirect 228 | otherContext = new CompilationContext( 229 | moduleContext, 230 | getFunctionStats(moduleContext.getType(instruction.funcType)) 231 | ) 232 | // Skip %rax since it may be clobbered by relocation 233 | indexRegister = INT_INTERMEDIATE_REGISTERS[1] 234 | const value = context.resolvePop() 235 | const indexDatum: asm.Datum = {type: 'register', register: indexRegister} 236 | output.push(value 237 | ? new asm.MoveInstruction({type: 'register', register: value}, indexDatum) 238 | : new asm.PopInstruction(indexDatum) 239 | ) 240 | } 241 | const {params, result, stackLocals} = otherContext 242 | const registersUsed = new Set(context.registersUsed()) 243 | const moves = new Map() 244 | const stackParams: ParamTarget[] = [] 245 | let calleeStackParams = 0 246 | for (let i = params.length - 1; i >= 0; i--) { 247 | const source = context.resolvePop() 248 | const target = otherContext.resolveParam(i) 249 | if (target instanceof SPRelative) { 250 | target.stackOffset -= stackLocals 251 | calleeStackParams++ 252 | } 253 | if (source) moves.set(source, target) 254 | else stackParams.push(target) 255 | } 256 | const {toRestore, output: relocateOutput} = 257 | relocateArguments(moves, stackParams, registersUsed) 258 | const pushed: asm.Datum[] = [] 259 | for (const register of toRestore) { 260 | const datum: asm.Datum = {type: 'register', register} 261 | output.push(new asm.PushInstruction(datum)) 262 | pushed.push(datum) 263 | } 264 | output.push(...relocateOutput) 265 | if (calleeStackParams) { // point to end of pushed parameters 266 | output.push(growStack(calleeStackParams)) 267 | } 268 | if (instruction.type === 'call') { 269 | output.push(new asm.CallInstruction( 270 | moduleContext.getFunctionLabel(instruction.func) 271 | )) 272 | } 273 | else { 274 | const tableRegister = INT_INTERMEDIATE_REGISTERS[2] 275 | output.push( 276 | new asm.LeaInstruction( 277 | {type: 'label', label: moduleContext.tableLabel(0)}, 278 | {type: 'register', register: tableRegister} 279 | ), 280 | new asm.CallInstruction({ 281 | type: 'indirect', 282 | register: tableRegister, 283 | offset: {register: indexRegister!, scale: 8} 284 | }) 285 | ) 286 | } 287 | if (calleeStackParams) { // pop parameters passed on the stack 288 | output.push(shrinkStack(calleeStackParams)) 289 | } 290 | for (const datum of reverse(pushed)) output.push(new asm.PopInstruction(datum)) 291 | const stackPopped = stackParams.length 292 | if (stackPopped) { // point to actual stack location 293 | output.push(shrinkStack(stackPopped)) 294 | } 295 | if (result) { 296 | const float = isFloat(result) 297 | const resultDatum: asm.Datum = 298 | {type: 'register', register: getResultRegister(float)} 299 | const pushTo = context.resolvePush(float) 300 | output.push(pushTo 301 | ? new asm.MoveInstruction( 302 | resultDatum, {type: 'register', register: pushTo}, 'q' 303 | ) 304 | : new asm.PushInstruction(resultDatum) 305 | ) 306 | } 307 | } -------------------------------------------------------------------------------- /compile/conventions.ts: -------------------------------------------------------------------------------- 1 | import {ValueType} from '../parse/value-type' 2 | import {makeArray} from '../util' 3 | import {Datum, Register, Width} from './x86_64-asm' 4 | 5 | if (!['darwin', 'linux'].includes(process.platform)) { 6 | throw new Error(`Unsupported platform ${process.platform}`) 7 | } 8 | export const isLinux = process.platform === 'linux' 9 | 10 | export const INT_INTERMEDIATE_REGISTERS: Register[] = ['rax', 'rcx', 'rdx'] 11 | export const [INT_RESULT_REGISTER] = INT_INTERMEDIATE_REGISTERS 12 | export const INT_GENERAL_REGISTERS: Register[] = [ 13 | 'rdi', 'rsi', 14 | 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', 'r15', 15 | 'rbx', 'rbp' 16 | ] 17 | export const SHIFT_REGISTER: Register = 'rcx' 18 | export const SHIFT_REGISTER_DATUM: Datum = 19 | {type: 'register', register: SHIFT_REGISTER} 20 | export const SHIFT_REGISTER_BYTE: Datum = {...SHIFT_REGISTER_DATUM, width: 'b'} 21 | export const DIV_LOWER_REGISTER: Register = 'rax' 22 | export const DIV_LOWER_DATUM = 23 | {type: 'register' as const, register: DIV_LOWER_REGISTER} 24 | const DIV_UPPER_REGISTER: Register = 'rdx' 25 | export const DIV_UPPER_DATUM = 26 | {type: 'register' as const, register: DIV_UPPER_REGISTER} 27 | 28 | export const FLOAT_INTERMEDIATE_REGISTERS: Register[] = 29 | ['xmm0', 'xmm1', 'xmm15'] 30 | export const [FLOAT_RESULT_REGISTER] = FLOAT_INTERMEDIATE_REGISTERS 31 | export const FLOAT_GENERAL_REGISTERS = makeArray( 32 | 16 - FLOAT_INTERMEDIATE_REGISTERS.length, 33 | i => `xmm${i + 2}` as Register 34 | ) 35 | 36 | export const getGeneralRegisters = (float: boolean): Register[] => 37 | float ? FLOAT_GENERAL_REGISTERS : INT_GENERAL_REGISTERS 38 | export const getIntermediateRegisters = (float: boolean): Register[] => 39 | float ? FLOAT_INTERMEDIATE_REGISTERS : INT_INTERMEDIATE_REGISTERS 40 | export const getResultRegister = (float: boolean): Register => 41 | float ? FLOAT_RESULT_REGISTER : INT_RESULT_REGISTER 42 | 43 | export const SYSV_INT_PARAM_REGISTERS: Register[] = 44 | ['rdi', 'rsi', 'rdx', 'rcx', 'r8', 'r9'] 45 | export const SYSV_FLOAT_PARAM_REGISTERS = makeArray(8, i => `xmm${i}` as Register) 46 | export const SYSV_CALLEE_SAVE_REGISTERS: Register[] = 47 | ['rbx', 'rbp', 'r12', 'r13', 'r14', 'r15'] 48 | export const SYSV_CALLEE_SAVE_SET = new Set(SYSV_CALLEE_SAVE_REGISTERS) 49 | // %rax and %xmm15 are not used for SysV params and are intermediate registers 50 | export const SYSV_UNUSED_REGISTERS: Register[] = ['rax', 'xmm15'] 51 | 52 | export const STACK_TOP = {type: 'indirect' as const, register: 'rsp' as Register} 53 | 54 | // Registers clobbered by mmap() syscall. 55 | // These are the same 6 argument registers plus %rcx and %r11 on Linux and macOS. 56 | export const MMAP_SYSCALL_REGISTERS = new Set( 57 | new Array('rax', 'rdi', 'rsi', 'rdx', 'r10', 'r8', 'r9', 'rcx', 'r11') 58 | .filter(register => INT_GENERAL_REGISTERS.includes(register)) 59 | ) 60 | export const SYSCALL_DATUM: Datum = {type: 'register', register: 'rax'}, 61 | MMAP_ADDR_DATUM = {type: 'register' as const, register: 'rdi' as Register}, 62 | MMAP_ADDR_INTERMEDIATE: Datum = 63 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[2]}, 64 | MMAP_LENGTH_DATUM = {type: 'register' as const, register: 'rsi' as Register}, 65 | MMAP_PROT_DATUM: Datum = {type: 'register', register: 'rdx'}, 66 | MMAP_FLAGS_DATUM: Datum = {type: 'register', register: 'r10'}, 67 | MMAP_FD_DATUM: Datum = {type: 'register', register: 'r8'}, 68 | MMAP_OFFSET_DATUM: Datum = {type: 'register', register: 'r9'}, 69 | MMAP_RESULT_DATUM: Datum = {type: 'register', register: 'rax'} 70 | export const PAGE_BITS: Datum = {type: 'immediate', value: 16} 71 | // PROT_READ | PROT_WRITE 72 | export const PROT_READ_WRITE: Datum = {type: 'immediate', value: 0x1 | 0x2} 73 | const MAP_ANONYMOUS = isLinux ? 0x20 : 0x1000 74 | // MAP_SHARED | MAP_FIXED | MAP_ANONYMOUS 75 | export const MMAP_FLAGS: Datum = {type: 'immediate', value: 0x01 | 0x10 | MAP_ANONYMOUS} 76 | 77 | const DARWIN_SYSCALL_OFFSET = 0x2000000 78 | export const SYSCALL = isLinux 79 | ? {exit: 60, mmap: 9} 80 | : {exit: DARWIN_SYSCALL_OFFSET + 1, mmap: DARWIN_SYSCALL_OFFSET + 197} 81 | 82 | export const INVALID_EXPORT_CHAR = /[^A-Za-z\d_]/g 83 | 84 | export function typeWidth(type: ValueType): Width { 85 | switch (type) { 86 | case 'i32': return 'l' 87 | case 'i64': return 'q' 88 | case 'f32': return 's' 89 | case 'f64': return 'd' 90 | } 91 | } 92 | export const isFloat = (type: ValueType): boolean => type[0] === 'f' -------------------------------------------------------------------------------- /compile/drop-select.ts: -------------------------------------------------------------------------------- 1 | import {CompilationContext} from './context' 2 | import {INT_INTERMEDIATE_REGISTERS, STACK_TOP} from './conventions' 3 | import {shrinkStack} from './helpers' 4 | import * as asm from './x86_64-asm' 5 | 6 | export function compileDropInstruction( 7 | context: CompilationContext, 8 | output: asm.AssemblyInstruction[] 9 | ): void { 10 | if (!context.resolvePop()) output.push(shrinkStack(1)) 11 | } 12 | export function compileSelectInstruction( 13 | context: CompilationContext, 14 | output: asm.AssemblyInstruction[] 15 | ): void { 16 | let cond = context.resolvePop() 17 | if (!cond) { 18 | cond = INT_INTERMEDIATE_REGISTERS[0] 19 | output.push(new asm.PopInstruction({type: 'register', register: cond})) 20 | } 21 | const condDatum: asm.Datum = {type: 'register', register: cond, width: 'l'} 22 | const float = context.peek() 23 | const ifFalse = context.resolvePop() 24 | let ifFalseDatum: asm.Datum 25 | if (ifFalse) { 26 | ifFalseDatum = {type: 'register', register: ifFalse} 27 | if (float) { 28 | // Put floats in an int register so we can use a cmov instruction 29 | const newIfFalse = INT_INTERMEDIATE_REGISTERS[1] 30 | const newDatum: asm.Datum = {type: 'register', register: newIfFalse} 31 | output.push(new asm.MoveInstruction(ifFalseDatum, newDatum, 'q')) 32 | ifFalseDatum = newDatum 33 | } 34 | } 35 | else ifFalseDatum = STACK_TOP 36 | const ifTrue = context.resolvePop() // also where the result will go 37 | const ifTrueDatum: asm.Datum = 38 | ifTrue ? {type: 'register', register: ifTrue} : {...STACK_TOP, immediate: 8} 39 | let ifTrueNewDatum: asm.Datum | undefined 40 | if (float || !ifTrue) { 41 | // Move result to an int register so we can use a cmov instruction 42 | const newIfTrue = INT_INTERMEDIATE_REGISTERS[2] 43 | ifTrueNewDatum = {type: 'register', register: newIfTrue} 44 | output.push(new asm.MoveInstruction(ifTrueDatum, ifTrueNewDatum, 'q')) 45 | } 46 | output.push( 47 | new asm.TestInstruction(condDatum, condDatum), 48 | new asm.CMoveInstruction(ifFalseDatum, ifTrueNewDatum || ifTrueDatum, 'e') 49 | ) 50 | if (ifTrueNewDatum) { 51 | output.push(new asm.MoveInstruction(ifTrueNewDatum, ifTrueDatum, 'q')) 52 | } 53 | context.push(float) 54 | if (!ifFalse) output.push(shrinkStack(1)) 55 | } -------------------------------------------------------------------------------- /compile/helpers.ts: -------------------------------------------------------------------------------- 1 | import {ValueType} from '../parse/value-type' 2 | import {CompilationContext, SPRelative} from './context' 3 | import { 4 | INT_INTERMEDIATE_REGISTERS, 5 | SYSV_UNUSED_REGISTERS, 6 | getIntermediateRegisters, 7 | getResultRegister, 8 | isFloat, 9 | typeWidth 10 | } from './conventions' 11 | import * as asm from './x86_64-asm' 12 | 13 | export type ParamTarget = asm.Register | SPRelative 14 | interface EvictedState { 15 | original: asm.Register 16 | current: asm.Register 17 | } 18 | interface RelocationResult { 19 | toRestore: asm.Register[] 20 | output: asm.AssemblyInstruction[] 21 | } 22 | 23 | export const shrinkStack = (count: number): asm.AssemblyInstruction => 24 | new asm.AddInstruction( 25 | {type: 'immediate', value: count << 3}, 26 | {type: 'register', register: 'rsp'} 27 | ) 28 | export const growStack = (count: number): asm.AssemblyInstruction => 29 | shrinkStack(-count) 30 | function unwindStack( 31 | intStackHeight: number, 32 | floatStackHeight: number, 33 | context: CompilationContext, 34 | output: asm.AssemblyInstruction[] 35 | ) { 36 | let popCount = 0 37 | while ( 38 | context.getStackHeight(false) > intStackHeight || 39 | context.getStackHeight(true) > floatStackHeight 40 | ) { 41 | if (!context.resolvePop()) popCount++ 42 | } 43 | if (popCount) output.push(shrinkStack(popCount)) 44 | } 45 | 46 | export function relocateArguments( 47 | moves: Map, 48 | stackParams: ParamTarget[], 49 | saveRegisters: Set, 50 | stackOffset = 0 51 | ): RelocationResult { 52 | let evicted: EvictedState | undefined 53 | const toRestore: asm.Register[] = [] 54 | const output: (() => asm.AssemblyInstruction)[] = [] 55 | while (moves.size) { 56 | let source: asm.Register, target: ParamTarget 57 | if (evicted) { 58 | const {original, current} = evicted 59 | source = current 60 | const maybeTarget = moves.get(original) 61 | if (!maybeTarget) throw new Error('Expected a parameter target') 62 | target = maybeTarget 63 | moves.delete(original) 64 | } 65 | else { 66 | // TODO: optimize order of moves to avoid conflicts 67 | [[source, target]] = moves 68 | moves.delete(source) 69 | } 70 | let needsMove = true 71 | evicted = undefined 72 | if (!(target instanceof SPRelative)) { 73 | if (moves.has(target)) { 74 | const evictTo = SYSV_UNUSED_REGISTERS 75 | .find(register => register !== source)! 76 | evicted = {original: target, current: evictTo} 77 | const move = new asm.MoveInstruction( 78 | {type: 'register', register: target}, 79 | {type: 'register', register: evictTo}, 80 | 'q' 81 | ) 82 | output.push(() => move) 83 | } 84 | if (saveRegisters.has(target)) toRestore.push(target) 85 | if (source === target) needsMove = false 86 | } 87 | if (needsMove) { 88 | const move = new asm.MoveInstruction( 89 | {type: 'register', register: source}, 90 | target instanceof SPRelative 91 | ? target.datum 92 | : {type: 'register', register: target}, 93 | 'q' 94 | ) 95 | output.push(() => move) 96 | } 97 | } 98 | stackParams.forEach((target, i) => { 99 | let register: asm.Register 100 | if (target instanceof SPRelative) [register] = INT_INTERMEDIATE_REGISTERS 101 | else { 102 | register = target 103 | if (saveRegisters.has(target)) toRestore.push(target) 104 | } 105 | const datum: asm.Datum = {type: 'register', register} 106 | output.push(() => new asm.MoveInstruction( 107 | new SPRelative(stackOffset + savedValues + i).datum, datum) 108 | ) 109 | if (target instanceof SPRelative) { 110 | const move = new asm.MoveInstruction(datum, target.datum) 111 | output.push(() => move) 112 | } 113 | }) 114 | const savedValues = toRestore.length 115 | return {toRestore, output: output.map(instruction => instruction())} 116 | } 117 | export function compileBranch( 118 | nesting: number, 119 | context: CompilationContext, 120 | output: asm.AssemblyInstruction[] 121 | ): string | undefined { 122 | const block = context.getNestedLabel(nesting) 123 | if (!block) { 124 | compileReturn(context, output) 125 | return 126 | } 127 | 128 | const {loop, label, intStackHeight, floatStackHeight, result} = block 129 | let resultRegister: asm.Register | undefined 130 | let float: boolean 131 | const saveResult = !(loop || !result) 132 | if (saveResult) { 133 | float = isFloat(result!) 134 | const toPop = context.resolvePop() 135 | if ( 136 | // If the relevant stack needs to be unwound, 137 | context.getStackHeight(float) > (float ? floatStackHeight : intStackHeight) || 138 | // or if the other stack needs to be unwound and the result value is on the stack 139 | context.getStackHeight(!float) > (float ? intStackHeight : floatStackHeight) 140 | && context.getValuesOnStack(float) 141 | ) { 142 | // Result is going to be moved 143 | [resultRegister] = getIntermediateRegisters(float) 144 | const resultDatum: asm.Datum = {type: 'register', register: resultRegister} 145 | output.push(toPop 146 | ? new asm.MoveInstruction({type: 'register', register: toPop}, resultDatum, 'q') 147 | : new asm.PopInstruction(resultDatum) 148 | ) 149 | } 150 | } 151 | unwindStack(intStackHeight, floatStackHeight, context, output) 152 | if (saveResult) { 153 | const target = context.resolvePush(float!) 154 | if (resultRegister) { 155 | const resultDatum: asm.Datum = {type: 'register', register: resultRegister} 156 | output.push(target 157 | ? new asm.MoveInstruction(resultDatum, {type: 'register', register: target}, 'q') 158 | : new asm.PushInstruction(resultDatum) 159 | ) 160 | } 161 | } 162 | output.push(new asm.JumpInstruction(label)) 163 | return label 164 | } 165 | export function popResultAndUnwind( 166 | context: CompilationContext, 167 | output: asm.AssemblyInstruction[] 168 | ): void { 169 | const {result} = context 170 | if (result) { 171 | const register = context.resolvePop() 172 | const resultDatum: asm.Datum = 173 | {type: 'register', register: getResultRegister(isFloat(result))} 174 | output.push(register 175 | ? new asm.MoveInstruction({type: 'register', register}, resultDatum, 'q') 176 | : new asm.PopInstruction(resultDatum) 177 | ) 178 | } 179 | unwindStack(0, 0, context, output) 180 | } 181 | export function compileReturn( 182 | context: CompilationContext, 183 | output: asm.AssemblyInstruction[] 184 | ): void { 185 | popResultAndUnwind(context, output) 186 | output.push(new asm.JumpInstruction( 187 | context.moduleContext.returnLabel(context.functionIndex) 188 | )) 189 | } 190 | 191 | export function memoryMoveWidth(type: ValueType, intermediate: boolean): asm.Width { 192 | if (isFloat(type) && intermediate) type = 'i' + type.slice(1) as ValueType 193 | return typeWidth(type) 194 | } -------------------------------------------------------------------------------- /compile/instructions.ts: -------------------------------------------------------------------------------- 1 | import {Instruction} from '../parse/instruction' 2 | import {CompilationContext} from './context' 3 | import { 4 | ALWAYS_BRANCHES, 5 | BranchResult, 6 | compileBlockInstruction, 7 | compileBranchInstruction, 8 | compileBranchTableInstruction, 9 | compileCallInstruction, 10 | compileIfInstruction, 11 | NEVER_BRANCHES, 12 | UNREACHABLE_INSTRUCTIONS 13 | } from './control' 14 | import {compileDropInstruction, compileSelectInstruction} from './drop-select' 15 | import {compileReturn} from './helpers' 16 | import { 17 | compileGrowInstruction, 18 | compileLoadInstruction, 19 | compileSizeInstruction, 20 | compileStoreInstruction 21 | } from './memory' 22 | import { 23 | compileBitCountInstruction, 24 | compileCompareInstruction, 25 | compileConstInstruction, 26 | compileConvertInstruction, 27 | compileExtendInstruction, 28 | compileFConvertInstruction, 29 | compileFloatBinaryInstruction, 30 | compileFloatUnaryInstruction, 31 | compileIntArithmeticInstruction, 32 | compileIntDivInstruction, 33 | compileIntMulInstruction, 34 | compileMinMaxInstruction, 35 | compileReinterpretInstruction, 36 | compileSignInstruction, 37 | compileTruncateInstruction, 38 | compileWrapInstruction 39 | } from './numeric' 40 | import { 41 | compileGetGlobalInstruction, 42 | compileGetLocalInstruction, 43 | compileSetGlobalInstruction, 44 | compileStoreLocalInstruction 45 | } from './variable' 46 | import * as asm from './x86_64-asm' 47 | 48 | function compileInstruction( 49 | instruction: Instruction, 50 | context: CompilationContext, 51 | output: asm.AssemblyInstruction[] 52 | ): BranchResult { 53 | // Uncomment these lines to show the wasm instruction 54 | // corresponding to each assembly instruction 55 | // output.push(new asm.Comment( 56 | // JSON.stringify(instruction, (_, v) => typeof v === 'bigint' ? String(v) : v) 57 | // )) 58 | switch (instruction.type) { 59 | case 'unreachable': 60 | output.push(...UNREACHABLE_INSTRUCTIONS) 61 | return ALWAYS_BRANCHES 62 | case 'nop': 63 | case 'i64.extend_u': // 32-bit values are already stored with upper bits zeroed 64 | break 65 | case 'block': 66 | case 'loop': 67 | return compileBlockInstruction(instruction, context, output) 68 | case 'if': 69 | return compileIfInstruction(instruction, context, output) 70 | case 'br': 71 | case 'br_if': 72 | return compileBranchInstruction(instruction, context, output) 73 | case 'br_table': 74 | return compileBranchTableInstruction(instruction, context, output) 75 | case 'call': 76 | case 'call_indirect': 77 | compileCallInstruction(instruction, context, output) 78 | break 79 | case 'return': 80 | compileReturn(context, output) 81 | return ALWAYS_BRANCHES 82 | case 'drop': 83 | compileDropInstruction(context, output) 84 | break 85 | case 'select': 86 | compileSelectInstruction(context, output) 87 | break 88 | case 'get_local': 89 | compileGetLocalInstruction(instruction.local, context, output) 90 | break 91 | case 'set_local': 92 | case 'tee_local': 93 | compileStoreLocalInstruction(instruction, context, output) 94 | break 95 | case 'get_global': 96 | compileGetGlobalInstruction(instruction.global, context, output) 97 | break 98 | case 'set_global': 99 | compileSetGlobalInstruction(instruction.global, context, output) 100 | break 101 | case 'i32.load': 102 | case 'i64.load': 103 | case 'f32.load': 104 | case 'f64.load': 105 | case 'i32.load8_s': 106 | case 'i32.load8_u': 107 | case 'i32.load16_s': 108 | case 'i32.load16_u': 109 | case 'i64.load8_s': 110 | case 'i64.load8_u': 111 | case 'i64.load16_s': 112 | case 'i64.load16_u': 113 | case 'i64.load32_s': 114 | case 'i64.load32_u': 115 | compileLoadInstruction(instruction, context, output) 116 | break 117 | case 'i32.store': 118 | case 'i64.store': 119 | case 'f32.store': 120 | case 'f64.store': 121 | case 'i32.store8': 122 | case 'i32.store16': 123 | case 'i64.store8': 124 | case 'i64.store16': 125 | case 'i64.store32': 126 | compileStoreInstruction(instruction, context, output) 127 | break 128 | case 'memory.size': 129 | compileSizeInstruction(context, output) 130 | break 131 | case 'memory.grow': 132 | compileGrowInstruction(context, output) 133 | break 134 | case 'i32.const': 135 | case 'i64.const': 136 | case 'f32.const': 137 | case 'f64.const': 138 | compileConstInstruction(instruction, context, output) 139 | break 140 | case 'i32.eqz': 141 | case 'i32.eq': 142 | case 'i32.ne': 143 | case 'i32.lt_s': 144 | case 'i32.lt_u': 145 | case 'i32.le_s': 146 | case 'i32.le_u': 147 | case 'i32.gt_s': 148 | case 'i32.gt_u': 149 | case 'i32.ge_s': 150 | case 'i32.ge_u': 151 | case 'i64.eqz': 152 | case 'i64.eq': 153 | case 'i64.ne': 154 | case 'i64.lt_s': 155 | case 'i64.lt_u': 156 | case 'i64.le_s': 157 | case 'i64.le_u': 158 | case 'i64.gt_s': 159 | case 'i64.gt_u': 160 | case 'i64.ge_s': 161 | case 'i64.ge_u': 162 | case 'f32.eq': 163 | case 'f32.ne': 164 | case 'f32.lt': 165 | case 'f32.gt': 166 | case 'f32.le': 167 | case 'f32.ge': 168 | case 'f64.eq': 169 | case 'f64.ne': 170 | case 'f64.lt': 171 | case 'f64.gt': 172 | case 'f64.le': 173 | case 'f64.ge': 174 | compileCompareInstruction(instruction.type, context, output) 175 | break 176 | case 'i32.clz': 177 | case 'i32.ctz': 178 | case 'i32.popcnt': 179 | case 'i64.clz': 180 | case 'i64.ctz': 181 | case 'i64.popcnt': 182 | compileBitCountInstruction(instruction.type, context, output) 183 | break 184 | case 'i32.add': 185 | case 'i32.sub': 186 | case 'i32.and': 187 | case 'i32.or': 188 | case 'i32.xor': 189 | case 'i32.shl': 190 | case 'i32.shr_s': 191 | case 'i32.shr_u': 192 | case 'i32.rotl': 193 | case 'i32.rotr': 194 | case 'i64.add': 195 | case 'i64.sub': 196 | case 'i64.and': 197 | case 'i64.or': 198 | case 'i64.xor': 199 | case 'i64.shl': 200 | case 'i64.shr_s': 201 | case 'i64.shr_u': 202 | case 'i64.rotl': 203 | case 'i64.rotr': 204 | compileIntArithmeticInstruction(instruction.type, context, output) 205 | break 206 | case 'i32.mul': 207 | case 'i64.mul': 208 | compileIntMulInstruction(instruction.type, context, output) 209 | break 210 | case 'i32.div_s': 211 | case 'i32.div_u': 212 | case 'i32.rem_s': 213 | case 'i32.rem_u': 214 | case 'i64.div_s': 215 | case 'i64.div_u': 216 | case 'i64.rem_s': 217 | case 'i64.rem_u': 218 | compileIntDivInstruction(instruction.type, context, output) 219 | break 220 | case 'f32.abs': 221 | case 'f32.neg': 222 | case 'f32.copysign': 223 | case 'f64.abs': 224 | case 'f64.neg': 225 | case 'f64.copysign': 226 | compileSignInstruction(instruction.type, context, output) 227 | break 228 | case 'f32.ceil': 229 | case 'f32.floor': 230 | case 'f32.trunc': 231 | case 'f32.nearest': 232 | case 'f32.sqrt': 233 | case 'f64.ceil': 234 | case 'f64.floor': 235 | case 'f64.trunc': 236 | case 'f64.nearest': 237 | case 'f64.sqrt': 238 | compileFloatUnaryInstruction(instruction.type, context, output) 239 | break 240 | case 'f32.add': 241 | case 'f32.sub': 242 | case 'f32.mul': 243 | case 'f32.div': 244 | case 'f64.add': 245 | case 'f64.sub': 246 | case 'f64.mul': 247 | case 'f64.div': 248 | compileFloatBinaryInstruction(instruction.type, context, output) 249 | break 250 | case 'f32.min': 251 | case 'f32.max': 252 | case 'f64.min': 253 | case 'f64.max': 254 | compileMinMaxInstruction(instruction.type, context, output) 255 | break 256 | case 'i32.wrap': 257 | compileWrapInstruction(context, output) 258 | break 259 | case 'i32.trunc_s/f32': 260 | case 'i32.trunc_u/f32': 261 | case 'i32.trunc_s/f64': 262 | case 'i32.trunc_u/f64': 263 | case 'i64.trunc_s/f32': 264 | case 'i64.trunc_u/f32': 265 | case 'i64.trunc_s/f64': 266 | case 'i64.trunc_u/f64': 267 | compileTruncateInstruction(instruction.type, context, output) 268 | break 269 | case 'i64.extend_s': 270 | compileExtendInstruction(context, output) 271 | break 272 | case 'f32.convert_s/i32': 273 | case 'f32.convert_u/i32': 274 | case 'f32.convert_s/i64': 275 | case 'f32.convert_u/i64': 276 | case 'f64.convert_s/i32': 277 | case 'f64.convert_u/i32': 278 | case 'f64.convert_s/i64': 279 | case 'f64.convert_u/i64': 280 | compileConvertInstruction(instruction.type, context, output) 281 | break 282 | case 'f32.demote': 283 | case 'f64.promote': 284 | compileFConvertInstruction(instruction.type, context, output) 285 | break 286 | case 'i32.reinterpret': 287 | case 'i64.reinterpret': 288 | case 'f32.reinterpret': 289 | case 'f64.reinterpret': 290 | compileReinterpretInstruction(instruction.type, context, output) 291 | break 292 | default: 293 | const unreachable: never = instruction 294 | unreachable 295 | } 296 | return NEVER_BRANCHES 297 | } 298 | export function compileInstructions( 299 | instructions: Instruction[], 300 | context: CompilationContext, 301 | output: asm.AssemblyInstruction[] 302 | ): BranchResult { 303 | const allBranches = new Set() 304 | for (const instruction of instructions) { 305 | const {branches, definitely} = compileInstruction(instruction, context, output) 306 | for (const label of branches) allBranches.add(label) 307 | if (definitely) return {branches: allBranches, definitely} 308 | } 309 | return {branches: allBranches, definitely: false} 310 | } -------------------------------------------------------------------------------- /compile/memory.ts: -------------------------------------------------------------------------------- 1 | import {LoadStoreInstruction} from '../parse/instruction' 2 | import {ValueType} from '../parse/value-type' 3 | import {reverse} from '../util' 4 | import {CompilationContext} from './context' 5 | import { 6 | INT_INTERMEDIATE_REGISTERS, 7 | MMAP_ADDR_DATUM, 8 | MMAP_ADDR_INTERMEDIATE, 9 | MMAP_FD_DATUM, 10 | MMAP_FLAGS, 11 | MMAP_FLAGS_DATUM, 12 | MMAP_LENGTH_DATUM, 13 | MMAP_OFFSET_DATUM, 14 | MMAP_PROT_DATUM, 15 | MMAP_RESULT_DATUM, 16 | MMAP_SYSCALL_REGISTERS, 17 | PAGE_BITS, 18 | PROT_READ_WRITE, 19 | SYSCALL, 20 | SYSCALL_DATUM, 21 | isFloat 22 | } from './conventions' 23 | import {memoryMoveWidth} from './helpers' 24 | import * as asm from './x86_64-asm' 25 | 26 | export function compileLoadInstruction( 27 | {type, access: {offset}}: LoadStoreInstruction, 28 | context: CompilationContext, 29 | output: asm.AssemblyInstruction[] 30 | ): void { 31 | let index = context.resolvePop() 32 | if (!index) { 33 | index = INT_INTERMEDIATE_REGISTERS[0] 34 | output.push(new asm.PopInstruction({type: 'register', register: index})) 35 | } 36 | const address: asm.Datum = 37 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[1]} 38 | // Constant offset can exceed 32 bits, so must store in register 39 | output.push(new asm.MoveInstruction( 40 | {type: 'immediate', value: context.moduleContext.memoryStart + offset}, 41 | address 42 | )) 43 | const source: asm.Datum = 44 | {type: 'indirect', register: address.register, offset: {register: index}} 45 | const [resultType, operation] = type.split('.') as [ValueType, string] 46 | const isLoad32U = operation === 'load32_u' 47 | let target = context.resolvePush(isFloat(resultType)) 48 | const push = !target 49 | if (push && type.endsWith('64.load')) output.push(new asm.PushInstruction(source)) 50 | else { 51 | if (push) [target] = INT_INTERMEDIATE_REGISTERS // push requires an int register 52 | const width = isLoad32U ? 'l' : memoryMoveWidth(resultType, push) 53 | const targetDatum: asm.Datum = {type: 'register', register: target!} 54 | const targetWidth = {...targetDatum, width} 55 | if (operation === 'load' || isLoad32U) { // no resize needed 56 | output.push(new asm.MoveInstruction(source, targetWidth, width)) 57 | } 58 | else { 59 | const [op, signedness] = operation.split('_') 60 | let sourceWidth: asm.Width 61 | switch (op) { 62 | case 'load8': 63 | sourceWidth = 'b' 64 | break 65 | case 'load16': 66 | sourceWidth = 'w' 67 | break 68 | default: // 'load32' 69 | sourceWidth = 'l' 70 | } 71 | output.push(new asm.MoveExtendInstruction( 72 | source, targetWidth, signedness === 's', {src: sourceWidth, dest: width} 73 | )) 74 | } 75 | if (push) output.push(new asm.PushInstruction(targetDatum)) 76 | } 77 | } 78 | export function compileStoreInstruction( 79 | {type, access: {offset}}: LoadStoreInstruction, 80 | context: CompilationContext, 81 | output: asm.AssemblyInstruction[] 82 | ): void { 83 | const [sourceType, operation] = type.split('.') as [ValueType, string] 84 | let value = context.resolvePop() 85 | const pop = !value 86 | if (pop) { 87 | value = INT_INTERMEDIATE_REGISTERS[0] // pop requires an int register 88 | output.push(new asm.PopInstruction({type: 'register', register: value})) 89 | } 90 | let index = context.resolvePop() 91 | if (!index) { 92 | index = INT_INTERMEDIATE_REGISTERS[1] 93 | output.push(new asm.PopInstruction({type: 'register', register: index})) 94 | } 95 | const address: asm.Datum = 96 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[2]} 97 | output.push(new asm.MoveInstruction( 98 | {type: 'immediate', value: context.moduleContext.memoryStart + offset}, 99 | address 100 | )) 101 | let width: asm.Width 102 | switch (operation) { 103 | case 'store8': 104 | width = 'b' 105 | break 106 | case 'store16': 107 | width = 'w' 108 | break 109 | case 'store32': 110 | width = 'l' 111 | break 112 | default: 113 | width = memoryMoveWidth(sourceType, pop) 114 | } 115 | output.push(new asm.MoveInstruction( 116 | {type: 'register', register: value!, width}, 117 | {type: 'indirect', register: address.register, offset: {register: index}}, 118 | width 119 | )) 120 | } 121 | export function compileSizeInstruction( 122 | context: CompilationContext, 123 | output: asm.AssemblyInstruction[] 124 | ): void { 125 | const target = context.resolvePush(false) 126 | const sizeDatum: asm.Datum = 127 | {type: 'label', label: context.moduleContext.memorySizeLabel} 128 | output.push(target 129 | ? new asm.MoveInstruction(sizeDatum, {type: 'register', register: target}) 130 | : new asm.PushInstruction(sizeDatum) 131 | ) 132 | } 133 | export function compileGrowInstruction( 134 | context: CompilationContext, 135 | output: asm.AssemblyInstruction[] 136 | ): void { 137 | let pages = context.resolvePop() 138 | if (!pages || MMAP_SYSCALL_REGISTERS.has(pages)) { 139 | // %rax and %rdx are used for mmap() arguments 140 | const newPages = INT_INTERMEDIATE_REGISTERS[1] 141 | const newPagesDatum: asm.Datum = {type: 'register', register: newPages} 142 | output.push(pages 143 | ? new asm.MoveInstruction({type: 'register', register: pages}, newPagesDatum) 144 | : new asm.PopInstruction(newPagesDatum) 145 | ) 146 | pages = newPages 147 | } 148 | const pagesDatum: asm.Datum = {type: 'register', register: pages, width: 'l'} 149 | const totalPagesDatum: asm.Datum = 150 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0], width: 'l'} 151 | const {moduleContext} = context 152 | const sizeLabel: asm.Datum = {type: 'label', label: moduleContext.memorySizeLabel} 153 | const failLabel = context.makeLabel('MMAP_FAIL'), 154 | skipLabel = context.makeLabel('MMAP_SKIP'), 155 | endLabel = context.makeLabel('MMAP_END') 156 | output.push( 157 | new asm.TestInstruction(pagesDatum, pagesDatum), 158 | new asm.JumpInstruction(skipLabel, 'e'), // mmap() of 0 pages isn't allowed 159 | new asm.MoveInstruction(sizeLabel, totalPagesDatum), 160 | new asm.AddInstruction(pagesDatum, totalPagesDatum), 161 | new asm.CmpInstruction( 162 | {type: 'immediate', value: moduleContext.maxPages}, totalPagesDatum 163 | ), 164 | new asm.JumpInstruction(failLabel, 'a') 165 | ) 166 | const registersUsed = new Set(context.registersUsed()) 167 | const pushed: asm.Datum[] = [{type: 'register', register: pages}] 168 | for (const register of MMAP_SYSCALL_REGISTERS) { 169 | if (registersUsed.has(register)) pushed.push({type: 'register', register}) 170 | } 171 | for (const datum of pushed) output.push(new asm.PushInstruction(datum)) 172 | output.push( 173 | new asm.MoveInstruction( 174 | {type: 'immediate', value: SYSCALL.mmap}, SYSCALL_DATUM 175 | ), 176 | new asm.MoveInstruction(sizeLabel, {...MMAP_ADDR_DATUM, width: 'l'}), 177 | new asm.ShlInstruction(PAGE_BITS, MMAP_ADDR_DATUM), 178 | new asm.MoveInstruction( // must mov 64-bit immediate to intermediate register 179 | {type: 'immediate', value: context.moduleContext.memoryStart}, 180 | MMAP_ADDR_INTERMEDIATE 181 | ), 182 | new asm.AddInstruction(MMAP_ADDR_INTERMEDIATE, MMAP_ADDR_DATUM), 183 | new asm.MoveInstruction(pagesDatum, {...MMAP_LENGTH_DATUM, width: 'l'}), 184 | new asm.ShlInstruction(PAGE_BITS, MMAP_LENGTH_DATUM), 185 | new asm.MoveInstruction(PROT_READ_WRITE, MMAP_PROT_DATUM), 186 | new asm.MoveInstruction(MMAP_FLAGS, MMAP_FLAGS_DATUM), 187 | new asm.MoveInstruction({type: 'immediate', value: -1}, MMAP_FD_DATUM), 188 | new asm.XorInstruction(MMAP_OFFSET_DATUM, MMAP_OFFSET_DATUM), 189 | new asm.SysCallInstruction 190 | ) 191 | for (const datum of reverse(pushed)) output.push(new asm.PopInstruction(datum)) 192 | let result = context.resolvePush(false) 193 | const push = !result 194 | if (push) [result] = INT_INTERMEDIATE_REGISTERS 195 | const resultDatum: asm.Datum = {type: 'register', register: result!, width: 'l'} 196 | output.push( 197 | // mmap() < 0 indicates failure 198 | new asm.TestInstruction(MMAP_RESULT_DATUM, MMAP_RESULT_DATUM), 199 | new asm.JumpInstruction(failLabel, 'l'), 200 | new asm.MoveInstruction(sizeLabel, resultDatum), 201 | new asm.AddInstruction(pagesDatum, sizeLabel), 202 | new asm.JumpInstruction(endLabel), 203 | new asm.Label(failLabel), 204 | new asm.MoveInstruction({type: 'immediate', value: -1}, resultDatum), 205 | new asm.JumpInstruction(endLabel), 206 | new asm.Label(skipLabel), 207 | new asm.MoveInstruction(sizeLabel, resultDatum), 208 | new asm.Label(endLabel) 209 | ) 210 | if (push) output.push(new asm.PushInstruction(resultDatum)) 211 | } -------------------------------------------------------------------------------- /compile/module.ts: -------------------------------------------------------------------------------- 1 | import {Instruction} from '../parse/instruction' 2 | import {FunctionType, MemoryInitializer, Section, TableInitializer} from '../parse/module' 3 | import {ValueType} from '../parse/value-type' 4 | import {reverse} from '../util' 5 | import {compileInstructions} from './instructions' 6 | import {FunctionDeclaration, getCType, GlobalDeclaration, HeaderDeclaration} from './c-header' 7 | import {CompilationContext, getFunctionStats, ModuleContext, ModuleIndices, SPRelative} from './context' 8 | import { 9 | INT_INTERMEDIATE_REGISTERS, 10 | INVALID_EXPORT_CHAR, 11 | SYSV_CALLEE_SAVE_REGISTERS, 12 | SYSV_CALLEE_SAVE_SET, 13 | SYSV_FLOAT_PARAM_REGISTERS, 14 | SYSV_INT_PARAM_REGISTERS, 15 | isLinux 16 | } from './conventions' 17 | import {growStack, ParamTarget, popResultAndUnwind, relocateArguments, shrinkStack} from './helpers' 18 | import * as asm from './x86_64-asm' 19 | 20 | interface GlobalDirective { 21 | align: asm.Directive 22 | data: asm.Directive 23 | } 24 | const WASM_TYPE_DIRECTIVES = new Map([ 25 | ['i32', { 26 | align: new asm.Directive({type: 'balign', args: [4]}), 27 | data: new asm.Directive({type: 'long', args: [0]}) 28 | }], 29 | ['i64', { 30 | align: new asm.Directive({type: 'balign', args: [8]}), 31 | data: new asm.Directive({type: 'quad', args: [0]}) 32 | }] 33 | ]) 34 | WASM_TYPE_DIRECTIVES.set('f32', WASM_TYPE_DIRECTIVES.get('i32')!) 35 | WASM_TYPE_DIRECTIVES.set('f64', WASM_TYPE_DIRECTIVES.get('i64')!) 36 | 37 | export interface Module { 38 | module: Section[] 39 | index: number 40 | moduleIndices: ModuleIndices 41 | moduleName: string 42 | } 43 | interface ParsedModule { 44 | context: ModuleContext 45 | moduleName: string 46 | tableLengths: number[] 47 | memoryMin?: number 48 | memoryInitializers: MemoryInitializer[] 49 | globalInitializers: Instruction[][] 50 | tableInitializers: TableInitializer[] 51 | startFunction?: number 52 | functionTypes: FunctionType[] 53 | functionBodies: Instruction[][] 54 | } 55 | export interface CompiledModule { 56 | instructions: asm.AssemblyInstruction[] 57 | declarations: HeaderDeclaration[] 58 | } 59 | 60 | export function parseSections({module, index, moduleIndices, moduleName}: Module): ParsedModule { 61 | const context = new ModuleContext(index, moduleIndices) 62 | const functionTypes: FunctionType[] = [] 63 | const functionLocals: ValueType[][] = [] 64 | const tableLengths: number[] = [] 65 | let memoriesCount = 0 66 | let memoryMin: number | undefined 67 | const memoryInitializers: MemoryInitializer[] = [] 68 | const globalInitializers: Instruction[][] = [] 69 | const tableInitializers: TableInitializer[] = [] 70 | let startFunction: number | undefined 71 | const functionBodies: Instruction[][] = [] 72 | for (const section of module) { 73 | switch (section.type) { 74 | case 'type': 75 | context.addTypes(section.types) 76 | break 77 | case 'import': 78 | context.addImports(section.imports) 79 | break 80 | case 'function': 81 | for (const index of section.typeIndices) { 82 | functionTypes.push(context.getType(index)) 83 | } 84 | break 85 | case 'table': 86 | for (const {min} of section.tables) tableLengths.push(min) 87 | break 88 | case 'memory': 89 | const {memories} = section 90 | memoriesCount += memories.length 91 | if (!memoriesCount) break 92 | if (memoriesCount > 1) throw new Error('Multiple memories') 93 | const [{min, max}] = memories 94 | memoryMin = min 95 | context.setMemoryMax(max) 96 | break 97 | case 'global': 98 | const {globals} = section 99 | context.addGlobals(globals) 100 | for (const {initializer} of globals) { 101 | globalInitializers.push(initializer) 102 | } 103 | break 104 | case 'export': 105 | context.addExports(section.exports) 106 | break 107 | case 'start': 108 | startFunction = section.functionIndex 109 | break 110 | case 'element': 111 | tableInitializers.push(...section.initializers) 112 | break 113 | case 'code': 114 | for (const {locals, instructions} of section.segments) { 115 | functionLocals.push(locals) 116 | functionBodies.push(instructions) 117 | } 118 | break 119 | case 'data': 120 | memoryInitializers.push(...section.initializers) 121 | } 122 | } 123 | if (functionTypes.length !== functionLocals.length) { 124 | throw new Error('Mismatched function counts') 125 | } 126 | functionTypes.forEach((type, i) => 127 | context.setFunctionStats(i, getFunctionStats(type, functionLocals[i])) 128 | ) 129 | return { 130 | context, 131 | moduleName, 132 | tableLengths, 133 | memoryMin, 134 | memoryInitializers, 135 | globalInitializers, 136 | tableInitializers, 137 | startFunction, 138 | functionTypes, 139 | functionBodies 140 | } 141 | } 142 | 143 | function addExportLabel(label: string, output: asm.AssemblyInstruction[], external = true) { 144 | if (external && !isLinux) label = '_' + label 145 | output.push( 146 | new asm.Directive({type: 'globl', args: [label]}), 147 | new asm.Label(label) 148 | ) 149 | } 150 | function addSysvLabel(module: string, label: string, output: asm.AssemblyInstruction[]) { 151 | const sysvLabel = `wasm_${module.replace(INVALID_EXPORT_CHAR, '_')}_${label}` 152 | addExportLabel(sysvLabel, output, true) 153 | return sysvLabel 154 | } 155 | 156 | const TABLE_ITEM = new asm.Directive({type: 'quad', args: [0]}) 157 | function compileGlobals( 158 | {context, moduleName, tableLengths, memoryMin}: ParsedModule, 159 | output: asm.AssemblyInstruction[], 160 | declarations: HeaderDeclaration[] 161 | ) { 162 | tableLengths.forEach((length, i) => { 163 | output.push(new asm.Label(context.tableLabel(i))) 164 | while (length--) output.push(TABLE_ITEM) 165 | }) 166 | 167 | if (memoryMin !== undefined) { 168 | const {exportMemory, memorySizeLabel, memoryStart} = context 169 | if (exportMemory.length) { 170 | for (const label of exportMemory) { 171 | const exportLabel = `wasm_${moduleName}_${label}_memory` 172 | addExportLabel(exportLabel, output) 173 | declarations.push(new GlobalDeclaration('pointer', true, exportLabel)) 174 | } 175 | // 8-byte align not necessary 176 | output.push(new asm.Directive({type: 'quad', args: [memoryStart]})) 177 | } 178 | for (const label of exportMemory) { 179 | const exportLabel = `wasm_${moduleName}_${label}_size` 180 | addExportLabel(exportLabel, output) 181 | declarations.push(new GlobalDeclaration('int', true, exportLabel)) 182 | } 183 | // 8-byte align not necessary 184 | output.push( 185 | new asm.Label(memorySizeLabel), 186 | new asm.Directive({type: 'quad', args: [0]}) 187 | ) 188 | } 189 | 190 | const {globalTypes, exportGlobals} = context 191 | globalTypes.forEach(({type, mutable}, i) => { 192 | const directives = WASM_TYPE_DIRECTIVES.get(type) 193 | if (!directives) throw new Error('Unable to emit global of type ' + type) 194 | output.push(directives.align) 195 | for (const label of exportGlobals.get(i) || []) { 196 | addExportLabel(context.exportLabel('GLOBAL', label), output, false) 197 | const headerLabel = `wasm_${moduleName}_${label}` 198 | addExportLabel(headerLabel, output) 199 | declarations.push(new GlobalDeclaration(getCType(type), !mutable, headerLabel)) 200 | } 201 | output.push( 202 | new asm.Label(context.globalLabel(i)), 203 | directives.data 204 | ) 205 | }) 206 | } 207 | 208 | function compileMemoryInitializer( 209 | {memoryIndex, offset, data}: MemoryInitializer, 210 | context: CompilationContext, 211 | output: asm.AssemblyInstruction[] 212 | ): void { 213 | if (memoryIndex) throw new Error('Invalid memory index') 214 | const {byteLength} = data 215 | const dataView = new DataView(data) 216 | let byte = 0 217 | while (true) { // store 8 bytes at a time 218 | const nextIndex = byte + 8 219 | if (nextIndex > byteLength) break 220 | 221 | compileInstructions(offset, context, output) 222 | compileInstructions([ 223 | {type: 'i64.const', value: dataView.getBigUint64(byte, true)}, 224 | {type: 'i64.store', access: {align: 0, offset: byte}} 225 | ], context, output) 226 | byte = nextIndex 227 | } 228 | while (byte < byteLength) { 229 | compileInstructions(offset, context, output) 230 | compileInstructions([ 231 | {type: 'i32.const', value: dataView.getUint8(byte)}, 232 | {type: 'i32.store8', access: {align: 0, offset: byte}} 233 | ], context, output) 234 | byte++ 235 | } 236 | } 237 | function compileTableInitializer( 238 | {tableIndex, offset, functionIndices}: TableInitializer, 239 | moduleContext: ModuleContext, 240 | context: CompilationContext, 241 | output: asm.AssemblyInstruction[] 242 | ): void { 243 | compileInstructions(offset, context, output) 244 | const tableRegister = INT_INTERMEDIATE_REGISTERS[0] 245 | output.push(new asm.LeaInstruction( 246 | {type: 'label', label: moduleContext.tableLabel(tableIndex)}, 247 | {type: 'register', register: tableRegister} 248 | )) 249 | const addressDatum: asm.Datum = 250 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[1]} 251 | const offsetRegister = context.resolvePop()! 252 | functionIndices.forEach((functionIndex, i) => { 253 | output.push( 254 | new asm.LeaInstruction( 255 | {type: 'label', label: moduleContext.functionLabel(functionIndex)}, 256 | addressDatum 257 | ), 258 | new asm.MoveInstruction(addressDatum, { 259 | type: 'indirect', 260 | register: tableRegister, 261 | immediate: i << 3, 262 | offset: {register: offsetRegister, scale: 8} 263 | }) 264 | ) 265 | }) 266 | } 267 | function compileInitInstructions( 268 | { 269 | context, 270 | moduleName, 271 | memoryMin, 272 | memoryInitializers, 273 | globalInitializers, 274 | tableInitializers, 275 | startFunction 276 | }: ParsedModule, 277 | output: asm.AssemblyInstruction[], 278 | declarations: HeaderDeclaration[] 279 | ) { 280 | if (!( 281 | memoryMin || 282 | globalInitializers.length || 283 | tableInitializers.length || 284 | startFunction !== undefined 285 | )) return 286 | 287 | // TODO: consider a special Instruction for table initialization 288 | // so we can compile the init as a wasm function and not directly into assembly 289 | 290 | declarations.push(new FunctionDeclaration( 291 | 'void', 292 | addSysvLabel(moduleName, 'init_module', output), 293 | ['void'] 294 | )) 295 | 296 | const initOutput: asm.AssemblyInstruction[] = [] 297 | const globalContext = new CompilationContext(context, {params: [], locals: []}) 298 | const compile = (...instructions: Instruction[]) => 299 | compileInstructions(instructions, globalContext, initOutput) 300 | 301 | if (memoryMin) { 302 | compile( 303 | {type: 'i32.const', value: memoryMin}, 304 | {type: 'memory.grow'}, 305 | {type: 'drop'} 306 | ) 307 | for (const initializer of memoryInitializers) { 308 | compileMemoryInitializer(initializer, globalContext, initOutput) 309 | } 310 | } 311 | 312 | globalInitializers.forEach((initializer, global) => { 313 | compile(...initializer) 314 | compile({type: 'set_global', global}) 315 | }) 316 | 317 | for (const initializer of tableInitializers) { 318 | compileTableInitializer(initializer, context, globalContext, initOutput) 319 | } 320 | 321 | if (startFunction !== undefined) compile({type: 'call', func: startFunction}) 322 | 323 | const registersUsed = new Set(globalContext.registersUsed(true)) 324 | const toRestore: asm.Datum[] = [] 325 | for (const register of SYSV_CALLEE_SAVE_REGISTERS) { 326 | if (registersUsed.has(register)) { 327 | const datum: asm.Datum = {type: 'register', register} 328 | output.push(new asm.PushInstruction(datum)) 329 | toRestore.push(datum) 330 | } 331 | } 332 | output.push(...initOutput) 333 | for (const register of reverse(toRestore)) { 334 | output.push(new asm.PopInstruction(register)) 335 | } 336 | output.push(new asm.RetInstruction) 337 | } 338 | 339 | export function compileFunctionBodies( 340 | {context, moduleName, functionTypes, functionBodies}: ParsedModule, 341 | output: asm.AssemblyInstruction[], 342 | declarations: HeaderDeclaration[] 343 | ) { 344 | const {exportFunctions} = context 345 | functionBodies.forEach((instructions, i) => { 346 | const functionIndex = context.getFunctionIndex(i) 347 | const sysvLabels: asm.AssemblyInstruction[] = [], 348 | functionOutput: asm.AssemblyInstruction[] = [] 349 | for (const label of exportFunctions.get(functionIndex) || []) { 350 | addExportLabel(context.exportLabel('FUNC', label), functionOutput, false) 351 | const {params, results} = functionTypes[i] 352 | declarations.push(new FunctionDeclaration( 353 | getCType((results as [ValueType?])[0] || 'empty'), 354 | addSysvLabel(moduleName, label, sysvLabels), 355 | params.length ? params.map(getCType) : ['void'] 356 | )) 357 | } 358 | const functionLabel = context.functionLabel(i) 359 | functionOutput.push(new asm.Label(functionLabel)) 360 | 361 | const functionContext = context.makeFunctionContext(functionIndex) 362 | const bodyOutput: asm.AssemblyInstruction[] = [] 363 | const branches = 364 | compileInstructions(instructions, functionContext, bodyOutput).definitely 365 | if (!branches) popResultAndUnwind(functionContext, bodyOutput) 366 | 367 | const pushed: asm.Datum[] = [] 368 | for (const register of functionContext.registersUsed(true)) { 369 | const datum: asm.Datum = {type: 'register', register} 370 | functionOutput.push(new asm.PushInstruction(datum)) 371 | pushed.push(datum) 372 | } 373 | const {stackParams, stackLocals} = functionContext 374 | const pushParam = new asm.PushInstruction( 375 | // Account for pushed registers and return value 376 | new SPRelative(pushed.length + stackParams).datum 377 | ) 378 | for (let i = 0; i < stackParams; i++) { 379 | // TODO: if the caller knew pushedRegisters, 380 | // they could put the stack params in the right locations 381 | functionOutput.push(pushParam) 382 | } 383 | const reservedLocals = stackLocals - stackParams 384 | if (reservedLocals) functionOutput.push(growStack(reservedLocals)) 385 | functionContext.locals.forEach(({float}, i) => { 386 | const datum = functionContext.resolveLocalDatum(i) 387 | const zeroDatum: asm.Datum = float && datum.type === 'register' 388 | ? {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]} 389 | : datum 390 | functionOutput.push(new asm.MoveInstruction( 391 | {type: 'immediate', value: 0}, zeroDatum, 'q' 392 | )) 393 | if (datum !== zeroDatum) { 394 | functionOutput.push(new asm.MoveInstruction(zeroDatum, datum, 'q')) 395 | } 396 | }) 397 | 398 | functionOutput.push(...bodyOutput) 399 | 400 | functionOutput.push(new asm.Label(context.returnLabel(functionIndex))) 401 | if (stackLocals) functionOutput.push(shrinkStack(stackLocals)) 402 | for (const datum of reverse(pushed)) { 403 | functionOutput.push(new asm.PopInstruction(datum)) 404 | } 405 | functionOutput.push(new asm.RetInstruction) 406 | 407 | if (sysvLabels.length) { 408 | output.push(...sysvLabels) 409 | const {params, stackLocals} = functionContext 410 | const moves = new Map() 411 | let intParam = 0, floatParam = 0 412 | const stackParams: ParamTarget[] = [] 413 | let calleeStackParams = 0 414 | params.forEach(({float}, param) => { 415 | const source = (float 416 | ? SYSV_FLOAT_PARAM_REGISTERS[floatParam++] 417 | : SYSV_INT_PARAM_REGISTERS[intParam++] 418 | ) as asm.Register | undefined 419 | const target = functionContext.resolveParam(param) 420 | if (target instanceof SPRelative) { 421 | target.stackOffset -= stackLocals 422 | calleeStackParams++ 423 | } 424 | if (source) moves.set(source, target) 425 | else stackParams.push(target) 426 | }) 427 | const {toRestore, output: relocateOutput} = 428 | relocateArguments(moves, stackParams, SYSV_CALLEE_SAVE_SET, 1) 429 | const pushed: asm.Datum[] = [] 430 | for (const register of toRestore) { 431 | const datum: asm.Datum = {type: 'register', register} 432 | output.push(new asm.PushInstruction(datum)) 433 | pushed.push(datum) 434 | } 435 | output.push(...relocateOutput) 436 | if (toRestore.length || calleeStackParams) { 437 | if (calleeStackParams) { // point to end of pushed parameters 438 | output.push(growStack(calleeStackParams)) 439 | } 440 | output.push(new asm.CallInstruction(functionLabel)) 441 | if (calleeStackParams) { // pop parameters passed on the stack 442 | output.push(shrinkStack(calleeStackParams)) 443 | } 444 | for (const datum of reverse(pushed)) output.push(new asm.PopInstruction(datum)) 445 | output.push(new asm.RetInstruction) 446 | } 447 | } 448 | output.push(...functionOutput) 449 | }) 450 | } 451 | 452 | export function compileModule(module: Module): CompiledModule { 453 | const parsedModule = parseSections(module) 454 | const output: asm.AssemblyInstruction[] = [ 455 | new asm.Directive({type: 'section', args: ['.data']}), 456 | new asm.Directive({type: 'balign', args: [8]}) 457 | ] 458 | const declarations: HeaderDeclaration[] = [] 459 | const originalOutputLength = output.length 460 | compileGlobals(parsedModule, output, declarations) 461 | if (output.length === originalOutputLength) output.length = 0 462 | output.push(new asm.Directive({type: 'section', args: ['.text']})) 463 | compileInitInstructions(parsedModule, output, declarations) 464 | compileFunctionBodies(parsedModule, output, declarations) 465 | return {instructions: output, declarations} 466 | } -------------------------------------------------------------------------------- /compile/numeric.ts: -------------------------------------------------------------------------------- 1 | import {ConstInstruction} from '../parse/instruction' 2 | import {ValueType} from '../parse/value-type' 3 | import {convertFloatToInt} from '../util' 4 | import {CompilationContext, SPRelative} from './context' 5 | import { 6 | DIV_LOWER_DATUM, 7 | DIV_LOWER_REGISTER, 8 | DIV_UPPER_DATUM, 9 | FLOAT_INTERMEDIATE_REGISTERS, 10 | INT_INTERMEDIATE_REGISTERS, 11 | SHIFT_REGISTER, 12 | SHIFT_REGISTER_BYTE, 13 | SHIFT_REGISTER_DATUM, 14 | STACK_TOP, 15 | isFloat, 16 | typeWidth, 17 | getIntermediateRegisters 18 | } from './conventions' 19 | import {growStack, shrinkStack} from './helpers' 20 | import * as asm from './x86_64-asm' 21 | 22 | const COMPARE_OPERATIONS = new Map([ 23 | ['eqz', 'e'], ['eq', 'e'], 24 | ['ne', 'ne'], 25 | ['lt_s', 'l'], 26 | ['lt_u', 'b'], ['lt', 'b'], 27 | ['le_s', 'le'], 28 | ['le_u', 'be'], ['le', 'be'], 29 | ['gt_s', 'g'], 30 | ['gt_u', 'a'], ['gt', 'a'], 31 | ['ge_s', 'ge'], 32 | ['ge_u', 'ae'], ['ge', 'ae'] 33 | ]) 34 | const INT_ARITHMETIC_OPERATIONS = new Map([ 35 | ['add', asm.AddInstruction], 36 | ['sub', asm.SubInstruction], 37 | ['and', asm.AndInstruction], 38 | ['or', asm.OrInstruction], 39 | ['xor', asm.XorInstruction], 40 | ['shl', asm.ShlInstruction], 41 | ['shr_s', asm.SarInstruction], 42 | ['shr_u', asm.ShrInstruction], 43 | ['rotl', asm.RolInstruction], 44 | ['rotr', asm.RorInstruction] 45 | ]) 46 | const SHIFT_OPERATIONS = new Set(['shl', 'shr_s', 'shr_u', 'rotl', 'rotr']) 47 | const BIT_COUNT_OPERATIONS = new Map([ 48 | ['clz', asm.BsrInstruction], 49 | ['ctz', asm.BsfInstruction], 50 | ['popcnt', asm.PopcntInstruction] 51 | ]) 52 | const FLOAT_BINARY_OPERATIONS = new Map([ 53 | ['add', asm.AddInstruction], 54 | ['sub', asm.SubInstruction], 55 | ['mul', asm.MulInstruction], 56 | ['div', asm.DivBinaryInstruction] 57 | ]) 58 | const MIN_MAX_OPERATIONS = new Map([ 59 | ['min', asm.MinInstruction], 60 | ['max', asm.MaxInstruction] 61 | ]) 62 | const ROUNDING_MODES = new Map([ 63 | ['nearest', 0], 64 | ['floor', 1], 65 | ['ceil', 2], 66 | ['trunc', 3] 67 | ]) 68 | 69 | export function compileConstInstruction( 70 | {type, value}: ConstInstruction, 71 | context: CompilationContext, 72 | output: asm.AssemblyInstruction[] 73 | ): void { 74 | const [constType] = type.split('.') as [ValueType, string] 75 | // Immediates cannot be loaded directly into SIMD registers 76 | const float = isFloat(constType) 77 | const wide = constType.endsWith('64') 78 | if (float) value = convertFloatToInt(value as number, wide) 79 | const immediate: asm.Datum = {type: 'immediate', value} 80 | const width = wide ? 'q' : 'l' 81 | const target = context.resolvePush(float) 82 | if (target) { 83 | const targetDatum: asm.Datum = 84 | {type: 'register', register: float ? INT_INTERMEDIATE_REGISTERS[0] : target} 85 | output.push(new asm.MoveInstruction(immediate, {...targetDatum, width})) 86 | if (float) { 87 | output.push(new asm.MoveInstruction( 88 | targetDatum, {type: 'register', register: target}, 'q' 89 | )) 90 | } 91 | } 92 | else { 93 | const valueNumber = Number(value) 94 | if (valueNumber === (valueNumber | 0) && (wide || valueNumber >= 0)) { 95 | output.push(new asm.PushInstruction(immediate)) 96 | } 97 | else { 98 | const intermediateDatum: asm.Datum = 99 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]} 100 | output.push( 101 | new asm.MoveInstruction(immediate, {...intermediateDatum, width}), 102 | new asm.PushInstruction(intermediateDatum) 103 | ) 104 | } 105 | } 106 | } 107 | export function compileCompareInstruction( 108 | instruction: 109 | 'i32.eqz' | 'i32.eq' | 'i32.ne' | 'i32.lt_s' | 'i32.lt_u' | 110 | 'i32.le_s' | 'i32.le_u' | 'i32.gt_s' | 'i32.gt_u' | 'i32.ge_s' | 'i32.ge_u' | 111 | 'i64.eqz' | 'i64.eq' | 'i64.ne' | 'i64.lt_s' | 'i64.lt_u' | 112 | 'i64.le_s' | 'i64.le_u' | 'i64.gt_s' | 'i64.gt_u' | 'i64.ge_s' | 'i64.ge_u' | 113 | 'f32.eq' | 'f32.ne' | 'f32.lt' | 'f32.gt' | 'f32.le' | 'f32.ge' | 114 | 'f64.eq' | 'f64.ne' | 'f64.lt' | 'f64.gt' | 'f64.le' | 'f64.ge', 115 | context: CompilationContext, 116 | output: asm.AssemblyInstruction[] 117 | ): void { 118 | const [type, operation] = instruction.split('.') as [ValueType, string] 119 | const width = typeWidth(type) 120 | const float = isFloat(type) 121 | let datum2: asm.Datum 122 | if (operation === 'eqz') datum2 = {type: 'immediate', value: 0} 123 | else { 124 | let arg2 = context.resolvePop() 125 | if (!arg2) { 126 | [arg2] = getIntermediateRegisters(float) 127 | output.push(new asm.PopInstruction({type: 'register', register: arg2})) 128 | } 129 | datum2 = {type: 'register', register: arg2, width} 130 | } 131 | const arg1 = context.resolvePop() 132 | const datum1: asm.Datum = arg1 133 | ? {type: 'register', register: arg1, width} 134 | : STACK_TOP 135 | let resultRegister = context.resolvePush(false) 136 | const push = !resultRegister 137 | if (push) resultRegister = INT_INTERMEDIATE_REGISTERS[0] 138 | const cond = COMPARE_OPERATIONS.get(operation) 139 | if (!cond) throw new Error('No comparison value found for ' + instruction) 140 | const result: asm.Datum = {type: 'register', register: resultRegister!} 141 | const result8: asm.Datum = {...result, width: 'b'} 142 | output.push( 143 | new asm.CmpInstruction(datum2, datum1, width), 144 | new asm.SetInstruction(result8, cond) 145 | ) 146 | if (float) { 147 | const parityDatum: asm.Datum = 148 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[1], width: 'b'} 149 | let parityCond: asm.JumpCond, parityInstruction: typeof asm.OrInstruction 150 | if (operation === 'ne') { // negative comparison returns true on nan 151 | parityCond = 'p' 152 | parityInstruction = asm.OrInstruction 153 | } 154 | else { // positive comparison returns false on nan 155 | parityCond = 'np' 156 | parityInstruction = asm.AndInstruction 157 | } 158 | output.push( 159 | new asm.SetInstruction(parityDatum, parityCond), 160 | new parityInstruction(parityDatum, result8) 161 | ) 162 | } 163 | output.push(new asm.MoveExtendInstruction(result8, {...result8, width: 'l'}, false)) 164 | if (push) output.push(new asm.MoveInstruction(result, STACK_TOP)) 165 | } 166 | export function compileBitCountInstruction( 167 | instruction: 168 | 'i32.clz' | 'i32.ctz' | 'i32.popcnt' | 'i64.clz' | 'i64.ctz' | 'i64.popcnt', 169 | context: CompilationContext, 170 | output: asm.AssemblyInstruction[] 171 | ): void { 172 | const [type, operation] = instruction.split('.') as [ValueType, string] 173 | const width = typeWidth(type) 174 | const asmInstruction = BIT_COUNT_OPERATIONS.get(operation) 175 | if (!asmInstruction) throw new Error('No instruction found for ' + instruction) 176 | 177 | const arg = context.resolvePop() 178 | const datum: asm.Datum = arg 179 | ? {type: 'register', register: arg, width} 180 | : STACK_TOP 181 | const result: asm.Datum = arg 182 | ? datum 183 | : {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0], width} 184 | output.push(new asmInstruction(datum, result)) 185 | if (operation !== 'popcnt') { 186 | // bsf and bsr are undefined on inputs that are 0 187 | const widthBits = Number(type.slice(1)) 188 | const clz = operation === 'clz' 189 | const widthDatum: asm.Datum = 190 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[1], width} 191 | output.push( 192 | new asm.MoveInstruction( 193 | {type: 'immediate', value: clz ? (widthBits << 1) - 1 : widthBits}, widthDatum 194 | ), 195 | new asm.CMoveInstruction(widthDatum, result, 'e') 196 | ) 197 | if (clz) { 198 | // bsr output needs to be flipped 199 | output.push( 200 | new asm.XorInstruction({type: 'immediate', value: widthBits - 1}, result) 201 | ) 202 | } 203 | } 204 | if (result !== datum) output.push(new asm.MoveInstruction(result, datum)) 205 | context.push(false) 206 | } 207 | export function compileIntArithmeticInstruction( 208 | instruction: 'i32.add' | 'i32.sub' | 'i32.and' | 'i32.or' | 'i32.xor' | 209 | 'i32.shl' | 'i32.shr_s' | 'i32.shr_u' | 'i32.rotl' | 'i32.rotr' | 210 | 'i64.add' | 'i64.sub' | 'i64.and' | 'i64.or' | 'i64.xor' | 211 | 'i64.shl' | 'i64.shr_s' | 'i64.shr_u' | 'i64.rotl' | 'i64.rotr', 212 | context: CompilationContext, 213 | output: asm.AssemblyInstruction[] 214 | ): void { 215 | let operand2 = context.resolvePop() 216 | if (!operand2) { 217 | // Using intermediate register 1 instead of 0 to avoid extra mov for shifts 218 | operand2 = INT_INTERMEDIATE_REGISTERS[1] 219 | output.push(new asm.PopInstruction({type: 'register', register: operand2})) 220 | } 221 | const operand1 = context.resolvePop() 222 | const [type, operation] = instruction.split('.') as [ValueType, string] 223 | const arithmeticInstruction = INT_ARITHMETIC_OPERATIONS.get(operation) 224 | if (!arithmeticInstruction) { 225 | throw new Error('No arithmetic instruction found for ' + instruction) 226 | } 227 | const width = typeWidth(type) 228 | let datum2: asm.Datum = {type: 'register', register: operand2} 229 | if (SHIFT_OPERATIONS.has(operation)) { 230 | if (operand2 !== SHIFT_REGISTER) { 231 | output.push(new asm.MoveInstruction(datum2, SHIFT_REGISTER_DATUM)) 232 | } 233 | datum2 = SHIFT_REGISTER_BYTE 234 | } 235 | else datum2.width = width 236 | output.push(new arithmeticInstruction( 237 | datum2, 238 | operand1 239 | ? {type: 'register', register: operand1, width} 240 | : STACK_TOP, 241 | width 242 | )) 243 | context.push(false) 244 | } 245 | export function compileIntMulInstruction( 246 | instruction: 'i32.mul' | 'i64.mul', 247 | context: CompilationContext, 248 | output: asm.AssemblyInstruction[] 249 | ): void { 250 | const width = instruction === 'i32.mul' ? 'l' : 'q' 251 | let operand2 = context.resolvePop() 252 | if (!operand2) { 253 | [operand2] = INT_INTERMEDIATE_REGISTERS 254 | output.push(new asm.PopInstruction({type: 'register', register: operand2})) 255 | } 256 | const datum2: asm.Datum = {type: 'register', register: operand2, width} 257 | const operand1 = context.resolvePop() 258 | if (operand1) { 259 | output.push(new asm.ImulInstruction( 260 | datum2, {type: 'register', register: operand1, width} 261 | )) 262 | } 263 | else { 264 | output.push( 265 | new asm.ImulInstruction(STACK_TOP, datum2), 266 | new asm.MoveInstruction(datum2, STACK_TOP) 267 | ) 268 | } 269 | context.push(false) 270 | } 271 | export function compileIntDivInstruction( 272 | instruction: 'i32.div_s' | 'i32.div_u' | 'i32.rem_s' | 'i32.rem_u' | 273 | 'i64.div_s' | 'i64.div_u' | 'i64.rem_s' | 'i64.rem_u', 274 | context: CompilationContext, 275 | output: asm.AssemblyInstruction[] 276 | ): void { 277 | const [type, operation] = instruction.split('.') as [ValueType, string] 278 | const [op, signedness] = operation.split('_') 279 | const width = typeWidth(type) 280 | const wide = width === 'q' 281 | const signed = signedness === 's' 282 | const operand2 = context.resolvePop() 283 | const datum2: asm.Datum = operand2 284 | ? {type: 'register', register: operand2, width} 285 | : STACK_TOP 286 | if (operation === 'rem_s') { 287 | // Special case for INT_MIN % -1, since the remainder 288 | // is 0 but the quotient (INT_MAX + 1) won't fit. 289 | // So, if the divisor is -1, set it to 1. 290 | const oneRegister: asm.Datum = 291 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[1], width} 292 | output.push( 293 | new asm.MoveInstruction({type: 'immediate', value: 1}, oneRegister), 294 | new asm.CmpInstruction({type: 'immediate', value: -1}, datum2, width), 295 | new asm.CMoveInstruction(oneRegister, datum2, 'e') 296 | ) 297 | } 298 | const operand1 = context.resolvePop() 299 | const datum1: asm.Datum = operand1 300 | ? {type: 'register', register: operand1} 301 | : new SPRelative(1).datum 302 | const result = op === 'div' ? DIV_LOWER_DATUM : DIV_UPPER_DATUM 303 | if (operand1 !== DIV_LOWER_REGISTER) { 304 | output.push(new asm.MoveInstruction(datum1, DIV_LOWER_DATUM)) 305 | } 306 | output.push( 307 | signed 308 | ? wide ? new asm.CqtoInstruction : new asm.CdqoInstruction 309 | : new asm.XorInstruction(DIV_UPPER_DATUM, DIV_UPPER_DATUM), 310 | new asm.DivInstruction(datum2, signed, width) 311 | ) 312 | if (!operand2) output.push(shrinkStack(1)) 313 | if (result.register !== operand1) { 314 | output.push(new asm.MoveInstruction(result, operand1 ? datum1 : STACK_TOP)) 315 | } 316 | context.push(false) 317 | } 318 | export function compileSignInstruction( 319 | instruction: 'f32.abs' | 'f32.neg' | 'f32.copysign' | 320 | 'f64.abs' | 'f64.neg' | 'f64.copysign', 321 | context: CompilationContext, 322 | output: asm.AssemblyInstruction[] 323 | ): void { 324 | const [type, operation] = instruction.split('.') as [ValueType, string] 325 | const width = typeWidth(type) 326 | const wide = width === 'd' 327 | const negZeroLoadDatum: asm.Datum = 328 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]}, 329 | negZeroDatum: asm.Datum = 330 | {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[0]} 331 | let negZero = 1n << (wide ? 63n : 31n) 332 | const setSignBit = operation === 'neg' 333 | if (!setSignBit) negZero ^= -1n // a bitmask to exclude the sign bit 334 | output.push( 335 | new asm.MoveInstruction( 336 | {type: 'immediate', value: negZero}, 337 | {...negZeroLoadDatum, width: wide ? 'q' : 'l'} 338 | ), 339 | new asm.MoveInstruction(negZeroLoadDatum, negZeroDatum, 'q') 340 | ) 341 | let signDatum: asm.Datum | undefined 342 | if (operation === 'copysign') { 343 | let signOperand = context.resolvePop() 344 | if (!signOperand) { 345 | signOperand = FLOAT_INTERMEDIATE_REGISTERS[1] 346 | output.push(new asm.PopInstruction({type: 'register', register: signOperand})) 347 | } 348 | signDatum = {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[2]} 349 | output.push( 350 | new asm.MoveInstruction(negZeroDatum, signDatum, width), 351 | new asm.AndNotPackedInstruction( 352 | {type: 'register', register: signOperand}, signDatum, width 353 | ) 354 | ) 355 | } 356 | const operand = context.resolvePop() 357 | let datum: asm.Datum 358 | if (operand) datum = {type: 'register', register: operand} 359 | else { 360 | datum = {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[1]} 361 | output.push(new asm.MoveInstruction(STACK_TOP, datum, width)) 362 | } 363 | const maskInstruction = 364 | setSignBit ? asm.XorPackedInstruction : asm.AndPackedInstruction 365 | output.push(new maskInstruction(negZeroDatum, datum, width)) 366 | if (signDatum) { 367 | // Doesn't matter whether this is OR or XOR, since bits are distinct 368 | output.push(new asm.XorPackedInstruction(signDatum, datum, width)) 369 | } 370 | if (!operand) output.push(new asm.MoveInstruction(datum, STACK_TOP, width)) 371 | context.push(true) 372 | } 373 | export function compileFloatUnaryInstruction( 374 | instruction: 375 | 'f32.ceil' | 'f32.floor' | 'f32.trunc' | 'f32.nearest' | 'f32.sqrt' | 376 | 'f64.ceil' | 'f64.floor' | 'f64.trunc' | 'f64.nearest' | 'f64.sqrt', 377 | context: CompilationContext, 378 | output: asm.AssemblyInstruction[] 379 | ): void { 380 | const [type, operation] = instruction.split('.') as [ValueType, string] 381 | const width = typeWidth(type) 382 | const operand = context.resolvePop() 383 | let datum: asm.Datum, result: asm.Datum 384 | if (operand) datum = result = {type: 'register', register: operand} 385 | else { 386 | datum = STACK_TOP 387 | result = {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[0]} 388 | } 389 | if (operation === 'sqrt') { 390 | output.push(new asm.SqrtInstruction(datum, result, width)) 391 | } 392 | else { 393 | const mode = ROUNDING_MODES.get(operation) 394 | if (mode === undefined) throw new Error('Unknown round type: ' + operation) 395 | output.push(new asm.RoundInstruction(mode, datum, result, width)) 396 | } 397 | if (!operand) output.push(new asm.MoveInstruction(result, datum, 'q')) 398 | context.push(true) 399 | } 400 | export function compileFloatBinaryInstruction( 401 | instruction: 402 | 'f32.add' | 'f32.sub' | 'f32.mul' | 'f32.div' | 403 | 'f64.add' | 'f64.sub' | 'f64.mul' | 'f64.div', 404 | context: CompilationContext, 405 | output: asm.AssemblyInstruction[] 406 | ): void { 407 | const [type, operation] = instruction.split('.') as [ValueType, string] 408 | const arithmeticInstruction = FLOAT_BINARY_OPERATIONS.get(operation) 409 | if (!arithmeticInstruction) { 410 | throw new Error('No arithmetic instruction found for ' + instruction) 411 | } 412 | const width = typeWidth(type) 413 | const operand2 = context.resolvePop() 414 | const datum2: asm.Datum = operand2 415 | ? {type: 'register', register: operand2} 416 | : STACK_TOP 417 | let operand1 = context.resolvePop() 418 | const onStack = !operand1 419 | if (onStack) { 420 | [operand1] = FLOAT_INTERMEDIATE_REGISTERS 421 | output.push(new asm.MoveInstruction( 422 | new SPRelative(1).datum, {type: 'register', register: operand1}, 'q' 423 | )) 424 | } 425 | const datum1: asm.Datum = {type: 'register', register: operand1!} 426 | output.push(new arithmeticInstruction(datum2, datum1, width)) 427 | if (!operand2) output.push(shrinkStack(1)) 428 | if (onStack) output.push(new asm.MoveInstruction(datum1, STACK_TOP, 'q')) 429 | context.push(true) 430 | } 431 | export function compileMinMaxInstruction( 432 | instruction: 'f32.min' | 'f32.max' | 'f64.min' | 'f64.max', 433 | context: CompilationContext, 434 | output: asm.AssemblyInstruction[] 435 | ) { 436 | const [type, operation] = instruction.split('.') as [ValueType, string] 437 | const minMaxInstruction = MIN_MAX_OPERATIONS.get(operation) 438 | if (!minMaxInstruction) { 439 | throw new Error('No arithmetic instruction found for ' + instruction) 440 | } 441 | const width = typeWidth(type) 442 | const operand2 = context.resolvePop() 443 | const datum2: asm.Datum = operand2 444 | ? {type: 'register', register: operand2} 445 | : STACK_TOP 446 | let operand1 = context.resolvePop() 447 | const onStack = !operand1 448 | if (onStack) { 449 | [operand1] = FLOAT_INTERMEDIATE_REGISTERS 450 | output.push(new asm.MoveInstruction( 451 | new SPRelative(1).datum, {type: 'register', register: operand1}, 'q' 452 | )) 453 | } 454 | const datum1: asm.Datum = {type: 'register', register: operand1!} 455 | const datum1Orig: asm.Datum = 456 | {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[1]} 457 | const nanMaskDatum: asm.Datum = 458 | {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[2]} 459 | // x86_64 min/max ignore NaN in the destination, so we need to check for it 460 | output.push( 461 | new asm.MoveInstruction(datum1, datum1Orig, width), 462 | new asm.MoveInstruction(datum1, nanMaskDatum, width), 463 | new asm.CmpOrderedInstruction(datum1, nanMaskDatum, width), 464 | new minMaxInstruction(datum2, datum1, width), 465 | // Store min(datum1, datum2) in datum1 iff datum1 is not NaN 466 | new asm.AndPackedInstruction(nanMaskDatum, datum1, width), 467 | // Store datum1 in nanMaskDatum iff datum1 is NaN 468 | new asm.AndNotPackedInstruction(datum1Orig, nanMaskDatum, width), 469 | new asm.OrPackedInstruction(nanMaskDatum, datum1, width) 470 | ) 471 | if (onStack) output.push(new asm.MoveInstruction(datum1, STACK_TOP, 'q')) 472 | context.push(true) 473 | } 474 | export function compileWrapInstruction( 475 | context: CompilationContext, 476 | output: asm.AssemblyInstruction[] 477 | ): void { 478 | let value = context.resolvePop() 479 | const onStack = !value 480 | if (onStack) [value] = INT_INTERMEDIATE_REGISTERS 481 | const datum: asm.Datum = {type: 'register', register: value!, width: 'l'} 482 | output.push(new asm.MoveInstruction(onStack ? STACK_TOP : datum, datum)) 483 | if (onStack) output.push(new asm.MoveInstruction(datum, STACK_TOP)) 484 | context.push(false) 485 | } 486 | export function compileTruncateInstruction( 487 | instruction: 488 | 'i32.trunc_s/f32' | 'i32.trunc_u/f32' | 'i32.trunc_s/f64' | 'i32.trunc_u/f64' | 489 | 'i64.trunc_s/f32' | 'i64.trunc_u/f32' | 'i64.trunc_s/f64' | 'i64.trunc_u/f64', 490 | context: CompilationContext, 491 | output: asm.AssemblyInstruction[] 492 | ): void { 493 | let [resultType, fullOperation] = instruction.split('.') as [ValueType, string] 494 | const [operation, sourceType] = fullOperation.split('/') as [string, ValueType] 495 | const sourceWidth = typeWidth(sourceType) 496 | const signed = operation.endsWith('s') 497 | let operand = context.resolvePop() 498 | const pop = !operand 499 | let datum: asm.Datum = 500 | operand ? {type: 'register', register: operand} : STACK_TOP 501 | let wrapTo32 = false 502 | let highBitDatum: asm.Datum | undefined 503 | if (!signed) { 504 | if (resultType === 'i32') { 505 | resultType = 'i64' 506 | wrapTo32 = true 507 | } 508 | else { // i64 509 | if (pop) { 510 | operand = FLOAT_INTERMEDIATE_REGISTERS[0] 511 | datum = {type: 'register', register: operand} 512 | output.push(new asm.MoveInstruction(STACK_TOP, datum, 'q')) 513 | } 514 | const highBitThreshold = 2 ** 63 515 | const wideSource = sourceWidth === 'd' 516 | const highBitInt = convertFloatToInt(highBitThreshold, wideSource) 517 | const thresholdIntDatum: asm.Datum = 518 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]} 519 | const thresholdDatum: asm.Datum = 520 | {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[1]} 521 | const label = context.makeLabel('CONVERT_U64_END') 522 | highBitDatum = {type: 'register', register: INT_INTERMEDIATE_REGISTERS[1]} 523 | output.push( 524 | new asm.XorInstruction(highBitDatum, highBitDatum), 525 | new asm.MoveInstruction( 526 | {type: 'immediate', value: highBitInt}, 527 | {...thresholdIntDatum, width: wideSource ? 'q' : 'l'} 528 | ), 529 | new asm.MoveInstruction(thresholdIntDatum, thresholdDatum, 'q'), 530 | new asm.CmpInstruction(thresholdDatum, datum, sourceWidth), 531 | new asm.JumpInstruction(label, 'b'), 532 | new asm.MoveInstruction( 533 | {type: 'immediate', value: BigInt(highBitThreshold)}, highBitDatum 534 | ), 535 | new asm.SubInstruction(thresholdDatum, datum, sourceWidth), 536 | new asm.Label(label) 537 | ) 538 | } 539 | } 540 | let resultRegister = context.resolvePush(false) 541 | const push = !resultRegister 542 | if (push) [resultRegister] = INT_INTERMEDIATE_REGISTERS 543 | const resultDatum: asm.Datum = 544 | {type: 'register', register: resultRegister!, width: typeWidth(resultType)} 545 | output.push(new asm.CvtToIntInstruction(datum, resultDatum, sourceWidth)) 546 | if (wrapTo32) { 547 | // i32 needs to be stored with upper 32 bits zeroed 548 | const lowDatum: asm.Datum = {...resultDatum, width: 'l'} 549 | output.push(new asm.MoveInstruction(lowDatum, lowDatum)) 550 | } 551 | if (highBitDatum) { 552 | output.push(new asm.XorInstruction(highBitDatum, resultDatum)) 553 | } 554 | const stackHeightChange = Number(push) - Number(pop) 555 | if (stackHeightChange) output.push(growStack(stackHeightChange)) 556 | if (push) output.push(new asm.MoveInstruction(resultDatum, STACK_TOP)) 557 | } 558 | export function compileExtendInstruction( 559 | context: CompilationContext, 560 | output: asm.AssemblyInstruction[] 561 | ): void { 562 | const value = context.resolvePop() 563 | let datum: asm.Datum, result: asm.Datum 564 | if (value) { 565 | result = {type: 'register', register: value} 566 | datum = {...result, width: 'l'} 567 | } 568 | else { 569 | datum = STACK_TOP 570 | result = {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]} 571 | } 572 | output.push(new asm.MoveExtendInstruction( 573 | datum, result, true, {src: 'l', dest: 'q'} 574 | )) 575 | if (!value) output.push(new asm.MoveInstruction(result, datum)) 576 | context.push(false) 577 | } 578 | export function compileConvertInstruction( 579 | instruction: 580 | 'f32.convert_s/i32' | 'f32.convert_u/i32' | 'f32.convert_s/i64' | 'f32.convert_u/i64' | 581 | 'f64.convert_s/i32' | 'f64.convert_u/i32' | 'f64.convert_s/i64' | 'f64.convert_u/i64', 582 | context: CompilationContext, 583 | output: asm.AssemblyInstruction[] 584 | ): void { 585 | const [resultType, fullOperation] = instruction.split('.') as [ValueType, string] 586 | let [operation, sourceType] = fullOperation.split('/') as [string, ValueType] 587 | const signed = operation.endsWith('_s') 588 | let operand = context.resolvePop() 589 | const pop = !operand 590 | let datum: asm.Datum = 591 | operand ? {type: 'register', register: operand} : STACK_TOP 592 | let doubleNeededDatum: asm.Datum | undefined 593 | if (!signed) { 594 | if (sourceType === 'i32') sourceType = 'i64' 595 | else { // i64 596 | if (pop) { 597 | operand = INT_INTERMEDIATE_REGISTERS[0] 598 | datum = {type: 'register', register: operand} 599 | output.push(new asm.MoveInstruction(STACK_TOP, datum)) 600 | } 601 | const lowBitDatum: asm.Datum = 602 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[1]} 603 | doubleNeededDatum = 604 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[2]} 605 | const label = context.makeLabel('CONVERT_U64_END') 606 | output.push( 607 | // Modified from https://stackoverflow.com/a/11725575 608 | new asm.XorInstruction(doubleNeededDatum, doubleNeededDatum), 609 | new asm.TestInstruction(datum, datum), 610 | new asm.JumpInstruction(label, 'ns'), 611 | new asm.NotInstruction(doubleNeededDatum), 612 | new asm.MoveInstruction(datum, lowBitDatum), 613 | new asm.AndInstruction({type: 'immediate', value: 1}, lowBitDatum), 614 | new asm.ShrInstruction({type: 'immediate', value: 1}, datum), 615 | new asm.OrInstruction(lowBitDatum, datum), 616 | new asm.Label(label) 617 | ) 618 | } 619 | } 620 | if (datum.type === 'register') datum.width = typeWidth(sourceType) 621 | const resultWidth = typeWidth(resultType) 622 | let resultRegister = context.resolvePush(true) 623 | const push = !resultRegister 624 | if (push) resultRegister = FLOAT_INTERMEDIATE_REGISTERS[0] 625 | const resultDatum: asm.Datum = {type: 'register', register: resultRegister!} 626 | output.push(new asm.CvtToFloatInstruction(datum, resultDatum, resultWidth)) 627 | if (doubleNeededDatum) { 628 | const addDatum: asm.Datum = 629 | {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[1]} 630 | const maskDatum: asm.Datum = 631 | {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[2]} 632 | output.push( 633 | new asm.MoveInstruction(resultDatum, addDatum, resultWidth), 634 | new asm.MoveInstruction(doubleNeededDatum, maskDatum, 'q'), 635 | new asm.AndPackedInstruction(maskDatum, addDatum, resultWidth), 636 | new asm.AddInstruction(addDatum, resultDatum, resultWidth) 637 | ) 638 | } 639 | const stackHeightChange = Number(push) - Number(pop) 640 | if (stackHeightChange) output.push(shrinkStack(stackHeightChange)) 641 | if (push) output.push(new asm.MoveInstruction(resultDatum, STACK_TOP)) 642 | } 643 | export function compileFConvertInstruction( 644 | instruction: 'f32.demote' | 'f64.promote', 645 | context: CompilationContext, 646 | output: asm.AssemblyInstruction[] 647 | ): void { 648 | const operand = context.resolvePop() 649 | let source: asm.Datum, target: asm.Datum 650 | if (operand) source = target = {type: 'register', register: operand} 651 | else { 652 | source = STACK_TOP 653 | target = {type: 'register', register: FLOAT_INTERMEDIATE_REGISTERS[0]} 654 | } 655 | const [type] = instruction.split('.') as [ValueType, string] 656 | const targetWidth = typeWidth(type) 657 | output.push(new asm.CvtFloatInstruction( 658 | source, target, targetWidth === 's' ? 'd' : 's', targetWidth 659 | )) 660 | if (!operand) output.push(new asm.MoveInstruction(target, source, targetWidth)) 661 | context.push(true) 662 | } 663 | export function compileReinterpretInstruction( 664 | instruction: 665 | 'i32.reinterpret' | 'i64.reinterpret' | 'f32.reinterpret' | 'f64.reinterpret', 666 | context: CompilationContext, 667 | output: asm.AssemblyInstruction[] 668 | ): void { 669 | const operand = context.resolvePop() 670 | const [type] = instruction.split('.') as [ValueType, string] 671 | const float = isFloat(type) 672 | const result = context.resolvePush(float) 673 | if (operand) { 674 | const datum: asm.Datum = {type: 'register', register: operand} 675 | output.push(result 676 | ? new asm.MoveInstruction( 677 | datum, {type: 'register', register: result}, 'q' 678 | ) 679 | : new asm.PushInstruction(datum) 680 | ) 681 | } 682 | else if (result) { 683 | output.push(new asm.PopInstruction({type: 'register', register: result})) 684 | } 685 | } -------------------------------------------------------------------------------- /compile/variable.ts: -------------------------------------------------------------------------------- 1 | import {LocalInstruction} from '../parse/instruction' 2 | import {CompilationContext} from './context' 3 | import {INT_INTERMEDIATE_REGISTERS, STACK_TOP, isFloat} from './conventions' 4 | import {memoryMoveWidth} from './helpers' 5 | import * as asm from './x86_64-asm' 6 | 7 | export function compileGetLocalInstruction( 8 | local: number, 9 | context: CompilationContext, 10 | output: asm.AssemblyInstruction[] 11 | ): void { 12 | const {float} = context.getParam(local) 13 | const localDatum = context.resolveParamDatum(local) 14 | const target = context.resolvePush(float) 15 | if (target) { 16 | output.push(new asm.MoveInstruction( 17 | localDatum, {type: 'register', register: target}, 'q' 18 | )) 19 | } 20 | else { 21 | let sourceDatum: asm.Datum 22 | if (float && localDatum.type === 'register') { 23 | // push requires an int register 24 | sourceDatum = {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]} 25 | output.push(new asm.MoveInstruction(localDatum, sourceDatum, 'q')) 26 | } 27 | else sourceDatum = localDatum 28 | output.push(new asm.PushInstruction(sourceDatum)) 29 | } 30 | } 31 | export function compileStoreLocalInstruction( 32 | {type, local}: LocalInstruction, 33 | context: CompilationContext, 34 | output: asm.AssemblyInstruction[] 35 | ) { 36 | const tee = type === 'tee_local' 37 | // We need to pop before resolving the local since pop references %rsp after increment 38 | const value = context.resolvePop() 39 | const {float} = context.getParam(local) 40 | if (tee) context.push(float) 41 | const localDatum = context.resolveParamDatum(local) 42 | if (value) { 43 | output.push(new asm.MoveInstruction( 44 | {type: 'register', register: value}, localDatum, 'q' 45 | )) 46 | } 47 | else if (tee) { 48 | if (localDatum.type === 'register') { 49 | output.push(new asm.MoveInstruction(STACK_TOP, localDatum, 'q')) 50 | } 51 | else { 52 | const intermediate: asm.Datum = 53 | {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]} 54 | output.push( 55 | new asm.MoveInstruction(STACK_TOP, intermediate), 56 | new asm.MoveInstruction(intermediate, localDatum) 57 | ) 58 | } 59 | } 60 | else { 61 | // pop requires an int register 62 | const target: asm.Datum = float && localDatum.type === 'register' 63 | ? {type: 'register', register: INT_INTERMEDIATE_REGISTERS[0]} 64 | : localDatum 65 | output.push(new asm.PopInstruction(target)) 66 | if (target !== localDatum) { 67 | output.push(new asm.MoveInstruction(target, localDatum, 'q')) 68 | } 69 | } 70 | } 71 | export function compileGetGlobalInstruction( 72 | global: number, 73 | context: CompilationContext, 74 | output: asm.AssemblyInstruction[] 75 | ): void { 76 | const {moduleContext} = context 77 | const type = moduleContext.resolveGlobalType(global) 78 | const sourceDatum: asm.Datum = 79 | {type: 'label', label: moduleContext.resolveGlobalLabel(global)} 80 | let target = context.resolvePush(isFloat(type)) 81 | const push = !target 82 | if (push && type.endsWith('64')) output.push(new asm.PushInstruction(sourceDatum)) 83 | else { 84 | if (push) [target] = INT_INTERMEDIATE_REGISTERS // push requires an int register 85 | const targetDatum: asm.Datum = {type: 'register', register: target!} 86 | const width = memoryMoveWidth(type, push) 87 | output.push(new asm.MoveInstruction(sourceDatum, {...targetDatum, width}, width)) 88 | if (push) output.push(new asm.PushInstruction(targetDatum)) 89 | } 90 | } 91 | export function compileSetGlobalInstruction( 92 | global: number, 93 | context: CompilationContext, 94 | output: asm.AssemblyInstruction[] 95 | ): void { 96 | const {moduleContext} = context 97 | const type = moduleContext.resolveGlobalType(global) 98 | const targetDatum: asm.Datum = 99 | {type: 'label', label: moduleContext.resolveGlobalLabel(global)} 100 | let value = context.resolvePop() 101 | const pop = !value 102 | if (pop && type.endsWith('64')) output.push(new asm.PopInstruction(targetDatum)) 103 | else { 104 | if (pop) { 105 | [value] = INT_INTERMEDIATE_REGISTERS // pop requires an int register 106 | output.push(new asm.PopInstruction({type: 'register', register: value})) 107 | } 108 | const width = memoryMoveWidth(type, pop) 109 | output.push(new asm.MoveInstruction( 110 | {type: 'register', register: value!, width}, targetDatum, width 111 | )) 112 | } 113 | } -------------------------------------------------------------------------------- /compile/x86_64-asm.ts: -------------------------------------------------------------------------------- 1 | import {STACK_TOP, isLinux} from './conventions' 2 | import {growStack, shrinkStack} from './helpers' 3 | 4 | export type Register 5 | = 'rax' | 'rbx' | 'rcx' | 'rdx' 6 | | 'rsp' | 'rbp' 7 | | 'rdi' | 'rsi' 8 | | 'r8' | 'r9' | 'r10' | 'r11' | 'r12' | 'r13' | 'r14' | 'r15' 9 | | 'xmm0' | 'xmm1' | 'xmm2' | 'xmm3' | 'xmm4' | 'xmm5' | 'xmm6' | 'xmm7' 10 | | 'xmm8' | 'xmm9' | 'xmm10' | 'xmm11' | 'xmm12' | 'xmm13' | 'xmm14' | 'xmm15' 11 | const LETTER_REGISTERS = new Set(['rax', 'rbx', 'rcx', 'rdx']) 12 | const X64_REGISTERS = new Set(['r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', 'r15']) 13 | export type Width = 'b' | 'w' | 'l' | 'q' | 's' | 'd' 14 | export interface Offset { 15 | register: Register 16 | scale?: number 17 | } 18 | export type Datum 19 | = {type: 'register', register: Register, width?: Width} 20 | | {type: 'indirect', register: Register, immediate?: number | bigint, offset?: Offset} 21 | | {type: 'label', label: string} 22 | | {type: 'immediate', value: number | bigint} 23 | const FLOAT_WIDTHS = new Set(['s', 'd']) 24 | 25 | export type JumpCond 26 | = 'e' | 'ne' 27 | | 'l' | 'g' 28 | | 'le' | 'ge' 29 | | 'a' | 'b' 30 | | 'ae' | 'be' 31 | | 'p' | 'np' 32 | | 's' | 'ns' 33 | 34 | export type GASDirective 35 | = {type: 'globl', args: [string]} 36 | | {type: 'long' | 'quad', args: [number | string]} 37 | | {type: 'balign', args: [number]} 38 | | {type: 'section', args: ['.data' | '.rodata' | '.text']} 39 | 40 | const isSIMDRegister = (register: Register) => register.startsWith('xmm') 41 | 42 | function registerToString(register: Register, width: Width = 'q') { 43 | switch (width) { 44 | case 'b': 45 | return X64_REGISTERS.has(register) 46 | ? register + 'b' 47 | : (LETTER_REGISTERS.has(register) ? register[1] : register.slice(1)) + 'l' 48 | case 'w': 49 | return X64_REGISTERS.has(register) 50 | ? register + 'w' 51 | : register.slice(1) 52 | case 'l': 53 | return X64_REGISTERS.has(register) 54 | ? register + 'd' 55 | : 'e' + register.slice(1) 56 | case 'q': 57 | case 's': 58 | case 'd': 59 | return register 60 | } 61 | } 62 | 63 | function datumToString(datum: Datum): string { 64 | switch (datum.type) { 65 | case 'register': { 66 | const {register, width} = datum 67 | return '%' + registerToString(register, width) 68 | } 69 | case 'indirect': { 70 | const {register, immediate, offset} = datum 71 | let result = `${immediate || ''}` 72 | result += '(%' + register 73 | if (offset) { 74 | const {register, scale} = offset 75 | result += ', %' + register 76 | if (scale) result += `, ${scale}` 77 | } 78 | return result + ')' 79 | } 80 | case 'label': return datum.label + '(%rip)' 81 | case 'immediate': return `$${datum.value}` 82 | } 83 | } 84 | 85 | export interface AssemblyInstruction { 86 | readonly str: string 87 | } 88 | 89 | abstract class UnaryInstruction implements AssemblyInstruction { 90 | constructor(readonly datum: Datum) {} 91 | abstract readonly op: string 92 | get str() { 93 | return `${this.op} ${datumToString(this.datum)}` 94 | } 95 | } 96 | abstract class SrcDestInstruction implements AssemblyInstruction { 97 | constructor( 98 | readonly src: Datum, 99 | readonly dest: Datum, 100 | readonly width?: Width 101 | ) {} 102 | abstract readonly op: string 103 | get packed() { return false } 104 | get str() { 105 | const {src, dest, width, op} = this 106 | const simdExtension = FLOAT_WIDTHS.has(width) 107 | ? this.packed ? 'p' : 's' 108 | : '' 109 | return `${op}${simdExtension}${width || ''} ${ 110 | datumToString(src) 111 | }, ${datumToString(dest)}` 112 | } 113 | } 114 | abstract class JumpLikeInstruction implements AssemblyInstruction { 115 | constructor(readonly target: string | Datum) {} 116 | abstract readonly op: string 117 | get str() { 118 | const {target} = this 119 | const targetString = typeof target === 'string' 120 | ? target 121 | : '*' + datumToString(target) 122 | return this.op + ' ' + targetString 123 | } 124 | } 125 | export class Directive implements AssemblyInstruction { 126 | constructor(readonly directive: GASDirective) {} 127 | get str() { 128 | let args: (string | number)[] 129 | if (!isLinux && this.directive.type === 'section') { 130 | args = this.directive.args.map(section => { 131 | const lowerCase = section.replace('.', '__') 132 | return lowerCase.toUpperCase() + ',' + lowerCase 133 | }) 134 | } 135 | else ({args} = this.directive) 136 | return `.${this.directive.type}${args ? ' ' + args.join(' ') : ''}` 137 | } 138 | } 139 | export class Label implements AssemblyInstruction { 140 | constructor(readonly label: string) {} 141 | get str() { return this.label + ':' } 142 | } 143 | export class Comment implements AssemblyInstruction { 144 | constructor(readonly comment: string) {} 145 | get str() { return '# ' + this.comment } 146 | } 147 | export class AddInstruction extends SrcDestInstruction { 148 | get op() { return 'add' } 149 | } 150 | export class AndInstruction extends SrcDestInstruction { 151 | get op() { return 'and' } 152 | } 153 | export class AndPackedInstruction extends AndInstruction { 154 | get packed() { return true } 155 | } 156 | export class AndNotPackedInstruction extends SrcDestInstruction { 157 | get op() { return 'andn' } 158 | get packed() { return true } 159 | } 160 | export class BsfInstruction extends SrcDestInstruction { 161 | get op() { return 'bsf' } 162 | } 163 | export class BsrInstruction extends SrcDestInstruction { 164 | get op() { return 'bsr' } 165 | } 166 | export class CallInstruction extends JumpLikeInstruction { 167 | get op() { return 'call' } 168 | } 169 | export class CdqoInstruction implements AssemblyInstruction { 170 | get str() { return 'cdq' } 171 | } 172 | export class CqtoInstruction implements AssemblyInstruction { 173 | get str() { return 'cqto' } 174 | } 175 | export class CMoveInstruction extends SrcDestInstruction { 176 | constructor(src: Datum, dest: Datum, readonly cond: JumpCond) { 177 | super(src, dest) 178 | } 179 | get op() { return 'cmov' + this.cond } 180 | } 181 | export class CmpInstruction extends SrcDestInstruction { 182 | get op() { 183 | return FLOAT_WIDTHS.has(this.width) ? 'ucomi' : 'cmp' 184 | } 185 | } 186 | export class CmpOrderedInstruction extends SrcDestInstruction { 187 | get op() { return 'cmpord' } 188 | } 189 | export class CvtFloatInstruction extends SrcDestInstruction { 190 | constructor( 191 | src: Datum, 192 | dest: Datum, 193 | readonly srcWidth: Width, 194 | destWidth: Width 195 | ) { super(src, dest, destWidth) } 196 | get op() { return `cvts${this.srcWidth}2` } 197 | } 198 | export class CvtToFloatInstruction extends SrcDestInstruction { 199 | get op() { return 'cvtsi2' } 200 | } 201 | export class CvtToIntInstruction extends SrcDestInstruction { 202 | constructor(src: Datum, dest: Datum, readonly srcWidth: Width) { 203 | super(src, dest) 204 | } 205 | get op() { return `cvtts${this.srcWidth}2si` } 206 | } 207 | export class DivInstruction extends UnaryInstruction { 208 | constructor(src: Datum, readonly signed: boolean, readonly width: Width) { 209 | super(src) 210 | } 211 | get op() { return `${this.signed ? 'i' : ''}div${this.width}` } 212 | } 213 | export class DivBinaryInstruction extends SrcDestInstruction { 214 | get op() { return 'div' } 215 | } 216 | export class ImulInstruction extends SrcDestInstruction { 217 | get op() { return 'imul' } 218 | } 219 | export class JumpInstruction extends JumpLikeInstruction { 220 | constructor(target: string | Datum, readonly cond?: JumpCond) { 221 | super(target) 222 | } 223 | get op() { return `j${this.cond || 'mp'}` } 224 | } 225 | export class LeaInstruction extends SrcDestInstruction { 226 | get op() { return 'lea' } 227 | } 228 | export class LzcntInstruction extends SrcDestInstruction { 229 | get op() { return 'lzcnt' } 230 | } 231 | export class MaxInstruction extends SrcDestInstruction { 232 | get op() { return 'max' } 233 | } 234 | export class MinInstruction extends SrcDestInstruction { 235 | get op() { return 'min' } 236 | } 237 | export class MoveInstruction extends SrcDestInstruction { 238 | get op() { return 'mov' } 239 | } 240 | export class MoveExtendInstruction extends SrcDestInstruction { 241 | constructor( 242 | src: Datum, 243 | dest: Datum, 244 | readonly signed: boolean, 245 | readonly widths?: {src: Width, dest: Width} 246 | ) { super(src, dest) } 247 | get op() { 248 | const movPrefix = `mov${this.signed ? 's' : 'z'}` 249 | if (this.widths) { 250 | const {src, dest} = this.widths 251 | return movPrefix + src + dest 252 | } 253 | return movPrefix + 'x' 254 | } 255 | } 256 | export class MulInstruction extends SrcDestInstruction { 257 | get op() { return 'mul' } 258 | } 259 | export class NotInstruction extends UnaryInstruction { 260 | get op() { return 'not' } 261 | } 262 | export class OrInstruction extends SrcDestInstruction { 263 | get op() { return 'or' } 264 | } 265 | export class OrPackedInstruction extends OrInstruction { 266 | get packed() { return true } 267 | } 268 | export class PopInstruction extends UnaryInstruction { 269 | get op() { return 'pop' } 270 | get str() { 271 | if (this.datum.type === 'register' && isSIMDRegister(this.datum.register)) { 272 | return new MoveInstruction(STACK_TOP, this.datum, 'q').str + '\n' + 273 | shrinkStack(1).str 274 | } 275 | return super.str 276 | } 277 | } 278 | export class PopcntInstruction extends SrcDestInstruction { 279 | get op() { return 'popcnt' } 280 | } 281 | export class PushInstruction extends UnaryInstruction { 282 | get op() { return 'push' } 283 | get str() { 284 | if (this.datum.type === 'register' && isSIMDRegister(this.datum.register)) { 285 | return growStack(1).str + '\n' + 286 | new MoveInstruction(this.datum, STACK_TOP, 'q').str 287 | } 288 | return super.str 289 | } 290 | } 291 | export class RetInstruction { 292 | get str() { return 'ret' } 293 | } 294 | export class RolInstruction extends SrcDestInstruction { 295 | get op() { return 'rol' } 296 | } 297 | export class RorInstruction extends SrcDestInstruction { 298 | get op() { return 'ror' } 299 | } 300 | export class RoundInstruction extends SrcDestInstruction { 301 | constructor(mode: number, src: Datum, readonly target: Datum, width: Width) { 302 | super({type: 'immediate', value: mode}, src, width) 303 | } 304 | get op() { return 'round' } 305 | get str() { return `${super.str}, ${datumToString(this.target)}` } 306 | } 307 | export class SarInstruction extends SrcDestInstruction { 308 | get op() { return 'sar' } 309 | } 310 | export class SetInstruction extends UnaryInstruction { 311 | constructor(dest: Datum, readonly cond: JumpCond) { 312 | super(dest) 313 | } 314 | get op() { return `set${this.cond}` } 315 | } 316 | export class ShlInstruction extends SrcDestInstruction { 317 | get op() { return 'shl' } 318 | } 319 | export class ShrInstruction extends SrcDestInstruction { 320 | get op() { return 'shr' } 321 | } 322 | export class SqrtInstruction extends SrcDestInstruction { 323 | get op() { return 'sqrt' } 324 | } 325 | export class SubInstruction extends SrcDestInstruction { 326 | get op() { return 'sub' } 327 | } 328 | export class SysCallInstruction { 329 | get str() { return 'syscall' } 330 | } 331 | export class TestInstruction extends SrcDestInstruction { 332 | get op() { return 'test' } 333 | } 334 | export class TzcntInstruction extends SrcDestInstruction { 335 | get op() { return 'tzcnt' } 336 | } 337 | export class XorInstruction extends SrcDestInstruction { 338 | get op() { return 'xor' } 339 | } 340 | export class XorPackedInstruction extends XorInstruction { 341 | get packed() { return true } 342 | } -------------------------------------------------------------------------------- /link-modules.ts: -------------------------------------------------------------------------------- 1 | import {Section} from './parse/module' 2 | import {ModuleIndices} from './compile/context' 3 | import {compileModule} from './compile/module' 4 | 5 | export interface NamedModules { 6 | [name: string]: Section[] 7 | } 8 | interface CompiledModuleString { 9 | assembly: string 10 | header: string 11 | } 12 | interface CompiledModules { 13 | [name: string]: CompiledModuleString 14 | } 15 | interface Stringable { 16 | readonly str: string 17 | } 18 | 19 | const makeString = (elements: Stringable[]): string => 20 | elements.map(({str}) => str + '\n').join('') 21 | 22 | export function linkModules(modules: NamedModules): CompiledModules { 23 | const moduleIndices: ModuleIndices = {} 24 | let moduleIndex = 0 25 | for (const moduleName in modules) moduleIndices[moduleName] = moduleIndex 26 | const compiledModules: CompiledModules = {} 27 | for (const moduleName in modules) { 28 | const {instructions, declarations} = compileModule({ 29 | module: modules[moduleName], 30 | index: moduleIndices[moduleName], 31 | moduleIndices, 32 | moduleName: moduleName 33 | }) 34 | compiledModules[moduleName] = { 35 | assembly: makeString(instructions), 36 | header: makeString(declarations) 37 | } 38 | } 39 | return compiledModules 40 | } -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | import {promisify} from 'util' 6 | import {linkModules, NamedModules} from './link-modules' 7 | import {parseModule} from './parse/module' 8 | 9 | const WASM_EXTENSION = '.wasm', 10 | ASM_EXTENSION = '.s', 11 | HEADER_EXTENSION = '.h' 12 | 13 | const readFile = promisify(fs.readFile), 14 | writeFile = promisify(fs.writeFile) 15 | 16 | interface ModuleFiles { 17 | [module: string]: string 18 | } 19 | 20 | (async () => { 21 | const modules = await Promise.all(process.argv.slice(2).map(async file => { 22 | if (!file.endsWith(WASM_EXTENSION)) { 23 | throw new Error(`File ${file} is not a .wasm file`) 24 | } 25 | 26 | const wasm = await readFile(file) 27 | const dataView = new DataView(wasm.buffer, wasm.byteOffset, wasm.byteLength) 28 | return {file, module: parseModule(dataView).value} 29 | })) 30 | const namedModules: NamedModules = {} 31 | const files: ModuleFiles = {} 32 | for (const {file, module} of modules) { 33 | const baseFile = file.slice(0, -WASM_EXTENSION.length) 34 | const name = path.basename(baseFile) 35 | namedModules[name] = module 36 | files[name] = baseFile 37 | } 38 | const compiledModules = linkModules(namedModules) 39 | await Promise.all(Object.keys(compiledModules).map(name => { 40 | const {assembly, header} = compiledModules[name] 41 | const baseFile = files[name] 42 | return Promise.all([ 43 | writeFile(baseFile + ASM_EXTENSION, assembly), 44 | writeFile(baseFile + HEADER_EXTENSION, header) 45 | ]) 46 | })) 47 | })().catch(e => { 48 | console.error(e) 49 | process.exit(1) 50 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-to-asm", 3 | "version": "1.0.0", 4 | "description": "Combile WASM to x86_64", 5 | "main": "main.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "wabt": "bash -c '[[ -d test/wabt ]]' || (git clone --recursive --depth 1 https://github.com/WebAssembly/wabt test/wabt && cd test/wabt && make clang-release-no-tests)", 9 | "clone-spec": "rm -rf test/spec && cd test && git clone https://github.com/WebAssembly/spec && cd spec && git checkout 704d9d9e9c861fdb957c3d5e928f1d046a31497e", 10 | "ava": "ava -v -T 1m test/main.js", 11 | "test": "npm run build && npm run wabt && tsc -p test/tsconfig.json && npm run clone-spec && npm run ava" 12 | }, 13 | "author": "Caleb Sander", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/node": "^13.9.8", 17 | "ava": "^3.5.2", 18 | "typescript": "^3.8.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /parse/index.ts: -------------------------------------------------------------------------------- 1 | const slice = ({buffer, byteOffset}: DataView, offset: number) => 2 | new DataView(buffer, byteOffset + offset) 3 | 4 | export interface ParseResult { 5 | value: A 6 | length: number 7 | } 8 | export type Parser = (data: DataView) => ParseResult 9 | 10 | export const parseReturn = (value: A): Parser => 11 | _ => ({value, length: 0}) 12 | export const parseAndThen = 13 | (parser: Parser, f: (a: A) => Parser): Parser => data => { 14 | const {value: a, length} = parser(data) 15 | const {value, length: length2} = f(a)(slice(data, length)) 16 | return {value, length: length + length2} 17 | } 18 | export const parseMap = (parser: Parser, f: (a: A) => B) => 19 | parseAndThen(parser, a => parseReturn(f(a))) 20 | export const parseIgnore = (parser1: Parser, parser2: Parser) => 21 | parseAndThen(parser1, _ => parser2) 22 | export const parseByte: Parser = 23 | data => ({value: data.getUint8(0), length: 1}) 24 | export const parseExact = (byte: number) => 25 | parseMap(parseByte, readByte => { 26 | if (readByte !== byte) throw new Error('Mismatched bytes') 27 | }) 28 | export const parseChoice = (parsers: Parser[]): Parser => data => { 29 | for (const parser of parsers) { 30 | try { return parser(data) } 31 | catch {} 32 | } 33 | throw new Error('No parser succeeded') 34 | } 35 | export const parseEnd: Parser = ({byteLength}) => { 36 | if (byteLength) throw new Error(`Buffer still has ${byteLength} bytes`) 37 | return {value: undefined, length: 0} 38 | } 39 | // These parse functions are implemented iteratively for efficiency, 40 | // and because V8 only allows a stack height of ~10,000 41 | const parseTimes = (parser: Parser) => (times: number): Parser => 42 | data => { 43 | const arr = new Array(times) 44 | let offset = 0 45 | for (let i = 0; i < times; i++) { 46 | const {value, length} = parser(slice(data, offset)) 47 | arr[i] = value 48 | offset += length 49 | } 50 | return {value: arr, length: offset} 51 | } 52 | export const parseWhile = (parser: Parser): Parser => data => { 53 | const arr: A[] = [] 54 | let offset = 0 55 | while (true) { 56 | try { 57 | const {value, length} = parser(slice(data, offset)) 58 | arr.push(value) 59 | offset += length 60 | } 61 | catch { break } 62 | } 63 | return {value: arr, length: offset} 64 | } 65 | export const parseUntil = (parser: Parser, end: Parser) => 66 | parseAndThen(parseWhile(parser), result => 67 | parseIgnore(end, parseReturn(result)) 68 | ) 69 | 70 | export const parseByOpcode = (parsers: Map>) => 71 | parseAndThen(parseByte, opcode => { 72 | const parser = parsers.get(opcode) 73 | if (!parser) throw new Error(`Unexpected opcode ${opcode}`) 74 | return parser 75 | }) 76 | export const parseUnsigned: Parser = parseAndThen(parseByte, n => 77 | n >> 7 78 | ? parseMap(parseUnsigned, m => (m << 7 | (n & ~(1 << 7))) >>> 0) 79 | : parseReturn(n) 80 | ) 81 | export const parseVector = (parser: Parser) => 82 | parseAndThen(parseUnsigned, parseTimes(parser)) -------------------------------------------------------------------------------- /parse/instruction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Parser, 3 | parseAndThen, 4 | parseByOpcode, 5 | parseByte, 6 | parseChoice, 7 | parseExact, 8 | parseIgnore, 9 | parseMap, 10 | parseReturn, 11 | parseUnsigned, 12 | parseUntil, 13 | parseVector, 14 | parseWhile 15 | } from '.' 16 | import {ValueType, parseValueType} from './value-type' 17 | 18 | export type ResultType = ValueType | 'empty' 19 | type Label = number 20 | type FunctionIndex = number 21 | type TypeIndex = number 22 | type LocalIndex = number 23 | type GlobalIndex = number 24 | interface MemoryAccess { 25 | align: number 26 | offset: number 27 | } 28 | export interface BlockInstruction { 29 | type: 'block' | 'loop' 30 | returns: ResultType 31 | instructions: Instruction[] 32 | } 33 | export interface IfInstruction { 34 | type: 'if' 35 | returns: ResultType 36 | ifInstructions: Instruction[] 37 | elseInstructions: Instruction[] 38 | } 39 | export interface BranchInstruction { 40 | type: 'br' | 'br_if' 41 | label: Label 42 | } 43 | export interface BranchTableInstruction { 44 | type: 'br_table' 45 | cases: Label[] 46 | defaultCase: Label 47 | } 48 | export interface CallInstruction { 49 | type: 'call' 50 | func: FunctionIndex 51 | } 52 | export interface CallIndirectInstruction { 53 | type: 'call_indirect' 54 | funcType: TypeIndex 55 | } 56 | type ControlInstruction 57 | = {type: 'unreachable' | 'nop' | 'return'} 58 | | BlockInstruction 59 | | IfInstruction 60 | | BranchInstruction 61 | | BranchTableInstruction 62 | | CallInstruction 63 | | CallIndirectInstruction 64 | type ParametricInstruction = {type: 'drop' | 'select'} 65 | export interface LocalInstruction { 66 | type: 'get_local' | 'set_local' | 'tee_local' 67 | local: LocalIndex 68 | } 69 | type VariableInstruction 70 | = LocalInstruction 71 | | {type: 'get_global' | 'set_global', global: GlobalIndex} 72 | export interface LoadStoreInstruction { 73 | type: 74 | 'i32.load' | 75 | 'i64.load' | 76 | 'f32.load' | 77 | 'f64.load' | 78 | 'i32.load8_s' | 79 | 'i32.load8_u' | 80 | 'i32.load16_s' | 81 | 'i32.load16_u' | 82 | 'i64.load8_s' | 83 | 'i64.load8_u' | 84 | 'i64.load16_s' | 85 | 'i64.load16_u' | 86 | 'i64.load32_s' | 87 | 'i64.load32_u' | 88 | 'i32.store' | 89 | 'i64.store' | 90 | 'f32.store' | 91 | 'f64.store' | 92 | 'i32.store8' | 93 | 'i32.store16' | 94 | 'i64.store8' | 95 | 'i64.store16' | 96 | 'i64.store32' 97 | access: MemoryAccess 98 | } 99 | type MemoryInstruction 100 | = LoadStoreInstruction 101 | | {type: 'memory.size' | 'memory.grow'} 102 | export type ConstInstruction 103 | = {type: 'i32.const' | 'f32.const' | 'f64.const', value: number} 104 | | {type: 'i64.const', value: bigint} 105 | type NumericInstruction 106 | = ConstInstruction 107 | | {type: 108 | 'i32.eqz' | 109 | 'i32.eq' | 110 | 'i32.ne' | 111 | 'i32.lt_s' | 112 | 'i32.lt_u' | 113 | 'i32.gt_s' | 114 | 'i32.gt_u' | 115 | 'i32.le_s' | 116 | 'i32.le_u' | 117 | 'i32.ge_s' | 118 | 'i32.ge_u' | 119 | 120 | 'i64.eqz' | 121 | 'i64.eq' | 122 | 'i64.ne' | 123 | 'i64.lt_s' | 124 | 'i64.lt_u' | 125 | 'i64.gt_s' | 126 | 'i64.gt_u' | 127 | 'i64.le_s' | 128 | 'i64.le_u' | 129 | 'i64.ge_s' | 130 | 'i64.ge_u' | 131 | 132 | 'f32.eq' | 133 | 'f32.ne' | 134 | 'f32.lt' | 135 | 'f32.gt' | 136 | 'f32.le' | 137 | 'f32.ge' | 138 | 139 | 'f64.eq' | 140 | 'f64.ne' | 141 | 'f64.lt' | 142 | 'f64.gt' | 143 | 'f64.le' | 144 | 'f64.ge' | 145 | 146 | 'i32.clz' | 147 | 'i32.ctz' | 148 | 'i32.popcnt' | 149 | 'i32.add' | 150 | 'i32.sub' | 151 | 'i32.mul' | 152 | 'i32.div_s' | 153 | 'i32.div_u' | 154 | 'i32.rem_s' | 155 | 'i32.rem_u' | 156 | 'i32.and' | 157 | 'i32.or' | 158 | 'i32.xor' | 159 | 'i32.shl' | 160 | 'i32.shr_s' | 161 | 'i32.shr_u' | 162 | 'i32.rotl' | 163 | 'i32.rotr' | 164 | 165 | 'i64.clz' | 166 | 'i64.ctz' | 167 | 'i64.popcnt' | 168 | 'i64.add' | 169 | 'i64.sub' | 170 | 'i64.mul' | 171 | 'i64.div_s' | 172 | 'i64.div_u' | 173 | 'i64.rem_s' | 174 | 'i64.rem_u' | 175 | 'i64.and' | 176 | 'i64.or' | 177 | 'i64.xor' | 178 | 'i64.shl' | 179 | 'i64.shr_s' | 180 | 'i64.shr_u' | 181 | 'i64.rotl' | 182 | 'i64.rotr' | 183 | 184 | 'f32.abs' | 185 | 'f32.neg' | 186 | 'f32.ceil' | 187 | 'f32.floor' | 188 | 'f32.trunc' | 189 | 'f32.nearest' | 190 | 'f32.sqrt' | 191 | 'f32.add' | 192 | 'f32.sub' | 193 | 'f32.mul' | 194 | 'f32.div' | 195 | 'f32.min' | 196 | 'f32.max' | 197 | 'f32.copysign' | 198 | 199 | 'f64.abs' | 200 | 'f64.neg' | 201 | 'f64.ceil' | 202 | 'f64.floor' | 203 | 'f64.trunc' | 204 | 'f64.nearest' | 205 | 'f64.sqrt' | 206 | 'f64.add' | 207 | 'f64.sub' | 208 | 'f64.mul' | 209 | 'f64.div' | 210 | 'f64.min' | 211 | 'f64.max' | 212 | 'f64.copysign' | 213 | 214 | 'i32.wrap' | 215 | 'i32.trunc_s/f32' | 216 | 'i32.trunc_u/f32' | 217 | 'i32.trunc_s/f64' | 218 | 'i32.trunc_u/f64' | 219 | 'i64.extend_s' | 220 | 'i64.extend_u' | 221 | 'i64.trunc_s/f32' | 222 | 'i64.trunc_u/f32' | 223 | 'i64.trunc_s/f64' | 224 | 'i64.trunc_u/f64' | 225 | 'f32.convert_s/i32' | 226 | 'f32.convert_u/i32' | 227 | 'f32.convert_s/i64' | 228 | 'f32.convert_u/i64' | 229 | 'f32.demote' | 230 | 'f64.convert_s/i32' | 231 | 'f64.convert_u/i32' | 232 | 'f64.convert_s/i64' | 233 | 'f64.convert_u/i64' | 234 | 'f64.promote' | 235 | 'i32.reinterpret' | 236 | 'i64.reinterpret' | 237 | 'f32.reinterpret' | 238 | 'f64.reinterpret' 239 | } 240 | export type Instruction 241 | = ControlInstruction 242 | | ParametricInstruction 243 | | VariableInstruction 244 | | MemoryInstruction 245 | | NumericInstruction 246 | 247 | const BODY_END = 0x0B 248 | 249 | const instructionParsers = new Map>() 250 | export const parseInstruction = parseByOpcode(instructionParsers) 251 | export const parseBody = parseUntil(parseInstruction, parseExact(BODY_END)) 252 | const parseIfBody = 253 | parseAndThen(parseWhile(parseInstruction), ifInstructions => 254 | parseByOpcode(new Map([ 255 | [0x05, parseMap(parseBody, elseInstructions => 256 | ({ifInstructions, elseInstructions}) 257 | )], 258 | [BODY_END, parseReturn({ifInstructions, elseInstructions: []})] 259 | ])) 260 | ) 261 | const parseResultType = parseChoice([ 262 | parseIgnore(parseExact(0x40), parseReturn('empty' as const)), 263 | parseValueType 264 | ]) 265 | const parseBlockLike = (type: 'block' | 'loop') => 266 | parseAndThen(parseResultType, returns => 267 | parseMap(parseBody, instructions => ({type, returns, instructions})) 268 | ) 269 | const parseBranchLike = (type: 'br' | 'br_if') => 270 | parseMap(parseUnsigned, label => ({type, label})) 271 | const parseFixedInstruction = (type: TYPE) => 272 | parseReturn({type}) 273 | const parseSigned: Parser = parseAndThen(parseByte, n => 274 | n >> 7 275 | ? parseMap(parseSigned, m => m << 7n | BigInt.asUintN(7, BigInt(n))) 276 | : parseReturn(BigInt.asIntN(7, BigInt(n))) // sign-extend lower 7 bits 277 | ) 278 | const parseLocalInstruction = (type: TYPE) => 279 | parseMap(parseUnsigned, local => ({type, local})) 280 | const parseGlobalInstruction = (type: TYPE) => 281 | parseMap(parseUnsigned, global => ({type, global})) 282 | const parseMemoryAccess = parseAndThen(parseUnsigned, align => 283 | parseMap(parseUnsigned, offset => ({align, offset})) 284 | ) 285 | const parseMemoryInstruction = (type: TYPE) => 286 | parseMap(parseMemoryAccess, access => ({type, access})) 287 | 288 | const opcodeParsers: [number, Parser][] = [ 289 | [0x00, parseFixedInstruction('unreachable')], 290 | [0x01, parseFixedInstruction('nop')], 291 | [0x02, parseBlockLike('block')], 292 | [0x03, parseBlockLike('loop')], 293 | [0x04, parseAndThen(parseResultType, returns => 294 | parseMap(parseIfBody, ifBody => ({type: 'if', returns, ...ifBody})) 295 | )], 296 | [0x0C, parseBranchLike('br')], 297 | [0x0D, parseBranchLike('br_if')], 298 | [0x0E, parseAndThen(parseVector(parseUnsigned), cases => 299 | parseMap(parseUnsigned, defaultCase => 300 | ({type: 'br_table', cases, defaultCase}) 301 | ) 302 | )], 303 | [0x0F, parseFixedInstruction('return')], 304 | [0x10, parseMap(parseUnsigned, func => ({type: 'call', func}))], 305 | [0x11, parseAndThen(parseUnsigned, funcType => 306 | parseIgnore(parseExact(0x00), 307 | parseReturn({type: 'call_indirect', funcType}) 308 | ) 309 | )], 310 | 311 | [0x1A, parseFixedInstruction('drop')], 312 | [0x1B, parseFixedInstruction('select')], 313 | 314 | [0x20, parseLocalInstruction('get_local')], 315 | [0x21, parseLocalInstruction('set_local')], 316 | [0x22, parseLocalInstruction('tee_local')], 317 | [0x23, parseGlobalInstruction('get_global')], 318 | [0x24, parseGlobalInstruction('set_global')], 319 | 320 | [0x28, parseMemoryInstruction('i32.load')], 321 | [0x29, parseMemoryInstruction('i64.load')], 322 | [0x2A, parseMemoryInstruction('f32.load')], 323 | [0x2B, parseMemoryInstruction('f64.load')], 324 | [0x2C, parseMemoryInstruction('i32.load8_s')], 325 | [0x2D, parseMemoryInstruction('i32.load8_u')], 326 | [0x2E, parseMemoryInstruction('i32.load16_s')], 327 | [0x2F, parseMemoryInstruction('i32.load16_u')], 328 | [0x30, parseMemoryInstruction('i64.load8_s')], 329 | [0x31, parseMemoryInstruction('i64.load8_u')], 330 | [0x32, parseMemoryInstruction('i64.load16_s')], 331 | [0x33, parseMemoryInstruction('i64.load16_u')], 332 | [0x34, parseMemoryInstruction('i64.load32_s')], 333 | [0x35, parseMemoryInstruction('i64.load32_u')], 334 | [0x36, parseMemoryInstruction('i32.store')], 335 | [0x37, parseMemoryInstruction('i64.store')], 336 | [0x38, parseMemoryInstruction('f32.store')], 337 | [0x39, parseMemoryInstruction('f64.store')], 338 | [0x3A, parseMemoryInstruction('i32.store8')], 339 | [0x3B, parseMemoryInstruction('i32.store16')], 340 | [0x3C, parseMemoryInstruction('i64.store8')], 341 | [0x3D, parseMemoryInstruction('i64.store16')], 342 | [0x3E, parseMemoryInstruction('i64.store32')], 343 | [0x3F, parseIgnore(parseExact(0x00), parseFixedInstruction('memory.size'))], 344 | [0x40, parseIgnore(parseExact(0x00), parseFixedInstruction('memory.grow'))], 345 | 346 | [0x41, parseMap(parseSigned, n => ({type: 'i32.const', value: Number(n)}))], 347 | [0x42, parseMap(parseSigned, value => ({type: 'i64.const', value}))], 348 | [0x43, parseMap( 349 | data => ({value: data.getFloat32(0, true), length: 4}), 350 | value => ({type: 'f32.const', value}) 351 | )], 352 | [0x44, parseMap( 353 | data => ({value: data.getFloat64(0, true), length: 8}), 354 | value => ({type: 'f64.const', value}) 355 | )], 356 | 357 | [0x45, parseFixedInstruction('i32.eqz')], 358 | [0x46, parseFixedInstruction('i32.eq')], 359 | [0x47, parseFixedInstruction('i32.ne')], 360 | [0x48, parseFixedInstruction('i32.lt_s')], 361 | [0x49, parseFixedInstruction('i32.lt_u')], 362 | [0x4A, parseFixedInstruction('i32.gt_s')], 363 | [0x4B, parseFixedInstruction('i32.gt_u')], 364 | [0x4C, parseFixedInstruction('i32.le_s')], 365 | [0x4D, parseFixedInstruction('i32.le_u')], 366 | [0x4E, parseFixedInstruction('i32.ge_s')], 367 | [0x4F, parseFixedInstruction('i32.ge_u')], 368 | 369 | [0x50, parseFixedInstruction('i64.eqz')], 370 | [0x51, parseFixedInstruction('i64.eq')], 371 | [0x52, parseFixedInstruction('i64.ne')], 372 | [0x53, parseFixedInstruction('i64.lt_s')], 373 | [0x54, parseFixedInstruction('i64.lt_u')], 374 | [0x55, parseFixedInstruction('i64.gt_s')], 375 | [0x56, parseFixedInstruction('i64.gt_u')], 376 | [0x57, parseFixedInstruction('i64.le_s')], 377 | [0x58, parseFixedInstruction('i64.le_u')], 378 | [0x59, parseFixedInstruction('i64.ge_s')], 379 | [0x5A, parseFixedInstruction('i64.ge_u')], 380 | 381 | [0x5B, parseFixedInstruction('f32.eq')], 382 | [0x5C, parseFixedInstruction('f32.ne')], 383 | [0x5D, parseFixedInstruction('f32.lt')], 384 | [0x5E, parseFixedInstruction('f32.gt')], 385 | [0x5F, parseFixedInstruction('f32.le')], 386 | [0x60, parseFixedInstruction('f32.ge')], 387 | 388 | [0x61, parseFixedInstruction('f64.eq')], 389 | [0x62, parseFixedInstruction('f64.ne')], 390 | [0x63, parseFixedInstruction('f64.lt')], 391 | [0x64, parseFixedInstruction('f64.gt')], 392 | [0x65, parseFixedInstruction('f64.le')], 393 | [0x66, parseFixedInstruction('f64.ge')], 394 | 395 | [0x67, parseFixedInstruction('i32.clz')], 396 | [0x68, parseFixedInstruction('i32.ctz')], 397 | [0x69, parseFixedInstruction('i32.popcnt')], 398 | [0x6A, parseFixedInstruction('i32.add')], 399 | [0x6B, parseFixedInstruction('i32.sub')], 400 | [0x6C, parseFixedInstruction('i32.mul')], 401 | [0x6D, parseFixedInstruction('i32.div_s')], 402 | [0x6E, parseFixedInstruction('i32.div_u')], 403 | [0x6F, parseFixedInstruction('i32.rem_s')], 404 | [0x70, parseFixedInstruction('i32.rem_u')], 405 | [0x71, parseFixedInstruction('i32.and')], 406 | [0x72, parseFixedInstruction('i32.or')], 407 | [0x73, parseFixedInstruction('i32.xor')], 408 | [0x74, parseFixedInstruction('i32.shl')], 409 | [0x75, parseFixedInstruction('i32.shr_s')], 410 | [0x76, parseFixedInstruction('i32.shr_u')], 411 | [0x77, parseFixedInstruction('i32.rotl')], 412 | [0x78, parseFixedInstruction('i32.rotr')], 413 | 414 | [0x79, parseFixedInstruction('i64.clz')], 415 | [0x7A, parseFixedInstruction('i64.ctz')], 416 | [0x7B, parseFixedInstruction('i64.popcnt')], 417 | [0x7C, parseFixedInstruction('i64.add')], 418 | [0x7D, parseFixedInstruction('i64.sub')], 419 | [0x7E, parseFixedInstruction('i64.mul')], 420 | [0x7F, parseFixedInstruction('i64.div_s')], 421 | [0x80, parseFixedInstruction('i64.div_u')], 422 | [0x81, parseFixedInstruction('i64.rem_s')], 423 | [0x82, parseFixedInstruction('i64.rem_u')], 424 | [0x83, parseFixedInstruction('i64.and')], 425 | [0x84, parseFixedInstruction('i64.or')], 426 | [0x85, parseFixedInstruction('i64.xor')], 427 | [0x86, parseFixedInstruction('i64.shl')], 428 | [0x87, parseFixedInstruction('i64.shr_s')], 429 | [0x88, parseFixedInstruction('i64.shr_u')], 430 | [0x89, parseFixedInstruction('i64.rotl')], 431 | [0x8A, parseFixedInstruction('i64.rotr')], 432 | 433 | [0x8B, parseFixedInstruction('f32.abs')], 434 | [0x8C, parseFixedInstruction('f32.neg')], 435 | [0x8D, parseFixedInstruction('f32.ceil')], 436 | [0x8E, parseFixedInstruction('f32.floor')], 437 | [0x8F, parseFixedInstruction('f32.trunc')], 438 | [0x90, parseFixedInstruction('f32.nearest')], 439 | [0x91, parseFixedInstruction('f32.sqrt')], 440 | [0x92, parseFixedInstruction('f32.add')], 441 | [0x93, parseFixedInstruction('f32.sub')], 442 | [0x94, parseFixedInstruction('f32.mul')], 443 | [0x95, parseFixedInstruction('f32.div')], 444 | [0x96, parseFixedInstruction('f32.min')], 445 | [0x97, parseFixedInstruction('f32.max')], 446 | [0x98, parseFixedInstruction('f32.copysign')], 447 | 448 | [0x99, parseFixedInstruction('f64.abs')], 449 | [0x9A, parseFixedInstruction('f64.neg')], 450 | [0x9B, parseFixedInstruction('f64.ceil')], 451 | [0x9C, parseFixedInstruction('f64.floor')], 452 | [0x9D, parseFixedInstruction('f64.trunc')], 453 | [0x9E, parseFixedInstruction('f64.nearest')], 454 | [0x9F, parseFixedInstruction('f64.sqrt')], 455 | [0xA0, parseFixedInstruction('f64.add')], 456 | [0xA1, parseFixedInstruction('f64.sub')], 457 | [0xA2, parseFixedInstruction('f64.mul')], 458 | [0xA3, parseFixedInstruction('f64.div')], 459 | [0xA4, parseFixedInstruction('f64.min')], 460 | [0xA5, parseFixedInstruction('f64.max')], 461 | [0xA6, parseFixedInstruction('f64.copysign')], 462 | 463 | [0xA7, parseFixedInstruction('i32.wrap')], 464 | [0xA8, parseFixedInstruction('i32.trunc_s/f32')], 465 | [0xA9, parseFixedInstruction('i32.trunc_u/f32')], 466 | [0xAA, parseFixedInstruction('i32.trunc_s/f64')], 467 | [0xAB, parseFixedInstruction('i32.trunc_u/f64')], 468 | [0xAC, parseFixedInstruction('i64.extend_s')], 469 | [0xAD, parseFixedInstruction('i64.extend_u')], 470 | [0xAE, parseFixedInstruction('i64.trunc_s/f32')], 471 | [0xAF, parseFixedInstruction('i64.trunc_u/f32')], 472 | [0xB0, parseFixedInstruction('i64.trunc_s/f64')], 473 | [0xB1, parseFixedInstruction('i64.trunc_u/f64')], 474 | [0xB2, parseFixedInstruction('f32.convert_s/i32')], 475 | [0xB3, parseFixedInstruction('f32.convert_u/i32')], 476 | [0xB4, parseFixedInstruction('f32.convert_s/i64')], 477 | [0xB5, parseFixedInstruction('f32.convert_u/i64')], 478 | [0xB6, parseFixedInstruction('f32.demote')], 479 | [0xB7, parseFixedInstruction('f64.convert_s/i32')], 480 | [0xB8, parseFixedInstruction('f64.convert_u/i32')], 481 | [0xB9, parseFixedInstruction('f64.convert_s/i64')], 482 | [0xBA, parseFixedInstruction('f64.convert_u/i64')], 483 | [0xBB, parseFixedInstruction('f64.promote')], 484 | [0xBC, parseFixedInstruction('i32.reinterpret')], 485 | [0xBD, parseFixedInstruction('i64.reinterpret')], 486 | [0xBE, parseFixedInstruction('f32.reinterpret')], 487 | [0xBF, parseFixedInstruction('f64.reinterpret')] 488 | ] 489 | for (const [opcode, parser] of opcodeParsers) { 490 | instructionParsers.set(opcode, parser) 491 | } -------------------------------------------------------------------------------- /parse/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Parser, 3 | ParseResult, 4 | parseAndThen, 5 | parseByOpcode, 6 | parseEnd, 7 | parseExact, 8 | parseIgnore, 9 | parseMap, 10 | parseReturn, 11 | parseUnsigned, 12 | parseUntil, 13 | parseVector 14 | } from '.' 15 | import {Instruction, parseBody} from './instruction' 16 | import {ValueType, parseValueType} from './value-type' 17 | 18 | const VERSION = 1 19 | 20 | export interface FunctionType { 21 | params: ValueType[] 22 | results: ValueType[] 23 | } 24 | interface Limits { 25 | min: number 26 | max?: number 27 | } 28 | export interface GlobalType { 29 | type: ValueType 30 | mutable: boolean 31 | } 32 | type ImportDescription 33 | = {type: 'function', typeIndex: number} 34 | | {type: 'table' | 'memory', limits: Limits} 35 | | {type: 'global', valueType: GlobalType} 36 | export interface Import { 37 | module: string 38 | name: string 39 | description: ImportDescription 40 | } 41 | export interface Global { 42 | type: GlobalType 43 | initializer: Instruction[] 44 | } 45 | interface ExportDescription { 46 | type: 'function' | 'table' | 'memory' | 'global' 47 | index: number 48 | } 49 | export interface Export { 50 | name: string 51 | description: ExportDescription 52 | } 53 | export interface TableInitializer { 54 | tableIndex: number 55 | offset: Instruction[] 56 | functionIndices: number[] 57 | } 58 | interface CodeSegment { 59 | locals: ValueType[] 60 | instructions: Instruction[] 61 | } 62 | export interface MemoryInitializer { 63 | memoryIndex: number 64 | offset: Instruction[] 65 | data: ArrayBuffer 66 | } 67 | interface CustomSection { 68 | type: 'custom' 69 | name: string 70 | contents: ArrayBuffer 71 | } 72 | interface TypeSection { 73 | type: 'type' 74 | types: FunctionType[] 75 | } 76 | interface ImportSection { 77 | type: 'import' 78 | imports: Import[] 79 | } 80 | interface FunctionSection { 81 | type: 'function' 82 | typeIndices: number[] 83 | } 84 | interface TableSection { 85 | type: 'table' 86 | tables: Limits[] 87 | } 88 | interface MemorySection { 89 | type: 'memory' 90 | memories: Limits[] 91 | } 92 | interface GlobalSection { 93 | type: 'global' 94 | globals: Global[] 95 | } 96 | interface ExportSection { 97 | type: 'export' 98 | exports: Export[] 99 | } 100 | interface StartSection { 101 | type: 'start' 102 | functionIndex: number 103 | } 104 | interface ElementSection { 105 | type: 'element' 106 | initializers: TableInitializer[] 107 | } 108 | interface CodeSection { 109 | type: 'code' 110 | segments: CodeSegment[] 111 | } 112 | interface DataSection { 113 | type: 'data' 114 | initializers: MemoryInitializer[] 115 | } 116 | export type Section 117 | = CustomSection 118 | | TypeSection 119 | | ImportSection 120 | | FunctionSection 121 | | TableSection 122 | | MemorySection 123 | | GlobalSection 124 | | ExportSection 125 | | StartSection 126 | | ElementSection 127 | | CodeSection 128 | | DataSection 129 | 130 | const parseByteVector = parseAndThen(parseUnsigned, length => 131 | ({buffer, byteOffset}) => 132 | ({value: buffer.slice(byteOffset, byteOffset + length), length}) 133 | ) 134 | const parseName = 135 | parseMap(parseByteVector, utf8 => Buffer.from(utf8).toString()) 136 | const parseValueTypes = parseVector(parseValueType) 137 | const parseFuncType = parseIgnore(parseExact(0x60), 138 | parseAndThen(parseValueTypes, params => 139 | parseMap(parseValueTypes, results => ({params, results})) 140 | ) 141 | ) 142 | const parseBoolean = parseByOpcode(new Map([ 143 | [0, parseReturn(false)], 144 | [1, parseReturn(true)] 145 | ])) 146 | const parseLimits = parseAndThen(parseBoolean, hasMax => 147 | parseAndThen(parseUnsigned, min => 148 | parseMap( 149 | hasMax ? parseUnsigned : parseReturn(undefined), 150 | max => ({min, max}) 151 | ) 152 | ) 153 | ) 154 | const parseTableType = parseIgnore(parseExact(0x70), parseLimits) 155 | const parseGlobalType = parseAndThen(parseValueType, type => 156 | parseMap(parseBoolean, mutable => ({type, mutable})) 157 | ) 158 | const parseImportDescription = 159 | parseByOpcode(new Map>([ 160 | [0, parseMap(parseUnsigned, typeIndex => ({type: 'function', typeIndex}))], 161 | [1, parseMap(parseTableType, limits => ({type: 'table', limits}))], 162 | [2, parseMap(parseLimits, limits => ({type: 'memory', limits}))], 163 | [3, parseMap(parseGlobalType, valueType => ({type: 'global', valueType}))] 164 | ])) 165 | const parseImport = parseAndThen(parseName, module => 166 | parseAndThen(parseName, name => 167 | parseMap(parseImportDescription, description => 168 | ({module, name, description}) 169 | ) 170 | ) 171 | ) 172 | const parseExportDescription = 173 | parseByOpcode(new Map>([ 174 | [0, parseMap(parseUnsigned, index => ({type: 'function', index}))], 175 | [1, parseMap(parseUnsigned, index => ({type: 'table', index}))], 176 | [2, parseMap(parseUnsigned, index => ({type: 'memory', index}))], 177 | [3, parseMap(parseUnsigned, index => ({type: 'global', index}))] 178 | ])) 179 | const parseExport = parseAndThen(parseName, name => 180 | parseMap(parseExportDescription, description => ({name, description})) 181 | ) 182 | const parseTableInitializer = parseAndThen(parseUnsigned, tableIndex => 183 | parseAndThen(parseBody, offset => 184 | parseMap(parseVector(parseUnsigned), functionIndices => 185 | ({tableIndex, offset, functionIndices}) 186 | ) 187 | ) 188 | ) 189 | const parseLocal = parseAndThen(parseUnsigned, count => 190 | parseMap(parseValueType, type => ({count, type})) 191 | ) 192 | const parseCodeSegment = parseIgnore(parseUnsigned, 193 | parseAndThen(parseVector(parseLocal), localRanges => 194 | parseMap(parseBody, instructions => { 195 | const locals: ValueType[] = [] 196 | for (const {count, type} of localRanges) { 197 | locals.push(...new Array(count).fill(type)) 198 | } 199 | return {locals, instructions} 200 | }) 201 | ) 202 | ) 203 | const parseMemoryInitializer = parseAndThen(parseUnsigned, memoryIndex => 204 | parseAndThen(parseBody, offset => 205 | parseMap(parseByteVector, data => ({memoryIndex, offset, data})) 206 | ) 207 | ) 208 | 209 | const sectionParsers = new Map([ 210 | [0, parseAndThen(parseUnsigned, length => 211 | data => parseAndThen(parseName, name => 212 | ({buffer, byteOffset}): ParseResult
=> { 213 | const contents = buffer.slice(byteOffset, data.byteOffset + length) 214 | return { 215 | value: {type: 'custom', name, contents}, 216 | length: contents.byteLength 217 | } 218 | } 219 | )(data) 220 | )] 221 | ]) 222 | const opcodeParsers: [number, Parser
][] = [ 223 | [1, parseMap(parseVector(parseFuncType), types => ({type: 'type', types}))], 224 | [2, parseMap(parseVector(parseImport), imports => 225 | ({type: 'import', imports}) 226 | )], 227 | [3, parseMap(parseVector(parseUnsigned), typeIndices => 228 | ({type: 'function', typeIndices}) 229 | )], 230 | [4, parseMap(parseVector(parseTableType), tables => 231 | ({type: 'table', tables}) 232 | )], 233 | [5, parseMap(parseVector(parseLimits), memories => 234 | ({type: 'memory', memories}) 235 | )], 236 | [6, parseMap( 237 | parseVector( 238 | parseAndThen(parseGlobalType, type => 239 | parseMap(parseBody, initializer => ({type, initializer})) 240 | ) 241 | ), 242 | globals => ({type: 'global', globals}) 243 | )], 244 | [7, parseMap(parseVector(parseExport), exports => 245 | ({type: 'export', exports}) 246 | )], 247 | [8, parseMap(parseUnsigned, functionIndex => 248 | ({type: 'start', functionIndex}) 249 | )], 250 | [9, parseMap(parseVector(parseTableInitializer), initializers => 251 | ({type: 'element', initializers}) 252 | )], 253 | [10, parseMap(parseVector(parseCodeSegment), segments => 254 | ({type: 'code', segments}) 255 | )], 256 | [11, parseMap(parseVector(parseMemoryInitializer), initializers => 257 | ({type: 'data', initializers}) 258 | )] 259 | ] 260 | for (const [id, parser] of opcodeParsers) { 261 | sectionParsers.set(id, parseIgnore(parseUnsigned, parser)) 262 | } 263 | const parseSection = parseByOpcode(sectionParsers) 264 | const parseMagic: Parser = data => { 265 | if (data.getUint32(0) !== 0x0061736D) throw new Error('Invalid magic bytes') 266 | return {value: undefined, length: 4} 267 | } 268 | const parseVersion: Parser = data => 269 | ({value: data.getUint32(0, true), length: 4}) 270 | export const parseModule = parseIgnore(parseMagic, 271 | parseIgnore( 272 | parseMap(parseVersion, version => { 273 | if (version !== VERSION) throw new Error(`Unsupported version ${version}`) 274 | }), 275 | parseUntil(parseSection, parseEnd) 276 | ) 277 | ) -------------------------------------------------------------------------------- /parse/value-type.ts: -------------------------------------------------------------------------------- 1 | import {parseByOpcode, parseReturn, Parser} from '.' 2 | 3 | export type ValueType 4 | = 'i32' 5 | | 'i64' 6 | | 'f32' 7 | | 'f64' 8 | 9 | export const parseValueType: Parser = 10 | parseByOpcode(new Map>([ 11 | [0x7F, parseReturn('i32')], 12 | [0x7E, parseReturn('i64')], 13 | [0x7D, parseReturn('f32')], 14 | [0x7C, parseReturn('f64')] 15 | ])) -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | spec 2 | wabt 3 | 4 | *.h 5 | *.s 6 | *.wasm 7 | *-test -------------------------------------------------------------------------------- /test/fib-expected.txt: -------------------------------------------------------------------------------- 1 | 0: 0 2 | 1: 1 3 | 2: 1 4 | 3: 2 5 | 4: 3 6 | 5: 5 7 | 6: 8 8 | 7: 13 9 | 8: 21 10 | 9: 34 11 | 10: 55 12 | 11: 89 13 | 12: 144 14 | 13: 233 15 | 14: 377 16 | 15: 610 17 | 16: 987 18 | 17: 1597 19 | 18: 2584 20 | 19: 4181 21 | 20: 6765 22 | 21: 10946 23 | 22: 17711 24 | 23: 28657 25 | 24: 46368 26 | 25: 75025 27 | 26: 121393 28 | 27: 196418 29 | 28: 317811 30 | 29: 514229 31 | 30: 832040 32 | 31: 1346269 33 | 32: 2178309 34 | 33: 3524578 35 | 34: 5702887 36 | 35: 9227465 37 | 36: 14930352 38 | 37: 24157817 39 | 38: 39088169 40 | 39: 63245986 41 | 40: 102334155 42 | 41: 165580141 43 | 42: 267914296 44 | 43: 433494437 45 | 44: 701408733 46 | 45: 1134903170 47 | 46: 1836311903 48 | 47: 2971215073 49 | 48: 4807526976 50 | 49: 7778742049 51 | 50: 12586269025 52 | 51: 20365011074 53 | 52: 32951280099 54 | 53: 53316291173 55 | 54: 86267571272 56 | 55: 139583862445 57 | 56: 225851433717 58 | 57: 365435296162 59 | 58: 591286729879 60 | 59: 956722026041 61 | 60: 1548008755920 62 | 61: 2504730781961 63 | 62: 4052739537881 64 | 63: 6557470319842 65 | 64: 10610209857723 66 | 65: 17167680177565 67 | 66: 27777890035288 68 | 67: 44945570212853 69 | 68: 72723460248141 70 | 69: 117669030460994 71 | 70: 190392490709135 72 | 71: 308061521170129 73 | 72: 498454011879264 74 | 73: 806515533049393 75 | 74: 1304969544928657 76 | 75: 2111485077978050 77 | 76: 3416454622906707 78 | 77: 5527939700884757 79 | 78: 8944394323791464 80 | 79: 14472334024676221 81 | 80: 23416728348467685 82 | 81: 37889062373143906 83 | 82: 61305790721611591 84 | 83: 99194853094755497 85 | 84: 160500643816367088 86 | 85: 259695496911122585 87 | 86: 420196140727489673 88 | 87: 679891637638612258 89 | 88: 1100087778366101931 90 | 89: 1779979416004714189 91 | 90: 2880067194370816120 92 | 91: 4660046610375530309 93 | 92: 7540113804746346429 94 | 93: 12200160415121876738 95 | -------------------------------------------------------------------------------- /test/fib-test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "fib.h" 3 | 4 | int main() { 5 | for (int i = 0; i <= 93; i++) { 6 | printf("%d: %lu\n", i, wasm_fib_fib(i)); 7 | } 8 | } -------------------------------------------------------------------------------- /test/fib.wast: -------------------------------------------------------------------------------- 1 | (module 2 | (func (export "fib") (param $n i32) (result i64) 3 | (local $prevPrev i64) 4 | (local $prev i64) 5 | (set_local $prevPrev (i64.const 1)) 6 | (loop $computeNext 7 | (if (get_local $n) (then 8 | (i64.add (get_local $prevPrev) (get_local $prev)) 9 | (set_local $prevPrev (get_local $prev)) 10 | (set_local $prev) 11 | (set_local $n (i32.sub (get_local $n) (i32.const 1))) 12 | (br $computeNext) 13 | )) 14 | ) 15 | (get_local $prev) 16 | ) 17 | ) -------------------------------------------------------------------------------- /test/main.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process' 2 | import * as fs from 'fs' 3 | import {promisify} from 'util' 4 | import test, {ExecutionContext} from 'ava' 5 | import {INVALID_EXPORT_CHAR} from '../compile/conventions' 6 | import {parse, SExpression} from './parse-s' 7 | 8 | process.chdir(__dirname) 9 | 10 | const CC = 'clang' 11 | const OPENSSL_CFLAGS = [ 12 | '-I/usr/local/opt/openssl/include', 13 | '-L/usr/local/opt/openssl/lib', 14 | '-lcrypto' 15 | ] 16 | const C_NAN = 'NAN' 17 | const SUCCESS = 'success' 18 | const SPEC_TESTS = [ 19 | 'align', 20 | 'address', 21 | 'block', 22 | 'br', 23 | 'br_if', 24 | 'br_table', 25 | 'break-drop', 26 | 'call', 27 | 'call_indirect', 28 | // 'conversions', requires support for nan:0x123abc expressions 29 | 'endianness', 30 | 'f32', 31 | 'f32_bitwise', 32 | 'f32_cmp', 33 | 'f64', 34 | 'f64_bitwise', 35 | 'f64_cmp', 36 | 'fac', 37 | 'float_exprs', 38 | // 'float_memory', requires support for nan:0x123abc expressions 39 | 'float_misc', 40 | 'forward', 41 | 'func', 42 | 'global', 43 | 'i32', 44 | 'i64', 45 | 'if', 46 | 'int_exprs', 47 | 'int_literals', 48 | 'labels', 49 | 'left-to-right', 50 | 'load', 51 | 'local_get', 52 | 'local_set', 53 | 'local_tee', 54 | 'loop', 55 | 'memory', 56 | 'memory_grow', 57 | 'memory_redundancy', 58 | 'memory_size', 59 | 'memory_trap', 60 | 'nop', 61 | 'return', 62 | 'select', 63 | 'stack', 64 | 'store', 65 | 'switch', 66 | 'unwind' 67 | ] 68 | const PROGRAM_TESTS = [ 69 | {name: 'sha256', flags: OPENSSL_CFLAGS}, 70 | {name: 'trig', flags: ['-lm']}, 71 | {name: 'params', flags: []} 72 | ] 73 | const FUNC_NAME = /^"(.+)"$/ 74 | const TESTS_START = /\n\((?:assert_return|assert_trap|invoke)/ 75 | const C_FILE_START = (test: string) => ` 76 | #include 77 | #include 78 | #include 79 | #include "${test}.h" 80 | 81 | int main() { 82 | ` 83 | const C_FILE_END = ` 84 | puts("${SUCCESS}"); 85 | } 86 | ` 87 | 88 | const exec = promisify(childProcess.exec), 89 | execFile = promisify(childProcess.execFile), 90 | readFile = promisify(fs.readFile), 91 | writeFile = promisify(fs.writeFile) 92 | 93 | async function compileTest(test: string, ...ccArgs: string[]): Promise { 94 | const wasmFile = test + '.wasm' 95 | await execFile('wabt/bin/wat2wasm', [test + '.wast', '-o', wasmFile]) 96 | await execFile('node', ['../main.js', wasmFile]) 97 | const runPath = test + '-test' 98 | await execFile(CC, [test + '.s', runPath + '.c', '-o', runPath, ...ccArgs]) 99 | return './' + runPath 100 | } 101 | const checkSuccessOutput = (t: ExecutionContext, {stdout}: {stdout: string}) => 102 | t.is(stdout, SUCCESS + '\n', 'Unexpected output:\n' + stdout) 103 | const exportName = (name: string): string => name.replace(INVALID_EXPORT_CHAR, '_') 104 | function getValue(expression: SExpression): string { 105 | if (expression.type === 'atom') { 106 | throw new Error('Invalid expression: ' + JSON.stringify(expression)) 107 | } 108 | let [op, value] = expression.items.map(item => { 109 | if (item.type !== 'atom') { 110 | throw new Error('Invalid expression: ' + JSON.stringify(expression)) 111 | } 112 | return item.atom 113 | }) 114 | switch (op) { 115 | case 'i32.const': 116 | case 'i64.const': 117 | case 'f32.const': 118 | case 'f64.const': { 119 | value = value.replace(/_/g, '') 120 | if (value === 'inf' || value === '-inf') { 121 | return value.replace('inf', 'INFINITY') 122 | } 123 | if (value.startsWith('nan')) return C_NAN 124 | if (value.startsWith('-nan')) return '-' + C_NAN 125 | 126 | if (op === 'i64.const') value += 'L' 127 | else if (op[0] === 'f') { 128 | try { 129 | BigInt(value) 130 | value += (value.startsWith('0x') ? 'p' : 'e') + '0' 131 | } 132 | catch {} 133 | if (op === 'f32.const') value += 'F' 134 | } 135 | return value 136 | } 137 | default: 138 | throw new Error('Unknown value type: ' + op) 139 | } 140 | } 141 | 142 | for (const testName of SPEC_TESTS) { 143 | test(testName, async t => { 144 | const wastPath = `spec/test/core/${testName}.wast` 145 | let testFile = await readFile(wastPath, 'utf8') 146 | let assertStartMatch: RegExpMatchArray | null 147 | let testCount = 0 148 | while (assertStartMatch = TESTS_START.exec(testFile)) { 149 | const assertsStart = assertStartMatch.index! 150 | const modulePath = wastPath.replace('.wast', '-module.wast') 151 | const nextModule = testFile.indexOf('\n(module', testFile.indexOf('(module')) 152 | if (nextModule >= 0 && nextModule < assertsStart) { // no asserts to test 153 | testFile = testFile.slice(nextModule) 154 | continue 155 | } 156 | await writeFile(modulePath, testFile.slice(0, assertsStart)) 157 | const wasmPath = wastPath.replace('.wast', '.wasm') 158 | await execFile('wabt/bin/wat2wasm', [modulePath, '-o', wasmPath]) 159 | await execFile('node', ['../main.js', wasmPath]) 160 | 161 | let cFile = C_FILE_START(testName) 162 | const hFilePath = wasmPath.replace('.wasm', '.h') 163 | const headerFile = await readFile(hFilePath, 'utf8') 164 | const initFunction = `wasm_${exportName(testName)}_init_module` 165 | const hasInit = headerFile.includes(`void ${initFunction}(void);`) 166 | if (hasInit) cFile += initFunction + '();\n' 167 | let asserts = nextModule < 0 168 | ? testFile.slice(assertsStart) 169 | : testFile.slice(assertsStart, nextModule) 170 | makeTests: while (TESTS_START.test(asserts)) { 171 | const {result, rest} = parse(asserts) 172 | if (result.type === 'atom') throw new Error('Not a test: ' + JSON.stringify(result)) 173 | const [op, ...args] = result.items 174 | if (op.type !== 'atom') throw new Error('Not a test: ' + JSON.stringify(result)) 175 | let processed: boolean 176 | switch (op.atom) { 177 | case 'module': 178 | break makeTests 179 | case 'assert_return': 180 | case 'invoke': 181 | let invoke: SExpression, expected: SExpression | undefined 182 | if (op.atom === 'invoke') invoke = result 183 | else [invoke, expected] = args as [SExpression, SExpression?] 184 | if (invoke.type === 'atom') throw new Error('Expected an invocation') 185 | const [invokeOp, func, ...funcArgs] = invoke.items 186 | if (!(invokeOp.type === 'atom' && invokeOp.atom === 'invoke') || func.type === 'list') { 187 | throw new Error('Expected an invocation') 188 | } 189 | const funcNameMatch = FUNC_NAME.exec(func.atom) 190 | if (!funcNameMatch) throw new Error('Not a function name: ' + func.atom) 191 | const funcName = exportName(`wasm_${testName}_${funcNameMatch[1]}`) 192 | const functionCall = `${funcName}(${funcArgs.map(getValue).join(', ')})` 193 | if (expected) { 194 | const value = getValue(expected) 195 | cFile += value.includes(C_NAN) 196 | ? `\tassert(isnan(${functionCall}));\n` 197 | : `\tassert(${functionCall} == ${value});\n` 198 | } 199 | else cFile += functionCall + ';\n' 200 | processed = true 201 | break 202 | case 'assert_exhaustion': 203 | case 'assert_invalid': 204 | case 'assert_malformed': 205 | case 'assert_return_arithmetic_nan': 206 | case 'assert_return_canonical_nan': 207 | case 'assert_trap': 208 | processed = false 209 | break 210 | default: 211 | throw new Error('Unknown assertion type: ' + op.atom) 212 | } 213 | if (processed) testCount++ 214 | asserts = rest 215 | } 216 | cFile += C_FILE_END 217 | testFile = testFile.slice(nextModule) 218 | const sFilePath = hFilePath.replace('.h', '.s'), 219 | cFilePath = sFilePath.replace('.s', '-test.c'), 220 | runPath = cFilePath.replace('.c', '') 221 | await writeFile(cFilePath, cFile) 222 | await execFile(CC, [sFilePath, cFilePath, '-o', runPath]) 223 | checkSuccessOutput(t, await execFile(runPath)) 224 | } 225 | t.log(`Passed ${testCount} tests`) 226 | }) 227 | } 228 | 229 | test('fib', async t => { 230 | const {title} = t 231 | const runPath = await compileTest(title) 232 | await exec(`${runPath} | diff ${title}-expected.txt -`) 233 | t.pass() 234 | }) 235 | for (const {name, flags} of PROGRAM_TESTS) { 236 | test(name, async t => { 237 | const runPath = await compileTest(name, ...flags) 238 | checkSuccessOutput(t, await execFile(runPath)) 239 | }) 240 | } -------------------------------------------------------------------------------- /test/params-test.c: -------------------------------------------------------------------------------- 1 | #include "assert.h" 2 | #include 3 | #include "params.h" 4 | 5 | int main() { 6 | for (int i = 0; i < 32; i++) { 7 | assert(wasm_params_select_param(i, 8 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 9 | 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 10 | ) == 300810240 + i + 1); 11 | } 12 | puts("success"); 13 | } -------------------------------------------------------------------------------- /test/params.wast: -------------------------------------------------------------------------------- 1 | (module 2 | (func $helper 3 | (param $index i32) 4 | (param i32) 5 | (param f32) 6 | (param i64) 7 | (param f64) 8 | (param i32) 9 | (param f32) 10 | (param i64) 11 | (param f64) 12 | (param i32) 13 | (param f32) 14 | (param i64) 15 | (param f64) 16 | (param i32) 17 | (param f32) 18 | (param i64) 19 | (param f64) 20 | (param i32) 21 | (param f32) 22 | (param i64) 23 | (param f64) 24 | (param i32) 25 | (param f32) 26 | (param i64) 27 | (param f64) 28 | (param i32) 29 | (param f32) 30 | (param i64) 31 | (param f64) 32 | (param i32) 33 | (param f32) 34 | (param i64) 35 | (param f64) 36 | (result i32) 37 | 38 | (block 39 | (block 40 | (block 41 | (block 42 | (block 43 | (block 44 | (block 45 | (block 46 | (block 47 | (block 48 | (block 49 | (block 50 | (block 51 | (block 52 | (block 53 | (block 54 | (block 55 | (block 56 | (block 57 | (block 58 | (block 59 | (block 60 | (block 61 | (block 62 | (block 63 | (block 64 | (block 65 | (block 66 | (block 67 | (block 68 | (block 69 | (block 70 | (block 71 | (br_table 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 (local.get $index)) 72 | ) 73 | (return (local.get 1))) 74 | (return (i32.trunc_f32_u (local.get 2)))) 75 | (return (i32.wrap_i64 (local.get 3)))) 76 | (return (i32.trunc_f64_u (local.get 4)))) 77 | (return (local.get 5))) 78 | (return (i32.trunc_f32_u (local.get 6)))) 79 | (return (i32.wrap_i64 (local.get 7)))) 80 | (return (i32.trunc_f64_u (local.get 8)))) 81 | (return (local.get 9))) 82 | (return (i32.trunc_f32_u (local.get 10)))) 83 | (return (i32.wrap_i64 (local.get 11)))) 84 | (return (i32.trunc_f64_u (local.get 12)))) 85 | (return (local.get 13))) 86 | (return (i32.trunc_f32_u (local.get 14)))) 87 | (return (i32.wrap_i64 (local.get 15)))) 88 | (return (i32.trunc_f64_u (local.get 16)))) 89 | (return (local.get 17))) 90 | (return (i32.trunc_f32_u (local.get 18)))) 91 | (return (i32.wrap_i64 (local.get 19)))) 92 | (return (i32.trunc_f64_u (local.get 20)))) 93 | (return (local.get 21))) 94 | (return (i32.trunc_f32_u (local.get 22)))) 95 | (return (i32.wrap_i64 (local.get 23)))) 96 | (return (i32.trunc_f64_u (local.get 24)))) 97 | (return (local.get 25))) 98 | (return (i32.trunc_f32_u (local.get 26)))) 99 | (return (i32.wrap_i64 (local.get 27)))) 100 | (return (i32.trunc_f64_u (local.get 28)))) 101 | (return (local.get 29))) 102 | (return (i32.trunc_f32_u (local.get 30)))) 103 | (return (i32.wrap_i64 (local.get 31)))) 104 | (return (i32.trunc_f64_u (local.get 32)))) 105 | unreachable 106 | ) 107 | (func (export "select_param") 108 | (param $index i32) 109 | (param i32) 110 | (param f32) 111 | (param i64) 112 | (param f64) 113 | (param i32) 114 | (param f32) 115 | (param i64) 116 | (param f64) 117 | (param i32) 118 | (param f32) 119 | (param i64) 120 | (param f64) 121 | (param i32) 122 | (param f32) 123 | (param i64) 124 | (param f64) 125 | (param i32) 126 | (param f32) 127 | (param i64) 128 | (param f64) 129 | (param i32) 130 | (param f32) 131 | (param i64) 132 | (param f64) 133 | (param i32) 134 | (param f32) 135 | (param i64) 136 | (param f64) 137 | (param i32) 138 | (param f32) 139 | (param i64) 140 | (param f64) 141 | (result i32) 142 | 143 | (local $i32_sum i32) 144 | (local $f32_sum f32) 145 | (local $i64_sum i64) 146 | (local $f64_sum f64) 147 | 148 | (local.set $i32_sum 149 | (i32.add 150 | (i32.add 151 | (i32.add 152 | (i32.add 153 | (i32.add 154 | (i32.add 155 | (i32.add 156 | (local.get 1) 157 | (local.get 5) 158 | ) 159 | (local.get 9) 160 | ) 161 | (local.get 13) 162 | ) 163 | (local.get 17) 164 | ) 165 | (local.get 21) 166 | ) 167 | (local.get 25) 168 | ) 169 | (local.get 29) 170 | ) 171 | ) 172 | (local.set $f32_sum 173 | (f32.add 174 | (f32.add 175 | (f32.add 176 | (f32.add 177 | (f32.add 178 | (f32.add 179 | (f32.add 180 | (local.get 2) 181 | (local.get 6) 182 | ) 183 | (local.get 10) 184 | ) 185 | (local.get 14) 186 | ) 187 | (local.get 18) 188 | ) 189 | (local.get 22) 190 | ) 191 | (local.get 26) 192 | ) 193 | (local.get 30) 194 | ) 195 | ) 196 | (local.set $i64_sum 197 | (i64.add 198 | (i64.add 199 | (i64.add 200 | (i64.add 201 | (i64.add 202 | (i64.add 203 | (i64.add 204 | (local.get 3) 205 | (local.get 7) 206 | ) 207 | (local.get 11) 208 | ) 209 | (local.get 15) 210 | ) 211 | (local.get 19) 212 | ) 213 | (local.get 23) 214 | ) 215 | (local.get 27) 216 | ) 217 | (local.get 31) 218 | ) 219 | ) 220 | (local.set $f64_sum 221 | (f64.add 222 | (f64.add 223 | (f64.add 224 | (f64.add 225 | (f64.add 226 | (f64.add 227 | (f64.add 228 | (local.get 4) 229 | (local.get 8) 230 | ) 231 | (local.get 12) 232 | ) 233 | (local.get 16) 234 | ) 235 | (local.get 20) 236 | ) 237 | (local.get 24) 238 | ) 239 | (local.get 28) 240 | ) 241 | (local.get 32) 242 | ) 243 | ) 244 | (i32.add 245 | (call $helper 246 | (local.get $index) 247 | (local.get 1) 248 | (local.get 2) 249 | (local.get 3) 250 | (local.get 4) 251 | (local.get 5) 252 | (local.get 6) 253 | (local.get 7) 254 | (local.get 8) 255 | (local.get 9) 256 | (local.get 10) 257 | (local.get 11) 258 | (local.get 12) 259 | (local.get 13) 260 | (local.get 14) 261 | (local.get 15) 262 | (local.get 16) 263 | (local.get 17) 264 | (local.get 18) 265 | (local.get 19) 266 | (local.get 20) 267 | (local.get 21) 268 | (local.get 22) 269 | (local.get 23) 270 | (local.get 24) 271 | (local.get 25) 272 | (local.get 26) 273 | (local.get 27) 274 | (local.get 28) 275 | (local.get 29) 276 | (local.get 30) 277 | (local.get 31) 278 | (local.get 32) 279 | ) 280 | (i32.mul 281 | (i32.mul 282 | (i32.mul 283 | (local.get $i32_sum) 284 | (i32.trunc_f32_u (local.get $f32_sum)) 285 | ) 286 | (i32.wrap_i64 (local.get $i64_sum)) 287 | ) 288 | (i32.trunc_f64_u (local.get $f64_sum)) 289 | ) 290 | ) 291 | ) 292 | ) 293 | -------------------------------------------------------------------------------- /test/parse-s.ts: -------------------------------------------------------------------------------- 1 | const WHITESPACE = /\s/ 2 | 3 | export type SExpression 4 | = {type: 'atom', atom: string} 5 | | {type: 'list', items: SExpression[]} 6 | export interface ParseResult { 7 | result: T 8 | rest: string 9 | } 10 | 11 | function parseSpace(text: string): string { 12 | let i = 0 13 | while (true) { 14 | if (text[i] === ';') { 15 | while (text[++i] !== '\n'); 16 | } 17 | else if (!WHITESPACE.test(text[i])) break 18 | i++ 19 | } 20 | return text.slice(i) 21 | } 22 | function parseAtom(text: string): ParseResult { 23 | let i = 1 24 | if (text[0] === '"') { 25 | while (text[i++] !== '"') { 26 | if (text[i] === '\\') i++ // skip escaped character 27 | } 28 | } 29 | else { 30 | while (!(text[i] === ')' || text[i] === ';' || WHITESPACE.test(text[i]))) i++ 31 | } 32 | return {result: {type: 'atom', atom: text.slice(0, i)},rest: text.slice(i)} 33 | } 34 | function parseList(text: string): ParseResult { 35 | const items: SExpression[] = [] 36 | while (true) { 37 | text = parseSpace(text) 38 | if (text[0] === ')') break 39 | 40 | const {result, rest} = parse(text) 41 | items.push(result) 42 | text = rest 43 | } 44 | return {result: {type: 'list', items}, rest: text.slice(1)} 45 | } 46 | export function parse(text: string): ParseResult { 47 | text = parseSpace(text) 48 | return text[0] === '(' ? parseList(text.slice(1)) : parseAtom(text) 49 | } -------------------------------------------------------------------------------- /test/sha256-test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "sha256.h" 7 | 8 | unsigned char *wasm_SHA256(unsigned char *data, unsigned length) { 9 | wasm_sha256_fitInput(length); 10 | memcpy(wasm_sha256_memory_memory + wasm_sha256_INPUT_START, data, length); 11 | wasm_sha256_sha256(length); 12 | return wasm_sha256_memory_memory; 13 | } 14 | 15 | void verify_hash(unsigned length) { 16 | unsigned char *data = malloc(length); 17 | memset(data, 'a', length); 18 | unsigned char *hash = SHA256(data, length, NULL); 19 | unsigned char *wasm_hash = wasm_SHA256(data, length); 20 | free(data); 21 | for (unsigned i = 0; i < SHA256_DIGEST_LENGTH; i++) { 22 | assert(hash[i] == wasm_hash[i]); 23 | } 24 | } 25 | 26 | int main() { 27 | wasm_sha256_init_module(); 28 | for (unsigned length = 0; length < 1 << 12; length++) verify_hash(length); 29 | verify_hash(1 << 24); 30 | puts("success"); 31 | } -------------------------------------------------------------------------------- /test/sha256.wast: -------------------------------------------------------------------------------- 1 | ;; Copied from https://github.com/calebsander/structure-bytes/blob/master/lib/sha256.wast 2 | (module 3 | ;; Memory layout: 4 | ;; 0 - 255: w; once hash has been computed, 0 - 31 is replaced with the result 5 | ;; 256 - 511: K 6 | ;; 512 - : buf to be hashed 7 | (global $K i32 (i32.const 256)) 8 | (global $INPUT_START (export "INPUT_START") i32 (i32.const 512)) 9 | (memory (export "memory") 1) 10 | (data (i32.const 256) "\98\2f\8a\42\91\44\37\71\cf\fb\c0\b5\a5\db\b5\e9\5b\c2\56\39\f1\11\f1\59\a4\82\3f\92\d5\5e\1c\ab\98\aa\07\d8\01\5b\83\12\be\85\31\24\c3\7d\0c\55\74\5d\be\72\fe\b1\de\80\a7\06\dc\9b\74\f1\9b\c1\c1\69\9b\e4\86\47\be\ef\c6\9d\c1\0f\cc\a1\0c\24\6f\2c\e9\2d\aa\84\74\4a\dc\a9\b0\5c\da\88\f9\76\52\51\3e\98\6d\c6\31\a8\c8\27\03\b0\c7\7f\59\bf\f3\0b\e0\c6\47\91\a7\d5\51\63\ca\06\67\29\29\14\85\0a\b7\27\38\21\1b\2e\fc\6d\2c\4d\13\0d\38\53\54\73\0a\65\bb\0a\6a\76\2e\c9\c2\81\85\2c\72\92\a1\e8\bf\a2\4b\66\1a\a8\70\8b\4b\c2\a3\51\6c\c7\19\e8\92\d1\24\06\99\d6\85\35\0e\f4\70\a0\6a\10\16\c1\a4\19\08\6c\37\1e\4c\77\48\27\b5\bc\b0\34\b3\0c\1c\39\4a\aa\d8\4e\4f\ca\9c\5b\f3\6f\2e\68\ee\82\8f\74\6f\63\a5\78\14\78\c8\84\08\02\c7\8c\fa\ff\be\90\eb\6c\50\a4\f7\a3\f9\be\f2\78\71\c6") 11 | (func $load32BE (param $loc i32) (result i32) 12 | (i32.or 13 | (i32.or 14 | (i32.shl (i32.load8_u (get_local $loc)) (i32.const 24)) 15 | (i32.shl (i32.load8_u offset=1 (get_local $loc)) (i32.const 16)) 16 | ) 17 | (i32.or 18 | (i32.shl (i32.load8_u offset=2 (get_local $loc)) (i32.const 8)) 19 | (i32.load8_u offset=3 (get_local $loc)) 20 | ) 21 | ) 22 | ) 23 | (func $store32BE (param $loc i32) (param $val i32) 24 | (i32.store8 offset=0 (get_local $loc) (i32.shr_u (get_local $val) (i32.const 24))) 25 | (i32.store8 offset=1 (get_local $loc) (i32.shr_u (get_local $val) (i32.const 16))) 26 | (i32.store8 offset=2 (get_local $loc) (i32.shr_u (get_local $val) (i32.const 8))) 27 | (i32.store8 offset=3 (get_local $loc) (get_local $val)) 28 | ) 29 | (func $store64BE (param $loc i32) (param $val i64) 30 | (call $store32BE ;; store32BE(loc, val >>> 32) 31 | (get_local $loc) 32 | (i32.wrap/i64 (i64.shr_u (get_local $val) (i64.const 32))) 33 | ) 34 | (call $store32BE ;; store32BE(loc + 4, val) 35 | (i32.add (get_local $loc) (i32.const 4)) 36 | (i32.wrap/i64 (get_local $val)) 37 | ) 38 | ) 39 | (func (export "fitInput") (param $byteLength i32) 40 | (local $needed i32) 41 | (set_local $needed ;; needed = INPUT_START + byteLength + 63 42 | ;; Could use up to 63 extra bytes 43 | (i32.add (get_local $byteLength) (i32.const 575)) ;; INPUT_START + 63 == 575 44 | ) 45 | (if ;; if (needed > 0) memory.grow(needed) 46 | (i32.gt_s 47 | (tee_local $needed ;; needed = (needed >> 16) + !!(needed % (1 << 16)) - memory.size 48 | (i32.sub 49 | (i32.add 50 | (i32.shr_u (get_local $needed) (i32.const 16)) 51 | (i32.ne (i32.and (get_local $needed) (i32.const 0xffff)) (i32.const 0)) 52 | ) 53 | (current_memory) 54 | ) 55 | ) 56 | (i32.const 0) 57 | ) 58 | (drop (grow_memory (get_local $needed))) 59 | ) 60 | ) 61 | (func (export "sha256") (param $byteLength i32) 62 | (local $messageLength i32) 63 | (local $i i32) (local $i4 i32) 64 | (local $temp1 i32) (local $temp2 i32) 65 | (local $a i32) (local $b i32) (local $c i32) (local $d i32) 66 | (local $e i32) (local $f i32) (local $g i32) (local $h i32) 67 | (local $chunkOrLengthStart i32) ;; used for both chunkStart and lengthStart 68 | (local $h0 i32) (local $h1 i32) (local $h2 i32) (local $h3 i32) 69 | (local $h4 i32) (local $h5 i32) (local $h6 i32) (local $h7 i32) 70 | (set_local $messageLength ;; messageLength = byteLength + extraBytes 71 | (i32.add 72 | ;; byteLength 73 | (get_local $byteLength) 74 | ;; extraBytes (== 72 - ((lBytes + 72) % 64)) 75 | (i32.sub 76 | (i32.const 72) 77 | (i32.and 78 | (i32.add (get_local $byteLength) (i32.const 72)) 79 | (i32.const 63) 80 | ) 81 | ) 82 | ) 83 | ) 84 | (set_local $i (get_local $byteLength)) 85 | (i32.store8 offset=512 (get_local $i) (i32.const 128)) ;; buf[byteLength] = (i8)128 86 | (set_local $chunkOrLengthStart ;; lengthStart = messageLength - 8 87 | (i32.sub (get_local $messageLength) (i32.const 8)) 88 | ) 89 | ;; for (i = byteLength + 1; i < lengthStart; i++) 90 | (loop $zeroBytes 91 | ;; if (++i < lengthStart) 92 | (i32.lt_u 93 | (tee_local $i (i32.add (get_local $i) (i32.const 1))) 94 | (get_local $chunkOrLengthStart) 95 | ) 96 | (if (then 97 | (i32.store8 offset=512 (get_local $i) (i32.const 0)) ;; buf[i] = 0 98 | (br $zeroBytes) ;; continue 99 | )) 100 | ) 101 | (call $store64BE ;; buf[lengthStart] = (i64)byteLength << 3 102 | (i32.add (get_global $INPUT_START) (get_local $chunkOrLengthStart)) 103 | (i64.shl (i64.extend_u/i32 (get_local $byteLength)) (i64.const 3)) 104 | ) 105 | (set_local $h0 (i32.const 0x6a09e667)) 106 | (set_local $h1 (i32.const 0xbb67ae85)) 107 | (set_local $h2 (i32.const 0x3c6ef372)) 108 | (set_local $h3 (i32.const 0xa54ff53a)) 109 | (set_local $h4 (i32.const 0x510e527f)) 110 | (set_local $h5 (i32.const 0x9b05688c)) 111 | (set_local $h6 (i32.const 0x1f83d9ab)) 112 | (set_local $h7 (i32.const 0x5be0cd19)) 113 | ;; for (chunkStart = 0; chunkStart < messageLength; chunkStart += 64) 114 | (set_local $chunkOrLengthStart (i32.const 0)) 115 | (loop $chunkLoop 116 | ;; for (i = 0; i < 16; i++) 117 | (set_local $i (i32.const 0)) 118 | (loop $initializeW 119 | (set_local $i4 (i32.shl (get_local $i) (i32.const 2))) ;; i4 = i << 2 120 | (i32.store align=2 ;; w[i] = buf.slice(chunkStart)[i] 121 | (get_local $i4) 122 | (call $load32BE 123 | (i32.add 124 | (get_global $INPUT_START) 125 | (i32.add (get_local $chunkOrLengthStart) (get_local $i4)) 126 | ) 127 | ) 128 | ) 129 | (br_if $initializeW ;; if (++i < 16) continue 130 | (i32.lt_u 131 | (tee_local $i (i32.add (get_local $i) (i32.const 1))) 132 | (i32.const 16) 133 | ) 134 | ) 135 | ) 136 | ;; for (i = 16; i < 64; i++) 137 | (loop $extendW 138 | (set_local $i4 (i32.shl (get_local $i) (i32.const 2))) ;; i4 = i << 2 139 | (set_local $temp1 ;; temp1 = w[i - 15] 140 | (i32.load align=2 (i32.sub (get_local $i4) (i32.const 60))) 141 | ) 142 | (set_local $temp2 ;; temp2 = w[i - 2] 143 | (i32.load align=2 (i32.sub (get_local $i4) (i32.const 8))) 144 | ) 145 | (i32.store align=2 ;; w[i] = w[i - 16] + s0 + w[i - 7] + s1 146 | (get_local $i4) 147 | (i32.add 148 | (i32.add 149 | ;; w[i - 16] 150 | (i32.load align=2 (i32.sub (get_local $i4) (i32.const 64))) 151 | ;; s0 (== rotr(temp1, 7) ^ rotr(temp1, 18) ^ (temp1 >>> 3)) 152 | (i32.xor 153 | (i32.rotr (get_local $temp1) (i32.const 7)) 154 | (i32.xor 155 | (i32.rotr (get_local $temp1) (i32.const 18)) 156 | (i32.shr_u (get_local $temp1) (i32.const 3)) 157 | ) 158 | ) 159 | ) 160 | (i32.add 161 | ;; w[i - 7] 162 | (i32.load align=2 (i32.sub (get_local $i4) (i32.const 28))) 163 | ;; s1 (== rotr(temp2, 17) ^ rotr(temp2, 19) ^ (temp2 >>> 10)) 164 | (i32.xor 165 | (i32.rotr (get_local $temp2) (i32.const 17)) 166 | (i32.xor 167 | (i32.rotr (get_local $temp2) (i32.const 19)) 168 | (i32.shr_u (get_local $temp2) (i32.const 10)) 169 | ) 170 | ) 171 | ) 172 | ) 173 | ) 174 | (br_if $extendW ;; if (++i < 64) continue 175 | (i32.lt_u 176 | (tee_local $i (i32.add (get_local $i) (i32.const 1))) 177 | (i32.const 64) 178 | ) 179 | ) 180 | ) 181 | (set_local $a (get_local $h0)) 182 | (set_local $b (get_local $h1)) 183 | (set_local $c (get_local $h2)) 184 | (set_local $d (get_local $h3)) 185 | (set_local $e (get_local $h4)) 186 | (set_local $f (get_local $h5)) 187 | (set_local $g (get_local $h6)) 188 | (set_local $h (get_local $h7)) 189 | ;; for (i = 0; i < 64; i++) 190 | (set_local $i (i32.const 0)) 191 | (loop $updateHash 192 | (set_local $i4 (i32.shl (get_local $i) (i32.const 2))) ;; i4 = i << 2 193 | (set_local $temp1 ;; temp1 = h + S1 + ch + K[i] + w[i] 194 | (i32.add 195 | (get_local $h) 196 | (i32.add 197 | (i32.add 198 | ;; S1 (== rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) 199 | (i32.xor 200 | (i32.rotr (get_local $e) (i32.const 6)) 201 | (i32.xor 202 | (i32.rotr (get_local $e) (i32.const 11)) 203 | (i32.rotr (get_local $e) (i32.const 25)) 204 | ) 205 | ) 206 | ;; ch (== (e & f) ^ (~e & g)) 207 | (i32.xor 208 | (i32.and (get_local $e) (get_local $f)) 209 | (i32.and 210 | (i32.xor (get_local $e) (i32.const -1)) ;; ~e 211 | (get_local $g) 212 | ) 213 | ) 214 | ) 215 | (i32.add 216 | ;; K[i] 217 | (i32.load align=2 (i32.add (get_global $K) (get_local $i4))) 218 | ;; w[i] 219 | (i32.load align=2 (get_local $i4)) 220 | ) 221 | ) 222 | ) 223 | ) 224 | (set_local $h (get_local $g)) ;; h = g 225 | (set_local $g (get_local $f)) ;; g = f 226 | (set_local $f (get_local $e)) ;; f = e 227 | (set_local $e (i32.add (get_local $d) (get_local $temp1))) ;; e = d + temp1 228 | (set_local $d (get_local $c)) ;; d = c 229 | (get_local $a) ;; preserve value of a 230 | (set_local $a ;; a = temp1 + temp2 231 | (i32.add 232 | (get_local $temp1) 233 | ;; temp2 (== S0 + maj) 234 | (i32.add 235 | ;; S0 (== rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) 236 | (i32.xor 237 | (i32.rotr (get_local $a) (i32.const 2)) 238 | (i32.xor 239 | (i32.rotr (get_local $a) (i32.const 13)) 240 | (i32.rotr (get_local $a) (i32.const 22)) 241 | ) 242 | ) 243 | ;; maj (== (a & b) ^ (a & c) ^ (b & c)) 244 | (i32.xor 245 | (i32.and (get_local $a) (get_local $b)) 246 | (i32.xor 247 | (i32.and (get_local $a) (get_local $c)) 248 | (i32.and (get_local $b) (get_local $c)) 249 | ) 250 | ) 251 | ) 252 | ) 253 | ) 254 | (set_local $c (get_local $b)) ;; c = b 255 | (set_local $b) ;; b = a 256 | (br_if $updateHash ;; if (++i < 64) continue 257 | (i32.lt_u 258 | (tee_local $i (i32.add (get_local $i) (i32.const 1))) 259 | (i32.const 64) 260 | ) 261 | ) 262 | ) 263 | (set_local $h0 (i32.add (get_local $h0) (get_local $a))) ;; h0 += a 264 | (set_local $h1 (i32.add (get_local $h1) (get_local $b))) ;; h1 += b 265 | (set_local $h2 (i32.add (get_local $h2) (get_local $c))) ;; h2 += c 266 | (set_local $h3 (i32.add (get_local $h3) (get_local $d))) ;; h3 += d 267 | (set_local $h4 (i32.add (get_local $h4) (get_local $e))) ;; h4 += e 268 | (set_local $h5 (i32.add (get_local $h5) (get_local $f))) ;; h5 += f 269 | (set_local $h6 (i32.add (get_local $h6) (get_local $g))) ;; h6 += g 270 | (set_local $h7 (i32.add (get_local $h7) (get_local $h))) ;; h7 += h 271 | (br_if $chunkLoop ;; if ((chunkStart += 64) < messageLength) continue 272 | (i32.lt_u 273 | (tee_local $chunkOrLengthStart (i32.add (get_local $chunkOrLengthStart) (i32.const 64))) 274 | (get_local $messageLength) 275 | ) 276 | ) 277 | ) 278 | ;; Write out result 279 | (call $store32BE (i32.const 0) (get_local $h0)) 280 | (call $store32BE (i32.const 4) (get_local $h1)) 281 | (call $store32BE (i32.const 8) (get_local $h2)) 282 | (call $store32BE (i32.const 12) (get_local $h3)) 283 | (call $store32BE (i32.const 16) (get_local $h4)) 284 | (call $store32BE (i32.const 20) (get_local $h5)) 285 | (call $store32BE (i32.const 24) (get_local $h6)) 286 | (call $store32BE (i32.const 28) (get_local $h7)) 287 | ) 288 | ) -------------------------------------------------------------------------------- /test/trig-test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "trig.h" 5 | 6 | #define SIN_TOLERANCE 7e-16 7 | #define COS_TOLERANCE (2 * SIN_TOLERANCE) 8 | 9 | int main() { 10 | wasm_trig_init_module(); 11 | for (double x = -10.0; x <= 10.0; x += 1e-4) { 12 | assert(fabs(wasm_trig_sin(x) - sin(x)) < SIN_TOLERANCE); 13 | assert(fabs(wasm_trig_cos(x) - cos(x)) < COS_TOLERANCE); 14 | } 15 | puts("success"); 16 | } -------------------------------------------------------------------------------- /test/trig.wast: -------------------------------------------------------------------------------- 1 | (module 2 | (global $MAX_TERM i32 (i32.const 21)) 3 | (global $HALF_PI f64 (f64.const 1.5707963267948966)) 4 | (global $PI f64 (f64.const 3.141592653589793)) 5 | (global $2PI f64 (f64.const 6.283185307179586)) 6 | ;; Computes a Maclaurin series approximation for sin(x) up to the x^MAX_TERM term 7 | (func $sin (export "sin") (param $x f64) (result f64) 8 | (local $sum f64) 9 | (local $pow i32) 10 | (local $term f64) 11 | ;; sum = 0 12 | ;; pow = 0 13 | ;; Get x in range (-PI, +PI) 14 | (set_local $x (f64.sub (get_local $x) ;; x -= 2PI * floor((x + PI) / 2PI) 15 | (f64.mul 16 | (get_global $2PI) 17 | (f64.floor (f64.div 18 | (f64.add (get_local $x) (get_global $PI)) 19 | (get_global $2PI) 20 | )) 21 | ) 22 | )) 23 | ;; Get x in range (-PI / 2, PI / 2) 24 | (if (f64.gt (f64.abs (get_local $x)) (get_global $HALF_PI)) (then ;; if (abs(x) > HALF_PI) 25 | (set_local $x (f64.sub ;; x = PI * sgn(x) - x 26 | (f64.copysign (get_global $PI) (get_local $x)) 27 | (get_local $x) 28 | )) 29 | )) 30 | (set_local $term (get_local $x)) ;; term = x 31 | (set_local $x (f64.mul (get_local $x) (get_local $x))) ;; x *= x 32 | (loop $add_terms (result f64) 33 | ;; sum += term 34 | (tee_local $sum (f64.add (get_local $sum) (get_local $term))) 35 | (if ;; if ((pow += 2) < MAX_TERM) 36 | (i32.le_u 37 | (tee_local $pow (i32.add (get_local $pow) (i32.const 2))) 38 | (get_global $MAX_TERM) 39 | ) 40 | (then 41 | (set_local $term (f64.mul ;; term *= -x / ((f32) pow * (f32) (pow + 1)) 42 | (get_local $term) 43 | (f64.div 44 | (f64.neg (get_local $x)) 45 | (f64.mul 46 | (f64.convert_s/i32 (get_local $pow)) 47 | (f64.convert_s/i32 (i32.add (get_local $pow) (i32.const 1))) 48 | ) 49 | ) 50 | )) 51 | (br $add_terms) ;; continue 52 | ) 53 | ) 54 | ) 55 | ;; return sum 56 | ) 57 | (func (export "cos") (param $x f64) (result f64) 58 | (call $sin (f64.add (get_local $x) (get_global $HALF_PI))) 59 | ) 60 | ) -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["*.ts"] 4 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": ["*.ts", "compile/*.ts", "parse/*.ts"] 67 | } -------------------------------------------------------------------------------- /util.ts: -------------------------------------------------------------------------------- 1 | export const makeArray = (len: number, init: (index: number) => A): A[] => 2 | new Array(len).fill(0).map((_, i) => init(i)) 3 | export function* reverse(arr: A[]): Generator { 4 | for (let i = arr.length - 1; i >= 0; i--) yield arr[i] 5 | } 6 | 7 | export function convertFloatToInt(value: number, wide: boolean): number | bigint { 8 | const dataView = new DataView(new ArrayBuffer(wide ? 8 : 4)) 9 | if (wide) dataView.setFloat64(0, value, true) 10 | else dataView.setFloat32(0, value, true) 11 | return wide ? dataView.getBigInt64(0, true) : dataView.getInt32(0, true) 12 | } --------------------------------------------------------------------------------