UNPKG

parkingsv-contract

Version:

ParkingSV library

365 lines (326 loc) 11.1 kB
import { basename, dirname, join } from 'path'; import { execSync } from 'child_process'; import { readFileSync, writeFileSync, unlinkSync, existsSync, rename } from 'fs'; import { oc } from 'ts-optchain'; import { ABIEntity, ABIEntityType } from './abi'; import { ContractDescription } from './contract'; import md5 = require('md5'); const SYNTAX_ERR_REG = /(?<filePath>[^\s]+):(?<line>\d+):(?<column>\d+):\n([^\n]+\n){3}(unexpected (?<unexpected>[^\n]+)\nexpecting (?<expecting>[^\n]+)|(?<message>[^\n]+))/g; const SEMANTIC_ERR_REG = /Error:\s*(?<filePath>[^\s]+):(?<line>\d+):(?<column>\d+)\n(?<message>[^\n]+)\n/g; const IMPORT_ERR_REG = /Syntax error:\s*(?<filePath>[^\s]+):(?<line>\d+):(?<column>\d+):\n([^\n]+\n){3}File not found: (?<fileName>[^\s]+)/g; export enum CompileErrorType { SyntaxError = 'SyntaxError', SemanticError = 'SemanticError', ImportError = 'ImportError' } export interface CompileErrorBase { type: string; filePath: string; position: { line: number; column: number; }; message: string; } export interface SyntaxError extends CompileErrorBase { type: CompileErrorType.SyntaxError; unexpected: string; expecting: string; } export interface SemanticError extends CompileErrorBase { type: CompileErrorType.SemanticError; } export interface ImportError extends CompileErrorBase { type: CompileErrorType.ImportError; file: string; } export type CompileError = SyntaxError | SemanticError | ImportError; export interface CompileResult { asm?: string; debugAsm?: DebugModeAsmWord[]; ast?: Record<string, unknown>; dependencyAsts?: Record<string, unknown>; abi?: Array<ABIEntity>; errors: CompileError[]; compilerVersion?: string; contract?: string; md5?: string; } export enum DebugModeTag { FuncStart = 'F0', FuncEnd = 'F1', LoopStart = 'L0' } export interface DebugModeAsmWord { file?: string; line?: number; endLine?: number; column?: number; endColumn: number; opcode: string; stack: string[]; debugTag?: DebugModeTag; } export function compile( source: { path: string, content?: string, }, settings: { ast?: boolean, asm?: boolean, debug?: boolean, desc?: boolean, outputDir?: string, outputToFiles?: boolean, cmdArgs?: string } = { asm: true, debug: true } ): CompileResult { const sourcePath = source.path; const srcDir = dirname(sourcePath); const sourceFileName = basename(sourcePath); const outputDir = settings.outputDir || srcDir; const outputFiles = {}; try { const sourceContent = source.content !== undefined ? source.content : readFileSync(sourcePath, 'utf8'); const cmd = `npx --no-install scryptc compile ${settings.asm || settings.desc ? '--asm' : ''} ${settings.ast || settings.desc ? '--ast' : ''} ${settings.debug == false ? '' : '--debug'} -r -o "${outputDir}" ${settings.cmdArgs ? settings.cmdArgs : ''}`; const output = execSync(cmd, { input: sourceContent, cwd: srcDir }).toString(); if (output.startsWith('Error:')) { if (output.includes('import') && output.includes('File not found')) { const importErrors: ImportError[] = [...output.matchAll(IMPORT_ERR_REG)].map(match => { const filePath = oc(match.groups).filePath(''); return { type: CompileErrorType.ImportError, filePath: getFullFilePath(filePath, srcDir, sourceFileName), message: `Imported file ${oc(match.groups).fileName()} does not exist`, position: { line: parseInt(oc(match.groups).line('-1')), column: parseInt(oc(match.groups).column('-1')), }, file: oc(match.groups).fileName('') }; }); return { errors: importErrors }; } else if (output.includes('Syntax error:')) { const syntaxErrors: CompileError[] = [...output.matchAll(SYNTAX_ERR_REG)].map(match => { const filePath = oc(match.groups).filePath(''); const unexpected = oc(match.groups).unexpected(''); const expecting = oc(match.groups).expecting(''); return { type: CompileErrorType.SyntaxError, filePath: getFullFilePath(filePath, srcDir, sourceFileName), position: { line: parseInt(oc(match.groups).line('-1')), column: parseInt(oc(match.groups).column('-1')), }, message: oc(match.groups).message(`unexpected ${unexpected}\nexpecting ${expecting}`), unexpected, expecting, }; }); return { errors: syntaxErrors }; } else { const semanticErrors: CompileError[] = [...output.matchAll(SEMANTIC_ERR_REG)].map(match => { const filePath = oc(match.groups).filePath(''); return { type: CompileErrorType.SemanticError, filePath: getFullFilePath(filePath, srcDir, sourceFileName), position: { line: parseInt(oc(match.groups).line('-1')), column: parseInt(oc(match.groups).column('-1')), }, message: oc(match.groups).message('') }; }); return { errors: semanticErrors }; } } const result: CompileResult = { errors: [] }; if (settings.ast || settings.desc) { const outputFilePath = getOutputFilePath(outputDir, 'ast'); outputFiles['ast'] = outputFilePath; const allAst = addSourceLocation(JSON.parse(readFileSync(outputFilePath, 'utf8')), srcDir); result.ast = allAst['stdin']; result.dependencyAsts = Object.keys(allAst) .filter(k => k !== 'stdin') .reduce((res, key) => { if (key === 'std') { res[key] = allAst[key]; } else { res[join(srcDir, key)] = allAst[key]; } return res; }, {}); } if (settings.asm || settings.desc) { const outputFilePath = getOutputFilePath(outputDir, 'asm'); outputFiles['asm'] = outputFilePath; if (settings.debug == false) { result.asm = readFileSync(outputFilePath, 'utf8'); } else { const asmObj = JSON.parse(readFileSync(outputFilePath, 'utf8')); const sources = asmObj.sources.map((s: string) => { return join(srcDir, s); }); result.debugAsm = asmObj.output.map(item => { const match = /^(?<fileIndex>-?\d+):(?<line>\d+):(?<col>\d+):(?<endLine>\d+):(?<endCol>\d+)(#(?<tagStr>.+))?/.exec(item.src); if (match && match.groups) { const fileIndex = parseInt(match.groups.fileIndex); let debugTag: DebugModeTag | undefined; const tagStr = match.groups.tagStr; if (/\w+\.\w+:0/.test(tagStr)) { debugTag = DebugModeTag.FuncStart; } if (/\w+\.\w+:1/.test(tagStr)) { debugTag = DebugModeTag.FuncEnd; } if (/loop:0/.test(tagStr)) { debugTag = DebugModeTag.LoopStart; } return { file: fileIndex > -1 ? sources[fileIndex] : undefined, line: sources[fileIndex] ? parseInt(match.groups.line) : undefined, endLine: sources[fileIndex] ? parseInt(match.groups.endLine) : undefined, column: sources[fileIndex] ? parseInt(match.groups.col) : undefined, endColumn: sources[fileIndex] ? parseInt(match.groups.endCol) : undefined, opcode: item.opcode, stack: item.stack, debugTag }; } throw new Error('Compile Failed: Asm output parsing Error!'); }); result.asm = result?.debugAsm?.map(item => item["opcode"].trim()).join(' '); } } if (settings.desc) { settings.outputToFiles = true; const { contract: name, abi } = getABIDeclaration(result.ast); const outputFilePath = getOutputFilePath(outputDir, 'desc'); outputFiles['desc'] = outputFilePath; const description: ContractDescription = { compilerVersion: compilerVersion(), contract: name, md5: md5(sourceContent), abi, asm: result.asm || "" }; writeFileSync(outputFilePath, JSON.stringify(description, null, 4)); result.compilerVersion = description.compilerVersion; result.contract = description.contract; result.md5 = description.md5; result.abi = abi; } return result; } finally { if (settings.outputToFiles) { Object.keys(outputFiles).forEach(outputType => { const file = outputFiles[outputType]; if (existsSync(file)) { if (settings[outputType]) { // rename all output files rename(file, file.replace('stdin', basename(sourcePath, '.scrypt')), () => { return; }); } else { unlinkSync(file); } } }); } else { // cleanup all output files Object.values<string>(outputFiles).forEach(file => { if (existsSync(file)) { unlinkSync(file); } }); } } } export function compilerVersion(): string { const text = execSync(`npx --no-install scryptc version`).toString(); // @ts-ignore return /Version:\s*([^\s]+)\s*/.exec(text)[1]; } function addSourceLocation(astRoot, basePath: string) { for (const fileName in astRoot) { const path = fileName === 'std' ? null : join(basePath, fileName); astRoot[fileName] = _addSourceLocationProperty(astRoot[fileName], path); } return astRoot; } function _addSourceLocationProperty(astObj, path: string | null) { if (!(astObj instanceof Object)) { return astObj; } for (const field in astObj) { const value = astObj[field]; if (field === 'src') { const matches = /:(\d+):(\d+):(\d+):(\d+)/.exec(value); if (!matches) { astObj.loc = null; } else { astObj.loc = { source: path, start: { line: parseInt(matches[1]), column: parseInt(matches[2]) }, end: { line: parseInt(matches[3]), column: parseInt(matches[4]) } }; } delete astObj['src']; } else if (value instanceof Object) { _addSourceLocationProperty(value, path); } } return astObj; } function getOutputFilePath(baseDir: string, target: 'ast' | 'asm' | 'desc'): string { return join(baseDir, `stdin_${target}.json`); } function getFullFilePath(relativePath: string, baseDir: string, curFileName: string): string { if (relativePath.endsWith('stdin')) { return join(baseDir, curFileName); // replace 'stdin' with real current compiling file name. } return join(baseDir, relativePath); } function getABIDeclaration(astRoot): { contract: string, abi: Array<ABIEntity> } { const mainContract = astRoot["contracts"][astRoot["contracts"].length - 1]; let pubIndex = 0; const interfaces: ABIEntity[] = mainContract['functions'] .filter(f => f['visibility'] === 'Public') .map(f => { const entity: ABIEntity = { type: ABIEntityType.FUNCTION, name: f['name'], index: f['nodeType'] === 'Constructor' ? undefined : pubIndex++, params: f['params'].map(p => { return { name: p['name'], type: p['type'] }; }), }; return entity; }); // explict constructor if (mainContract['construcotr']) { interfaces.push({ type: ABIEntityType.CONSTRUCTOR, name: 'constructor', params: mainContract['construcotr']['params'].map(p => { return { name: p['name'], type: p['type'] }; }), }); } else { // implicit constructor if (mainContract['properties']) { interfaces.push({ type: ABIEntityType.CONSTRUCTOR, name: 'constructor', params: mainContract['properties'].map(p => { return { name: p['name'].replace('this.', ''), type: p['type'] }; }), }); } } return { contract: mainContract['name'], abi: interfaces }; }