Skip to content

Commit

Permalink
Merge pull request #834 from jasonpaulos/teal-source-map-improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonpaulos authored Nov 20, 2023
2 parents a4f44c6 + ccf6c68 commit d527840
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 49 deletions.
123 changes: 94 additions & 29 deletions src/logic/sourcemap.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
import * as vlq from 'vlq';

export class SourceMap {
version: number;
sources: string[];
names: string[];
mappings: string;
/**
* Represents a location in a source file.
*/
export interface SourceLocation {
line: number;
column: number;
sourceIndex: number;
nameIndex?: number;
}

/**
* Represents the location of a specific PC in a source line.
*/
export interface PcLineLocation {
pc: number;
column: number;
nameIndex?: number;
}

/**
* Contains a mapping from TEAL program PC to source file location.
*/
export class ProgramSourceMap {
public readonly version: number;
/**
* A list of original sources used by the "mappings" entry.
*/
public readonly sources: string[];
/**
* A list of symbol names used by the "mappings" entry.
*/
public readonly names: string[];
/**
* A string with the encoded mapping data.
*/
public readonly mappings: string;

pcToLine: { [key: number]: number };
lineToPc: { [key: number]: number[] };
private pcToLocation: Map<number, SourceLocation>;

// Key is `${sourceIndex}:${line}`
private sourceAndLineToPc: Map<string, PcLineLocation[]>;

constructor({
version,
Expand All @@ -33,35 +66,67 @@ export class SourceMap {
'mapping undefined, cannot build source map without `mapping`'
);

const pcList = this.mappings.split(';').map((m) => {
const decoded = vlq.decode(m);
if (decoded.length > 2) return decoded[2];
return undefined;
});

this.pcToLine = {};
this.lineToPc = {};

let lastLine = 0;
for (const [pc, lineDelta] of pcList.entries()) {
// If the delta is not undefined, the lastLine should be updated with
// lastLine + the delta
if (lineDelta !== undefined) {
lastLine += lineDelta;
const pcList = this.mappings.split(';').map(vlq.decode);

this.pcToLocation = new Map();
this.sourceAndLineToPc = new Map();

const lastLocation: SourceLocation = {
line: 0,
column: 0,
sourceIndex: 0,
nameIndex: 0,
};
for (const [pc, data] of pcList.entries()) {
if (data.length < 4) continue;

const nameDelta = data.length > 4 ? data[4] : undefined;
const [, sourceDelta, lineDelta, columnDelta] = data;

lastLocation.sourceIndex += sourceDelta;
lastLocation.line += lineDelta;
lastLocation.column += columnDelta;
if (typeof nameDelta !== 'undefined') {
lastLocation.nameIndex += nameDelta;
}

if (!(lastLine in this.lineToPc)) this.lineToPc[lastLine] = [];
const sourceAndLineKey = `${lastLocation.sourceIndex}:${lastLocation.line}`;
let pcsForSourceAndLine = this.sourceAndLineToPc.get(sourceAndLineKey);
if (pcsForSourceAndLine === undefined) {
pcsForSourceAndLine = [];
this.sourceAndLineToPc.set(sourceAndLineKey, pcsForSourceAndLine);
}

this.lineToPc[lastLine].push(pc);
this.pcToLine[pc] = lastLine;
const pcInLine: PcLineLocation = {
pc,
column: lastLocation.column,
};
const pcLocation: SourceLocation = {
line: lastLocation.line,
column: lastLocation.column,
sourceIndex: lastLocation.sourceIndex,
};
if (typeof nameDelta !== 'undefined') {
pcInLine.nameIndex = lastLocation.nameIndex;
pcLocation.nameIndex = lastLocation.nameIndex;
}

pcsForSourceAndLine.push(pcInLine);
this.pcToLocation.set(pc, pcLocation);
}
}

getLineForPc(pc: number): number | undefined {
return this.pcToLine[pc];
getPcs(): number[] {
return Array.from(this.pcToLocation.keys());
}

getLocationForPc(pc: number): SourceLocation | undefined {
return this.pcToLocation.get(pc);
}

getPcsForLine(line: number): number[] | undefined {
return this.lineToPc[line];
getPcsOnSourceLine(sourceIndex: number, line: number): PcLineLocation[] {
const pcs = this.sourceAndLineToPc.get(`${sourceIndex}:${line}`);
if (pcs === undefined) return [];
return pcs;
}
}
6 changes: 5 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@ export {
verifyMultisig,
multisigAddress,
} from './multisig';
export { SourceMap } from './logic/sourcemap';
export {
ProgramSourceMap,
SourceLocation,
PcLineLocation,
} from './logic/sourcemap';

export * from './dryrun';
export * from './makeTxn';
Expand Down
113 changes: 113 additions & 0 deletions tests/8.LogicSig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,3 +690,116 @@ describe('signLogicSigTransaction', () => {
assert.deepStrictEqual(actual, expected);
});
});

describe('ProgramSourceMap', () => {
const input = {
version: 3,
sources: ['test/scripts/e2e_subs/tealprogs/sourcemap-test.teal'],
names: [],
mappings:
';;;;AAGA;;AAAmB;;AAAO;AAAI;;;AAE9B;;AAAO;;AAAO;AACd;;AAAO;AAAO;AALO;AAAI;AASrB;AACA',
};

const expectedLocations = new Map<number, algosdk.SourceLocation>([
[4, { sourceIndex: 0, line: 3, column: 0 }],
[6, { sourceIndex: 0, line: 3, column: 19 }],
[8, { sourceIndex: 0, line: 3, column: 26 }],
[9, { sourceIndex: 0, line: 3, column: 30 }],
[12, { sourceIndex: 0, line: 5, column: 0 }],
[14, { sourceIndex: 0, line: 5, column: 7 }],
[16, { sourceIndex: 0, line: 5, column: 14 }],
[17, { sourceIndex: 0, line: 6, column: 0 }],
[19, { sourceIndex: 0, line: 6, column: 7 }],
[20, { sourceIndex: 0, line: 6, column: 14 }],
[21, { sourceIndex: 0, line: 1, column: 21 }],
[22, { sourceIndex: 0, line: 1, column: 25 }],
[23, { sourceIndex: 0, line: 10, column: 4 }],
[24, { sourceIndex: 0, line: 11, column: 4 }],
]);

const expectedPcsForLine = new Map<number, algosdk.PcLineLocation[]>([
[
3,
[
{ pc: 4, column: 0 },
{ pc: 6, column: 19 },
{ pc: 8, column: 26 },
{ pc: 9, column: 30 },
],
],
[
5,
[
{ pc: 12, column: 0 },
{ pc: 14, column: 7 },
{ pc: 16, column: 14 },
],
],
[
6,
[
{ pc: 17, column: 0 },
{ pc: 19, column: 7 },
{ pc: 20, column: 14 },
],
],
[
1,
[
{ pc: 21, column: 21 },
{ pc: 22, column: 25 },
],
],
[10, [{ pc: 23, column: 4 }]],
[11, [{ pc: 24, column: 4 }]],
]);

it('should be able to read a ProgramSourceMap', () => {
const sourceMap = new algosdk.ProgramSourceMap(input);

assert.strictEqual(sourceMap.version, input.version);
assert.deepStrictEqual(sourceMap.sources, input.sources);
assert.deepStrictEqual(sourceMap.names, input.names);
assert.strictEqual(sourceMap.mappings, input.mappings);
});

describe('getLocationForPc', () => {
it('should return the correct location for all pcs', () => {
const sourceMap = new algosdk.ProgramSourceMap(input);
const maxPcToCheck = 30;

for (let pc = 0; pc < maxPcToCheck; pc++) {
const expected = expectedLocations.get(pc);
assert.deepStrictEqual(
sourceMap.getLocationForPc(pc),
expected,
`pc=${pc}`
);
}
});
});

describe('getPcs', () => {
it('should return the correct pcs', () => {
const sourceMap = new algosdk.ProgramSourceMap(input);
const expectedPcs = Array.from(expectedLocations.keys());
assert.deepStrictEqual(sourceMap.getPcs(), expectedPcs);
});
});

describe('getPcsForLine', () => {
it('should return the correct pcs for all lines', () => {
const sourceMap = new algosdk.ProgramSourceMap(input);
const maxLineToCheck = 15;

for (let line = 1; line <= maxLineToCheck; line++) {
const expected = expectedPcsForLine.get(line) || [];
assert.deepStrictEqual(
sourceMap.getPcsOnSourceLine(0, line),
expected,
`line=${line}`
);
}
});
});
});
45 changes: 27 additions & 18 deletions tests/cucumber/steps/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -4694,32 +4694,41 @@ module.exports = function getSteps(options) {

Given('a source map json file {string}', async function (srcmap) {
const js = await loadResourceAsJson(srcmap);
this.sourcemap = new algosdk.SourceMap(js);
this.sourcemap = new algosdk.ProgramSourceMap(js);
});

Then(
'the string composed of pc:line number equals {string}',
function (mapping) {
const buff = Object.entries(this.sourcemap.pcToLine).map(
([pc, line]) => `${pc}:${line}`
);
assert.deepStrictEqual(buff.join(';'), mapping);
}
);
Then('the source map contains pcs {string}', function (pcsString) {
const expectedPcs = makeArray(
...pcsString.split(',').map((pc) => parseInt(pc, 10))
);
const actualPcs = makeArray(...this.sourcemap.getPcs());
assert.deepStrictEqual(actualPcs, expectedPcs);
});

Then(
'getting the line associated with a pc {string} equals {string}',
function (pc, expectedLine) {
const actualLine = this.sourcemap.getLineForPc(parseInt(pc));
assert.deepStrictEqual(actualLine, parseInt(expectedLine));
'the source map maps pc {int} to line {int} and column {int} of source {string}',
function (pc, expectedLine, expectedColumn, source) {
const actual = this.sourcemap.getLocationForPc(pc);
assert.ok(actual);
assert.strictEqual(actual.line, expectedLine);
assert.strictEqual(actual.column, expectedColumn);
assert.strictEqual(this.sourcemap.sources[actual.sourceIndex], source);
}
);

Then(
'getting the last pc associated with a line {string} equals {string}',
function (line, expectedPc) {
const actualPcs = this.sourcemap.getPcsForLine(parseInt(line));
assert.deepStrictEqual(actualPcs.pop(), parseInt(expectedPc));
'the source map maps source {string} and line {int} to pc {int} at column {int}',
function (source, line, pc, expectedColumn) {
const sourceIndex = this.sourcemap.sources.indexOf(source);
assert.ok(sourceIndex >= 0);
const actualPcs = this.sourcemap.getPcsOnSourceLine(sourceIndex, line);
for (const actualPcInfo of actualPcs) {
if (actualPcInfo.pc === pc) {
assert.strictEqual(actualPcInfo.column, expectedColumn);
return;
}
}
throw new Error(`Could not find pc ${pc}`);
}
);

Expand Down
2 changes: 1 addition & 1 deletion tests/cucumber/unit.tags
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
@unit.responses.timestamp
@unit.responses.txid.json
@unit.responses.txngroupdeltas.json
@unit.sourcemap
@unit.sourcemapv2
@unit.stateproof.paths
@unit.stateproof.responses
@unit.stateproof.responses.msgp
Expand Down

0 comments on commit d527840

Please sign in to comment.