@neo-one/smart-contract-compiler
Version:
NEO•ONE TypeScript smart contract compiler.
466 lines (464 loc) • 19.5 kB
JavaScript
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