parkingsv-contract
Version:
ParkingSV library
365 lines (326 loc) • 11.1 kB
text/typescript
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
};
}