UNPKG

@neo-one/smart-contract-compiler

Version:

NEO•ONE TypeScript smart contract compiler.

466 lines (464 loc) 19.5 kB
import { assertSysCall, BinaryWriter, ByteBuffer, common, crypto, Op, ScriptBuilder as ClientScriptBuilder, toSysCallHash, UnknownOpError, utils, } from '@neo-one/client-common'; import { tsUtils } from '@neo-one/ts-utils'; import { utils as commonUtils } from '@neo-one/utils'; import BN from 'bn.js'; import { SourceMapConsumer, SourceMapGenerator } from 'source-map'; import { DiagnosticCode } from '../../DiagnosticCode'; import { DiagnosticMessage } from '../../DiagnosticMessage'; import { declarations } from '../declaration'; import { expressions } from '../expression'; import { files } from '../file'; import { Call, DeferredProgramCounter, Jmp, Jump, Line, ProgramCounterHelper } from '../pc'; import { statements } from '../statement'; import { JumpTable } from './JumpTable'; import { resolveJumps } from './resolveJumps'; const compilers = [declarations, expressions, files, statements]; export class BaseScriptBuilder { constructor(context, helpers, sourceFile, contractInfo, linked = {}, allHelpers = []) { this.context = context; this.helpers = helpers; this.sourceFile = sourceFile; this.contractInfo = contractInfo; this.linked = linked; this.allHelpers = allHelpers; this.jumpTable = new JumpTable(); this.mutableBytecode = []; this.mutablePC = 0; this.jumpTablePC = new DeferredProgramCounter(); this.mutableProcessedByteCode = []; this.mutableCurrentTags = []; this.nodes = new Map(); this.mutableModuleMap = {}; this.mutableReverseModuleMap = {}; this.mutableExportMap = {}; this.mutableNextModuleIndex = 0; this.mutableCurrentModuleIndex = 0; this.mutableFeatures = { storage: false, dynamicInvoke: false }; this.compilers = compilers .reduce((acc, kindCompilers) => acc.concat(kindCompilers), []) .reduce((acc, kindCompilerClass) => { const kindCompiler = new kindCompilerClass(); if (acc[kindCompiler.kind] !== undefined) { throw new Error(`Found duplicate compiler for kind ${kindCompiler.kind}`); } acc[kindCompiler.kind] = kindCompiler; return acc; }, {}); } get scope() { if (this.mutableCurrentScope === undefined) { throw new Error('Scope has not been set'); } return this.mutableCurrentScope; } get moduleIndex() { return this.mutableCurrentModuleIndex; } process() { const sourceFile = this.sourceFile; const { bytecode } = this.capture(() => { const sourceFilePath = tsUtils.file.getFilePath(sourceFile); this.mutableModuleMap[sourceFilePath] = this.mutableNextModuleIndex; this.mutableReverseModuleMap[this.mutableNextModuleIndex] = sourceFilePath; this.mutableCurrentModuleIndex = this.mutableNextModuleIndex; this.mutableNextModuleIndex += 1; this.mutableCurrentScope = this.createScope(sourceFile, 0, undefined); this.nodes.set(sourceFile, 0); const options = {}; this.mutableCurrentScope.emit(this, sourceFile, options, (innerOptions) => { this.emitHelper(sourceFile, this.pushValueOptions(innerOptions), this.helpers.createGlobalObject); this.emitOp(sourceFile, 'DUP'); this.scope.setGlobal(this, sourceFile, this.pushValueOptions(innerOptions)); this.emitOp(sourceFile, 'DUP'); this.emitHelper(sourceFile, this.pushValueOptions(innerOptions), this.helpers.addEmptyModule); this.allHelpers.forEach((helper) => { if (helper.needsGlobal) { this.emitOp(sourceFile, 'DUP'); } helper.emitGlobal(this, sourceFile, innerOptions); }); this.emitOp(sourceFile, 'DROP'); this.visit(sourceFile, innerOptions); const contractInfo = this.contractInfo; if (contractInfo !== undefined) { this.emitHelper(contractInfo.smartContract, innerOptions, this.helpers.invokeSmartContract({ contractInfo, })); } this.scope.getGlobal(this, sourceFile, options); this.allHelpers.forEach((helper) => { if (helper.needsGlobalOut) { this.emitOp(sourceFile, 'DUP'); } helper.emitGlobalOut(this, sourceFile, innerOptions); }); this.emitOp(sourceFile, 'DROP'); }); }); this.mutableProcessedByteCode = bytecode; } getFinalResult(sourceMaps) { this.withProgramCounter((programCounter) => { this.emitJmp(this.sourceFile, 'JMP', programCounter.getLast()); this.jumpTablePC.setPC(programCounter.getCurrent()); this.jumpTable.emitTable(this, this.sourceFile); }); this.emitBytecode(this.mutableProcessedByteCode); const bytecode = resolveJumps(this.mutableBytecode); let pc = 0; const sourceMapGenerator = new SourceMapGenerator(); const addedFiles = new Set(); const mutableTagToLength = {}; const buffers = bytecode.map(([node, tags, value], idx) => { let finalValue; if (value instanceof Jump) { let jumpPCBuffer = Buffer.alloc(2, 0); const offsetPC = new BN(value.pc.getPC()).sub(new BN(pc)); const jumpPC = offsetPC.toTwos(16); try { if (jumpPC.fromTwos(16).toNumber() !== value.pc.getPC() - pc) { throw new Error(`Something went wrong, expected 2's complement of ${value.pc.getPC() - pc}, found: ${jumpPC .fromTwos(16) .toNumber()}`); } jumpPCBuffer = jumpPC.toArrayLike(Buffer, 'le', 2); } catch { this.context.reportError(node, DiagnosticCode.SomethingWentWrong, DiagnosticMessage.CompilationFailedPleaseReport); } const byteCodeBuffer = ByteBuffer[Op[value.op]]; if (byteCodeBuffer === undefined) { throw new Error('Something went wrong, could not find bytecode buffer'); } finalValue = Buffer.concat([byteCodeBuffer, jumpPCBuffer]); } else if (value instanceof Line) { const currentLine = new BN(idx + 1); const byteCodeBuffer = ByteBuffer[Op.PUSHBYTES4]; finalValue = Buffer.concat([byteCodeBuffer, currentLine.toArrayLike(Buffer, 'le', 4)]); } else { finalValue = value; } const sourceFile = tsUtils.node.getSourceFile(node); const filePath = tsUtils.file.getFilePath(sourceFile); const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); sourceMapGenerator.addMapping({ generated: { line: idx + 1, column: 0 }, original: { line: line + 1, column: character }, source: filePath, }); if (!addedFiles.has(filePath)) { addedFiles.add(filePath); sourceMapGenerator.setSourceContent(filePath, node.getSourceFile().getFullText()); } const tag = tags[0]; if (tag !== undefined) { const currentLength = mutableTagToLength[tag]; mutableTagToLength[tag] = currentLength === undefined ? finalValue.length : currentLength + finalValue.length; } pc += finalValue.length; return finalValue; }); const sourceMap = (async () => { await Promise.all(Object.entries(sourceMaps).map(async ([filePath, srcMap]) => { await SourceMapConsumer.with(srcMap, undefined, async (consumer) => { sourceMapGenerator.applySourceMap(consumer, filePath); }); })); return sourceMapGenerator.toJSON(); })(); return { code: Buffer.concat(buffers), sourceMap, features: this.mutableFeatures, }; } visit(node, options) { const compiler = this.compilers[node.kind]; if (compiler === undefined) { this.context.reportUnsupported(node); } else { compiler.visitNode(this, node, options); } } withScope(node, options, func) { let index = this.nodes.get(node); if (index === undefined) { index = 0; } else { index += 1; } this.nodes.set(node, index); const currentScope = this.mutableCurrentScope; this.mutableCurrentScope = this.createScope(node, index, currentScope); this.mutableCurrentScope.emit(this, node, options, func); this.mutableCurrentScope = currentScope; } withProgramCounter(func) { const pc = new ProgramCounterHelper(() => this.mutablePC); func(pc); pc.setLast(); } emitOp(node, code, buffer) { if (((code === 'APPCALL' || code === 'TAILCALL') && buffer !== undefined && buffer.equals(Buffer.alloc(20, 0))) || code === 'CALL_ED') { this.mutableFeatures = { ...this.mutableFeatures, dynamicInvoke: true }; } const bytecode = Op[code]; if (bytecode === undefined) { throw new UnknownOpError(code); } this.emitOpByte(node, bytecode, buffer); } emitPushInt(node, valueIn) { const value = new BN(valueIn); if (value.eq(utils.NEGATIVE_ONE)) { this.emitOp(node, 'PUSHM1'); } else if (value.eq(utils.ZERO)) { this.emitPush(node, utils.toSignedBuffer(value)); } else if (value.gt(utils.ZERO) && value.lt(utils.SIXTEEN)) { this.emitOpByte(node, Op.PUSH1 - 1 + value.toNumber()); } else { this.emitPush(node, utils.toSignedBuffer(value)); } } emitPushBoolean(node, value) { this.emitOp(node, value ? 'PUSH1' : 'PUSH0'); } emitPushString(node, value) { this.emitPush(node, this.toBuffer(value)); } emitPushBuffer(node, value) { this.emitPush(node, value); } emitJmp(node, code, pc) { this.emitJump(node, new Jmp(code, pc)); } emitHelper(node, options, helper) { const prevTags = this.mutableCurrentTags; this.mutableCurrentTags = [helper.constructor.name]; helper.emit(this, node, options); this.mutableCurrentTags = prevTags; } emitBytecode(bytecode) { const pc = this.mutablePC; bytecode.forEach(([node, tags, code]) => { if (code instanceof Call) { this.emitJump(node, code, tags); } else if (code instanceof Jmp) { this.emitJump(node, code.plus(pc), tags); } else { if (code instanceof Jump) { throw new Error('Something went wrong.'); } if (code instanceof Line) { this.emitLineRaw(node, code, tags); } else { this.emitRaw(node, code, tags); } } }); } emitCall(node) { this.emitJump(node, new Call(this.jumpTablePC)); } emitSysCall(node, name) { if (name === 'Neo.Storage.Put' || name === 'Neo.Storage.Delete') { this.mutableFeatures = { ...this.mutableFeatures, storage: true }; } const sysCallBuffer = Buffer.allocUnsafe(4); sysCallBuffer.writeUInt32LE(toSysCallHash(assertSysCall(name)), 0); const writer = new BinaryWriter(); writer.writeVarBytesLE(sysCallBuffer); this.emitOp(node, 'SYSCALL', writer.toBuffer()); } emitLine(node) { this.emitLineRaw(node, new Line()); } isCurrentSmartContract(node) { if (this.contractInfo === undefined) { return false; } const symbol = this.context.analysis.getSymbol(node); if (symbol === undefined) { return false; } const symbols = this.context.analysis.getSymbolAndAllInheritedSymbols(this.contractInfo.smartContract); if (symbols.some((smartContractSymbol) => smartContractSymbol === symbol)) { return true; } const typeSymbol = this.context.analysis.getTypeSymbol(node); return typeSymbol !== undefined && symbols.some((smartContractSymbol) => smartContractSymbol === typeSymbol); } loadModule(sourceFile) { const options = {}; let moduleIndex = this.mutableModuleMap[tsUtils.file.getFilePath(sourceFile)]; if (moduleIndex === undefined) { moduleIndex = this.mutableNextModuleIndex; this.mutableNextModuleIndex += 1; this.mutableModuleMap[tsUtils.file.getFilePath(sourceFile)] = moduleIndex; this.mutableReverseModuleMap[moduleIndex] = tsUtils.file.getFilePath(sourceFile); const currentScope = this.mutableCurrentScope; this.mutableCurrentScope = this.createScope(sourceFile, 0, undefined); const currentModuleIndex = this.mutableCurrentModuleIndex; this.mutableCurrentModuleIndex = moduleIndex; this.scope.getGlobal(this, sourceFile, this.pushValueOptions(options)); this.emitOp(sourceFile, 'DUP'); this.emitHelper(sourceFile, this.pushValueOptions(options), this.helpers.addEmptyModule); this.mutableCurrentScope.emit(this, sourceFile, options, (innerOptions) => { this.scope.setGlobal(this, sourceFile, options); this.visit(sourceFile, innerOptions); }); this.mutableCurrentScope = currentScope; this.mutableCurrentModuleIndex = currentModuleIndex; } this.scope.getGlobal(this, sourceFile, this.pushValueOptions(options)); this.emitHelper(sourceFile, this.pushValueOptions(options), this.helpers.getModule({ moduleIndex })); } capture(func) { const originalCapturedBytecode = this.mutableCapturedBytecode; this.mutableCapturedBytecode = []; const originalPC = this.mutablePC; this.mutablePC = 0; func(); const capturedBytecode = this.mutableCapturedBytecode; this.mutableCapturedBytecode = originalCapturedBytecode; const capturedLength = this.mutablePC; this.mutablePC = originalPC; return { length: capturedLength, bytecode: capturedBytecode }; } getLinkedScriptHash(node, filePath, smartContractClass) { const reportError = () => { this.context.reportError(node, DiagnosticCode.InvalidLinkedSmartContract, DiagnosticMessage.InvalidLinkedSmartContractMissing, smartContractClass); }; const fileLinked = this.linked[filePath]; if (fileLinked === undefined) { reportError(); return undefined; } const address = fileLinked[smartContractClass]; if (address === undefined) { reportError(); return undefined; } return crypto.addressToScriptHash({ addressVersion: common.NEO_ADDRESS_VERSION, address, }); } pushValueOptions(options) { return { ...options, pushValue: true }; } noPushValueOptions(options) { return { ...options, pushValue: false }; } setValueOptions(options) { return { ...options, setValue: true }; } noSetValueOptions(options) { return { ...options, setValue: false }; } noValueOptions(options) { return { ...options, pushValue: false, setValue: false }; } breakPCOptions(options, pc) { return { ...options, breakPC: pc }; } continuePCOptions(options, pc) { return { ...options, continuePC: pc }; } catchPCOptions(options, pc) { return { ...options, catchPC: pc }; } noCatchPCOptions(options) { return { ...options, catchPC: undefined }; } finallyPCOptions(options, pc) { return { ...options, finallyPC: pc }; } handleSuperConstructOptions(options, handleSuperConstruct) { return { ...options, handleSuperConstruct }; } castOptions(options, cast) { return { ...options, cast }; } noCastOptions(options) { return { ...options, cast: undefined }; } superClassOptions(options, superClass) { return { ...options, superClass }; } noSuperClassOptions(options) { return { ...options, superClass: undefined }; } hasExport(sourceFile, name) { const exported = this.mutableExportMap[tsUtils.file.getFilePath(sourceFile)]; return exported !== undefined && exported.has(name); } addExport(name) { const filePath = commonUtils.nullthrows(this.mutableReverseModuleMap[this.mutableCurrentModuleIndex]); let fileExports = this.mutableExportMap[filePath]; if (fileExports === undefined) { this.mutableExportMap[filePath] = fileExports = new Set(); } fileExports.add(name); } toBuffer(value) { return Buffer.from(value, 'utf8'); } emitPush(node, value) { if (value.length <= Op.PUSHBYTES75) { this.emitOpByte(node, value.length, value); } else if (value.length < 0x100) { this.emitOp(node, 'PUSHDATA1', new ClientScriptBuilder().emitUInt8(value.length).emit(value).build()); } else if (value.length < 0x10000) { this.emitOp(node, 'PUSHDATA2', new ClientScriptBuilder().emitUInt16LE(value.length).emit(value).build()); } else if (value.length < 0x100000000) { this.emitOp(node, 'PUSHDATA4', new ClientScriptBuilder().emitUInt32LE(value.length).emit(value).build()); } else { throw new Error('Value too large.'); } } emitOpByte(node, byteCode, buffer) { const byteCodeBuffer = ByteBuffer[byteCode]; let value = byteCodeBuffer; if (buffer !== undefined) { value = Buffer.concat([byteCodeBuffer, buffer]); } this.emitRaw(node, value); } emitRaw(node, value, tags = this.mutableCurrentTags) { this.push(node, tags, value); this.mutablePC += value.length; } emitJump(node, jump, tags = this.mutableCurrentTags) { this.push(node, tags, jump); this.mutablePC += 3; } emitLineRaw(node, line, tags = this.mutableCurrentTags) { this.push(node, tags, line); this.mutablePC += 5; } push(node, tags, value) { if (this.mutableCapturedBytecode !== undefined) { this.mutableCapturedBytecode.push([node, tags, value]); } else { this.mutableBytecode.push([node, tags, value]); } } } //# sourceMappingURL=BaseScriptBuilder.js.map