Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"chipnet",
"cleanstack",
"cleanup",
"cleanups",
"remappings",
"codegen",
"cryptocurrency",
"collateralized",
"datasig",
Expand Down
4 changes: 4 additions & 0 deletions packages/cashc/src/ast/AST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export class FunctionDefinitionNode extends Node implements Named {
symbolTable?: SymbolTable;
opRolls: Map<string, IdentifierNode> = new Map();

// Source provenance for debugging. Set on imported functions, left undefined for functions in the contract's own file.
sourceCode?: string;
sourceFile?: string;

constructor(
public kind: FunctionKind,
public name: string,
Expand Down
1 change: 1 addition & 0 deletions packages/cashc/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function compileString(code: string, compilerOptions: CompileOptions = {}
logs: optimisationResult.logs,
requires: optimisationResult.requires,
...(sourceTags ? { sourceTags } : {}),
...(traversal.frames.length > 0 ? { functions: traversal.frames } : {}),
};

const fingerprint = computeBytecodeFingerprintWithConstructorArgs(optimisationResult.script, constructorParamLength);
Expand Down
10 changes: 9 additions & 1 deletion packages/cashc/src/dependency-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,15 @@ function collectImports(
if (visitedPaths.has(absolutePath)) return [];
visitedPaths.add(absolutePath);

const importedAst = parseCode(readImportedFile(importNode, absolutePath), options.errorListener);
const importedSource = readImportedFile(importNode, absolutePath);
const importedAst = parseCode(importedSource, options.errorListener);

// Record source provenance so debug frames can attribute to the imported file
importedAst.functions.forEach((func) => {
func.sourceCode = importedSource;
func.sourceFile = path.basename(absolutePath);
});

return [...collect(importedAst.imports, path.dirname(absolutePath)), ...importedAst.functions];
});

Expand Down
27 changes: 23 additions & 4 deletions packages/cashc/src/generation/GenerateTargetTraversal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { hexToBin } from '@bitauth/libauth';
import { binToHex, hexToBin } from '@bitauth/libauth';
import {
asmToScript,
encodeBool,
Expand All @@ -12,7 +12,9 @@ import {
scriptToBytecode,
optimiseBytecode,
generateSourceMap,
generateSourceTags,
FullLocationData,
DebugFrame,
LogEntry,
RequireStatement,
PositionHint,
Expand Down Expand Up @@ -77,6 +79,7 @@ export default class GenerateTargetTraversal extends AstTraversal {
consoleLogs: LogEntry[] = [];
requires: RequireStatement[] = [];
sourceTags: SourceTagEntry[] = [];
frames: DebugFrame[] = [];
finalStackUsage: Record<string, StackItem> = {};

private scopeDepth = 0;
Expand Down Expand Up @@ -151,7 +154,7 @@ export default class GenerateTargetTraversal extends AstTraversal {
private defineGlobalFunctions(node: SourceFileNode): void {
node.functions.forEach((func) => {
const { functionId } = node.symbolTable!.getFromThis(func.name)!;
const bodyBytecode = this.compileGlobalFunctionBody(func);
const bodyBytecode = this.compileGlobalFunctionBody(func, functionId!);

const locationData = { location: func.location, positionHint: PositionHint.START };
this.emit(bodyBytecode, locationData); // <function_body_bytes>
Expand All @@ -160,7 +163,7 @@ export default class GenerateTargetTraversal extends AstTraversal {
});
}

private compileGlobalFunctionBody(node: FunctionDefinitionNode): Uint8Array {
private compileGlobalFunctionBody(node: FunctionDefinitionNode, functionId: number): Uint8Array {
const bodyTraversal = new GenerateTargetTraversal(this.compilerOptions);
bodyTraversal.currentFunction = node;
bodyTraversal.constructorParameterCount = 0;
Expand All @@ -183,7 +186,23 @@ export default class GenerateTargetTraversal extends AstTraversal {
0,
);

return scriptToBytecode(optimised.script);
const bodyBytecode = scriptToBytecode(optimised.script);
const sourceTags = generateSourceTags(optimised.sourceTags);

this.frames.push({
id: functionId,
name: node.name,
inputs: node.parameters.map((parameter) => ({ name: parameter.name, type: parameter.type.toString() })),
bytecode: binToHex(bodyBytecode),
sourceMap: generateSourceMap(optimised.locationData),
...(sourceTags ? { sourceTags } : {}),
...(node.sourceCode !== undefined ? { source: node.sourceCode } : {}),
...(node.sourceFile !== undefined ? { sourceFile: node.sourceFile } : {}),
logs: optimised.logs,
requires: optimised.requires,
});

return bodyBytecode;
}

cleanGlobalFunctionStack(node: FunctionDefinitionNode): void {
Expand Down
68 changes: 0 additions & 68 deletions packages/cashc/test/ast/Location.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import fs from 'fs';
import { URL } from 'url';
import { compileString } from '../../src/compiler.js';
import { parseCode } from '../../src/parser.js';
import { buildLineToAsmMap, bytecodeToAsm, bytecodeToScript } from '@cashscript/utils';
import { hexToBin } from '@bitauth/libauth';

describe('Location', () => {
it('should retrieve correct text from location', () => {
Expand All @@ -16,71 +13,6 @@ describe('Location', () => {
expect((f.location).text(code)).toEqual('function hello(sig s, pubkey pk) {\n require(checkSig(s, pk));\n }');
});

const wrap = (code: string): string => {
return `
contract test() {
function test() {
require(${code});
}
}`;
};

describe('Line to ASM map generation', () => {
const blocks = [
'1 < 1', '1 <= 1', '1 == 1', '1 != 1', '1 > 1', '1 >= 1',
'(1 - 1) == 1', '(1 + 1) == 1', '(1 * 1) == 1', '(1 / 1) == 1',
'(true && true) == true', '(true || true) == true',
'(0x01 & 0x01) == 0x01', '(0x01 | 0x01) == 0x01', '(0x01 ^ 0x01) == 0x01',
'"1" + "1" == "1"', '"1" + "1" != "1"', '"11".split(1)[0] == "1"', '"11".split(1)[1] == "1"',
'"1".reverse() == "1"', '"1".length == 1', '0x01.length == 1', '-333 == 1',
'tx.inputs[0].tokenAmount == 1',
'this.activeInputIndex == 1', 'tx.version == 1',
'abs(-1) == 1', 'within(1,1,1) == true', 'bytes(sha256(1)) == bytes(0x01)',
'checkSig(sig(0x00), pubkey(0x00))', 'checkMultiSig([sig(0x00), sig(0x00)], [pubkey(0x00), pubkey(0x00)])',
'checkDataSig(datasig(0x00), 0x00, pubkey(0x00))',
'tx.time >= 1', 'this.age >= 1',
'bytes(1) == 0x01', 'int(0x01) == 1',
];

blocks.forEach(block => {
it(`it should generate the same bytecode using line-to-asm map as the regular compiler for: ${block}`, () => {
{
const source = wrap(block);

// Compile the source code using regular CashScript compilation
const artifact = compileString(source);
const expected = bytecodeToAsm(hexToBin(artifact.debug!.bytecode));

// Generate the line-to-asm map from the source code
const opCodeMap = buildLineToAsmMap(
bytecodeToScript(hexToBin(artifact.debug!.bytecode)), artifact.debug!.sourceMap,
);

// Convert the line-to-asm map to CashScript bytecode to make sure that the generated opcode map matches the
// bytecode generated by CashScript
const received = Object.values(opCodeMap).join(' ')
.replaceAll('<0x', '').replaceAll('>', '').replace(/\s+/g, ' ');
expect(received).toBe(expected);
}

// Repeat the tests with the source code modified to test the position hint functionality
{
const source = wrap(block.replaceAll(' ', '\n').replaceAll(')', '\n)'))
.replaceAll('(\n)', '()').replace(/\((?!\))/g, '(\n');
const artifact = compileString(source);
const expected = bytecodeToAsm(hexToBin(artifact.debug!.bytecode));
const opCodeMap = buildLineToAsmMap(
bytecodeToScript(hexToBin(artifact.debug!.bytecode)), artifact.debug!.sourceMap,
);

const received = Object.values(opCodeMap).join(' ')
.replaceAll('<0x', '').replaceAll('>', '').replace(/\s+/g, ' ');
expect(received).toBe(expected);
}
});
});
});

it('should set the correct location points', () => {
const code = fs.readFileSync(new URL('../valid-contract-files/simple_functions.cash', import.meta.url), { encoding: 'utf-8' });
const ast = parseCode(code);
Expand Down
68 changes: 68 additions & 0 deletions packages/cashc/test/generation/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,17 @@ export const fixtures: Fixture[] = [
{ ip: 7, line: 7 },
],
sourceMap: '1::3:1;;::::1;7:16:7:25;;:29::30:0;:8::32:1',
functions: [
{
id: 0,
name: 'double',
inputs: [{ name: 'a', type: 'int' }],
bytecode: '5295',
sourceMap: '2:15:2:16;:11:::1',
logs: [],
requires: [],
},
],
},
source: fs.readFileSync(new URL('../valid-contract-files/global_function_simple.cash', import.meta.url), { encoding: 'utf-8' }),
compiler: {
Expand Down Expand Up @@ -1461,6 +1472,17 @@ export const fixtures: Fixture[] = [
{ ip: 8, line: 7 },
],
sourceMap: '1::3:1;;::::1;7:23:7:24:0;:16::25:1;;:29::30:0;:8::32:1',
functions: [
{
id: 0,
name: 'sub',
inputs: [{ name: 'a', type: 'int' }, { name: 'b', type: 'int' }],
bytecode: '94',
sourceMap: '2:11:2:16:1',
logs: [],
requires: [],
},
],
},
source: fs.readFileSync(new URL('../valid-contract-files/global_function_multi_param.cash', import.meta.url), { encoding: 'utf-8' }),
compiler: {
Expand Down Expand Up @@ -1494,6 +1516,17 @@ export const fixtures: Fixture[] = [
{ ip: 8, line: 8 },
],
sourceMap: '1::3:1;;::::1;7:24:7:25:0;:8::26:1;;8:20:8:23:0;:8::25:1',
functions: [
{
id: 0,
name: 'requirePositive',
inputs: [{ name: 'a', type: 'int' }],
bytecode: '00a069',
sourceMap: '2:16:2:17;:12:::1;:4::19',
logs: [],
requires: [{ ip: 2, line: 2 }],
},
],
},
source: fs.readFileSync(new URL('../valid-contract-files/global_function_void.cash', import.meta.url), { encoding: 'utf-8' }),
compiler: {
Expand Down Expand Up @@ -1533,6 +1566,41 @@ export const fixtures: Fixture[] = [
{ ip: 18, line: 6 },
],
sourceMap: '2::4:1;;::::1;1::3::0;;::::1;2::4::0;;::::1;6:19:6:20:0;:16::21:1;;:27::28:0;:24::29:1;;:16;:33::35:0;:8::37:1',
functions: [
{
id: 0,
name: 'm1',
inputs: [{ name: 'a', type: 'int' }],
bytecode: '518a5295',
sourceMap: '3:11:3:18:1;;:21::22:0;:11:::1',
logs: [],
requires: [],
source: fs.readFileSync(new URL('../import-fixtures/mid1.cash', import.meta.url), { encoding: 'utf-8' }),
sourceFile: 'mid1.cash',
},
{
id: 1,
name: 'leaf',
inputs: [{ name: 'a', type: 'int' }],
bytecode: '8b',
sourceMap: '2:11:2:16:1',
logs: [],
requires: [],
source: fs.readFileSync(new URL('../import-fixtures/leaf.cash', import.meta.url), { encoding: 'utf-8' }),
sourceFile: 'leaf.cash',
},
{
id: 2,
name: 'm2',
inputs: [{ name: 'a', type: 'int' }],
bytecode: '518a5393',
sourceMap: '3:11:3:18:1;;:21::22:0;:11:::1',
logs: [],
requires: [],
source: fs.readFileSync(new URL('../import-fixtures/mid2.cash', import.meta.url), { encoding: 'utf-8' }),
sourceFile: 'mid2.cash',
},
],
},
source: fs.readFileSync(new URL('../import-fixtures/diamond.cash', import.meta.url), { encoding: 'utf-8' }),
compiler: {
Expand Down
38 changes: 27 additions & 11 deletions packages/cashscript/src/Errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Artifact, RequireStatement, sourceMapToLocationData, Type } from '@cashscript/utils';
import { ResolvedFrame, rootFrame } from './debug-frame.js';

export class TypeError extends Error {
constructor(actual: string, expected: Type) {
Expand Down Expand Up @@ -134,12 +135,15 @@ export class FailedTransactionEvaluationError extends FailedTransactionError {
public inputIndex: number,
public bitauthUri: string,
public libauthErrorMessage: string,
frame?: ResolvedFrame,
) {
let message = `${artifact.contractName}.cash Error in transaction at input ${inputIndex} in contract ${artifact.contractName}.cash.\nReason: ${libauthErrorMessage}`;

if (artifact.debug) {
const { statement, lineNumber } = getLocationDataForInstructionPointer(artifact, failingInstructionPointer);
message = `${artifact.contractName}.cash:${lineNumber} Error in transaction at input ${inputIndex} in contract ${artifact.contractName}.cash at line ${lineNumber}.\nReason: ${libauthErrorMessage}\nFailing statement: ${statement}`;
const resolvedFrame = frame ?? rootFrame(artifact);
const { statement, lineNumber } = getLocationDataForFrame(resolvedFrame, failingInstructionPointer);
const context = formatFrameContext(resolvedFrame, artifact.contractName, lineNumber);
message = `${resolvedFrame.sourceName}:${lineNumber} Error in transaction at input ${inputIndex} ${context}.\nReason: ${libauthErrorMessage}\nFailing statement: ${statement}`;
}

super(message, bitauthUri);
Expand All @@ -154,10 +158,13 @@ export class FailedRequireError extends FailedTransactionError {
public inputIndex: number,
public bitauthUri: string,
public libauthErrorMessage?: string,
frame?: ResolvedFrame,
) {
const { statement, lineNumber } = getLocationDataForInstructionPointer(artifact, failingInstructionPointer);
const resolvedFrame = frame ?? rootFrame(artifact);
const { statement, lineNumber } = getLocationDataForFrame(resolvedFrame, failingInstructionPointer);
const context = formatFrameContext(resolvedFrame, artifact.contractName, lineNumber);

const baseMessage = `${artifact.contractName}.cash:${lineNumber} Require statement failed at input ${inputIndex} in contract ${artifact.contractName}.cash at line ${lineNumber}`;
const baseMessage = `${resolvedFrame.sourceName}:${lineNumber} Require statement failed at input ${inputIndex} ${context}`;
const baseMessageWithRequireMessage = `${baseMessage} with the following message: ${requireStatement.message}`;
const headline = `${requireStatement.message ? baseMessageWithRequireMessage : baseMessage}.`;

Expand All @@ -169,19 +176,28 @@ export class FailedRequireError extends FailedTransactionError {
}
}

const getLocationDataForInstructionPointer = (
artifact: Artifact,
const formatFrameContext = (frame: ResolvedFrame, contractName: string, lineNumber: number): string => {
if (frame.functionName) {
return `in contract ${contractName}, function ${frame.functionName} (${frame.sourceName}, line ${lineNumber})`;
}

return `in contract ${contractName}.cash at line ${lineNumber}`;
};

const getLocationDataForFrame = (
frame: ResolvedFrame,
instructionPointer: number,
): { lineNumber: number, statement: string } => {
const locationData = sourceMapToLocationData(artifact.debug!.sourceMap);
const locationData = sourceMapToLocationData(frame.sourceMap);

// We subtract the constructor inputs because these are present in the evaluation (and thus the instruction pointer)
// but they are not present in the source code (and thus the location data)
const modifiedInstructionPointer = instructionPointer - artifact.constructorInputs.length;
// We subtract the frame's ip offset (the constructor-arg prefix for the root frame, 0 for helper
// frames) because those pushes are present in the evaluation (and thus the instruction pointer) but
// not in the source code (and thus the location data).
const modifiedInstructionPointer = instructionPointer - frame.ipOffset;

const { location } = locationData[modifiedInstructionPointer];

const failingLines = artifact.source.split('\n').slice(location.start.line - 1, location.end.line);
const failingLines = frame.source.split('\n').slice(location.start.line - 1, location.end.line);

// Slice off the start and end of the statement's start and end lines to only return the failing part
// Note that we first slice off the end, to avoid shifting the end column index
Expand Down
Loading
Loading